@momo-kits/native-kits 0.162.2-sp.7-debug → 0.162.2-sp.8-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 (160) hide show
  1. package/.claude/settings.local.json +15 -1
  2. package/CLAUDE.md +78 -0
  3. package/compose/build.gradle.kts +12 -31
  4. package/compose/build.gradle.kts.backup +11 -30
  5. package/compose/compose.podspec +1 -1
  6. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/OsFontScale.android.kt +7 -0
  7. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +2 -7
  8. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +8 -11
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +6 -24
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +4 -19
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +1 -10
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +2 -2
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +1 -1
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +1 -1
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +6 -17
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +1 -1
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +6 -101
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Localize.kt +269 -0
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +8 -19
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +8 -33
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +1 -1
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +22 -25
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeRibbon.kt +27 -29
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BaselineView.kt +3 -26
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +30 -31
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +3 -13
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +0 -5
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +3 -11
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Collapse.kt +3 -18
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +11 -11
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +42 -65
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +69 -26
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +1 -5
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +84 -59
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +13 -71
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +10 -51
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +6 -1
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +4 -21
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +1 -1
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +16 -75
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +5 -26
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +1 -1
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +3 -11
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +16 -40
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +3 -9
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +0 -28
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +2 -13
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +4 -0
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +4 -10
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +3 -28
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Stepper.kt +8 -47
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Steps.kt +4 -25
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/components/SuggestAction.kt +2 -11
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +1 -17
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +1 -6
  56. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +50 -111
  57. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +1 -5
  58. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +1 -5
  59. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +1 -12
  60. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +4 -96
  61. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +5 -7
  62. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Uploader.kt +2 -12
  63. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +9 -21
  64. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt +2 -2
  65. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +9 -14
  66. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +8 -11
  67. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +22 -0
  68. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +1 -1
  69. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +11 -20
  70. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +1 -1
  71. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +0 -5
  72. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +0 -2
  73. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/DeprecatedModifier.kt +94 -4
  74. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +0 -2
  75. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +2 -2
  76. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +3 -7
  77. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +9 -15
  78. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +36 -24
  79. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +48 -33
  80. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +67 -61
  81. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +6 -13
  82. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +13 -31
  83. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +1 -1
  84. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +6 -24
  85. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +11 -6
  86. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +2 -1
  87. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +9 -6
  88. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +1 -1
  89. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +3 -13
  90. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +3 -2
  91. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +1 -40
  92. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +0 -2
  93. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/OsFontScale.kt +9 -0
  94. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +0 -8
  95. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +2 -17
  96. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Tracking.kt +1 -1
  97. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +3 -3
  98. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +2 -3
  99. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +66 -0
  100. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +0 -2
  101. package/gradle/libs.versions.toml +2 -0
  102. package/gradle.properties +1 -1
  103. package/ios/Application/ApplicationEnvironment.swift +4 -2
  104. package/ios/Application/Components.swift +16 -0
  105. package/ios/Application/Localize.swift +277 -0
  106. package/ios/Application/MaxApi.swift +18 -0
  107. package/ios/Application/Navigation/BottomTab/BottomTab.swift +1 -0
  108. package/ios/Application/Navigation/NavigationContainer.swift +27 -3
  109. package/ios/Application/Navigation/Navigator.swift +50 -29
  110. package/ios/Application/Navigation/Overplay/BottomSheet.swift +87 -19
  111. package/ios/Application/Navigation/Overplay/ModalScreen.swift +6 -0
  112. package/ios/Application/Navigation/Tracking/ScreenTracker.swift +117 -0
  113. package/ios/Application/Screen.swift +1 -1
  114. package/ios/Application/StackScreen.swift +49 -1
  115. package/ios/Badge/BadgeRibbon.swift +80 -0
  116. package/ios/Button/Button.swift +68 -12
  117. package/ios/Carousel/Carousel.swift +16 -1
  118. package/ios/Checkbox/Checkbox.swift +14 -20
  119. package/ios/Chip/Chip.swift +35 -26
  120. package/ios/Collapse/Collapse.swift +11 -1
  121. package/ios/Colors+Radius+Spacing/Colors.swift +9 -6
  122. package/ios/Colors+Radius+Spacing/Spacing.swift +3 -1
  123. package/ios/DateTimePicker/DateTimePicker.swift +4 -2
  124. package/ios/DateTimePicker/WheelPicker.swift +7 -3
  125. package/ios/Extensions/ActiveOpacityButtonStyle.swift +14 -0
  126. package/ios/Extensions/Color++.swift +5 -2
  127. package/ios/Icon/Icon.swift +5 -1
  128. package/ios/IconButton/IconButton.swift +35 -7
  129. package/ios/Image/Image.swift +16 -3
  130. package/ios/Information/Information.swift +3 -1
  131. package/ios/Input/ErrorView.swift +3 -1
  132. package/ios/Input/Input.swift +31 -8
  133. package/ios/Input/InputPhoneNumber.swift +14 -3
  134. package/ios/Input/InputSearch.swift +7 -1
  135. package/ios/Input/InputTextArea.swift +78 -34
  136. package/ios/InputDropDown/InputDropDown.swift +4 -1
  137. package/ios/InputMoney/InputMoney.swift +16 -4
  138. package/ios/PARITY_AUDIT.md +641 -0
  139. package/ios/Popup/PopupDisplay.swift +3 -4
  140. package/ios/Popup/PopupNotify.swift +3 -4
  141. package/ios/Popup/PopupPromotion.swift +16 -8
  142. package/ios/ProgressInfo/ProgressInfo.swift +4 -2
  143. package/ios/Radio/Radio.swift +12 -13
  144. package/ios/Rating/Rating.swift +1 -1
  145. package/ios/Stepper/Stepper.swift +2 -2
  146. package/ios/Steps/Steps.swift +22 -6
  147. package/ios/SuggestAction/SuggestAction.swift +5 -3
  148. package/ios/Swipeable/SwipeCell.swift +5 -3
  149. package/ios/Switch/Switch.swift +53 -28
  150. package/ios/TabView/TabView.swift +18 -14
  151. package/ios/Template/TrustBanner/TrustBanner.swift +6 -7
  152. package/ios/Tooltip/Tooltip.swift +3 -0
  153. package/ios/Typography/FontScaleStore.swift +19 -0
  154. package/ios/Typography/Text.swift +3 -0
  155. package/ios/Typography/Typography.swift +8 -5
  156. package/ios/Uploader/Uploader.swift +0 -9
  157. package/ios/native-kits.podspec +1 -1
  158. package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
  159. package/package.json +1 -1
  160. package/ios/Colors+Radius+Spacing/MoMoUIKitsResources.swift +0 -2
@@ -0,0 +1,641 @@
1
+ # iOS Swift Kit Parity Audit
2
+
3
+ Tracks the ongoing parity pass between `compose/src/commonMain/kotlin/vn/momo/kits/` (source of truth)
4
+ and the hand-maintained native SwiftUI port under `ios/`. Appended to per tier/batch so the pass is
5
+ resumable across sessions without re-deriving prior findings.
6
+
7
+ ## Tier 0 — Tokens
8
+
9
+ - **Colors.swift**: fixed scrambled violet scale (violet04/05/06/09/10 held wrong Kotlin values, shifted
10
+ by 1-3 steps); added missing `violet04` value (`a03ff1`), `violet11`, `violet11Stroke`; added missing
11
+ `pinkMoMoBranding`; fixed `info` alias to reference `blue03` instead of a duplicated hardcoded hex.
12
+ No existing call sites used the wrong violet tokens (grepped, zero hits), so this was a pure bug fix
13
+ with no visual regression risk.
14
+ - **Spacing.swift**: added missing `Size4XL`(56)/`Size5XL`(64); renamed `XXXL`→`Size3XL` to match Kotlin
15
+ (zero external call sites found, so the rename is safe, not just aliased).
16
+ - **Typography.swift** (`ios/Typography/Typography.swift`): **no fix needed.** The initial audit flagged
17
+ missing strikethrough variants here, but this file's snake_case `Font` extension is dead code (zero
18
+ call sites anywhere in `ios/`). The actual typography system used throughout the port is
19
+ `TypographyStyle`/`MomoText` in `ios/Typography/Text.swift`, which already has full 1:1 parity with
20
+ Kotlin's `AppTypography`, including all three strikethrough variants. Flagged as stale/legacy, not
21
+ touched — removing dead code was out of scope for this pass.
22
+
23
+ ## Tier 1 — Navigation core
24
+
25
+ - **`Navigator.pop()` snackbar bug — fixed.** Kotlin's `pop()` special-cases `SNACK_BAR` to fall through
26
+ and pop the screen underneath (confirmed at `Navigator.kt:84-91`); Swift's `pop()` previously treated
27
+ any `overplay != nil` (including snackbar) as fully consuming the pop via `hideOverplay()`, silently
28
+ swallowing back-navigation while a snackbar was showing. Fixed by special-casing `.snackBar` to clear
29
+ the overplay immediately (no exit animation, matching Kotlin's `hardClearAfterDismiss()`) and falling
30
+ through to pop the screen. Extracted the screen-pop branch into a `popScreen() -> Bool` helper to avoid
31
+ duplicating it between the snackbar and normal branches while preserving the original "nothing left to
32
+ pop → request host dismiss and break" behavior. Internal control-flow only, no signature change.
33
+ - **`DynamicScreenRegistry` singleton → instance-scoped — fixed.** Was a process-wide `static let shared`
34
+ (`Navigator.swift:33`); Kotlin scopes one registry per `NavigationContainer` and disposes it
35
+ (`remember { DynamicScreenRegistry() }`). Converted to a `public let registry = DynamicScreenRegistry()`
36
+ owned by `Navigator`; disposal now happens implicitly via ARC when the `Navigator` (`@StateObject` in
37
+ `NavigationContainer`) deallocates, rather than needing an explicit teardown call. Contained to 2 files
38
+ (`Navigator.swift`, `NavigationContainer.swift` — the only two referencing the old singleton, confirmed
39
+ by repo-wide grep). **Behavior-changing**: multiple concurrent `NavigationContainer`s no longer share
40
+ global screen-ID state — call this out explicitly if this pass gets reviewed/merged.
41
+ - **Deferred, documented gaps (not fixed this pass):** Kotlin's `Navigation.kt` (per-screen mutable
42
+ `NavigationOptions` + `setOptions()` + `LocalNavigation`, letting a pushed screen imperatively mutate
43
+ its own header at runtime) has no Swift port at all — new API surface, bigger scope than a parity fix.
44
+ The `IsShowBaseLineDebug` global debug-border flag also has no Swift equivalent — low priority.
45
+ - Verified via full `iosApp` scheme build (`xcodebuild ... -scheme iosApp -destination 'id=EFD1594B-...'`,
46
+ iPhone 14 / iOS 16.4 simulator) — `** BUILD SUCCEEDED **`. This exercises the real `Demo/*.swift` call
47
+ sites, confirming no signature breakage from the registry/pop changes.
48
+
49
+ ## Tier 2 — Cross-cutting subsystems (Localize, ScreenTracker)
50
+
51
+ - **`ios/Application/MaxApi.swift` (new).** Minimal `MaxApi` protocol (`track`, `dismiss`, `getLanguage`)
52
+ mirroring `vn.momo.maxapi.IMaxApi` — scoped to exactly the 3 methods actually called anywhere in the
53
+ Kotlin `kits` source (confirmed by a repo-wide grep for `maxApi?.`/`api.` call sites: `ScreenTracker.kt`
54
+ calls `track`, `Navigator.kt` calls `dismiss`, `NavigationContainer.kt` calls `getLanguage`). `IMaxApi`
55
+ itself is an external compiled dependency with no source in this repo, so this is intentionally not a
56
+ guess at its full surface.
57
+ - **`ios/Application/Navigation/Tracking/ScreenTracker.swift` (new).** 1:1 port of `ScreenTracker.kt` —
58
+ `ScreenTrackingState`, `trackScreenNavigated/Displayed/Interacted`, `trackPopupDisplayed`,
59
+ `getLastScreenName`/`reset`. Pure logic, no platform dependency beyond `MaxApi`.
60
+ - **`ios/Application/Localize.swift` (new).** 1:1 port of `Localize.kt` — `LocalizationObject`,
61
+ `Localize` (`ObservableObject`, `@Published currentLanguage` so a language switch republishes to
62
+ consumers), `translate`/`translateData`(×2)/`addTranslations`/`changeLanguage`, and the full 101-key
63
+ VI/EN default dictionary. **Verified via scripted key-set AND value diff against `Localize.kt`: 0
64
+ mismatches** (all 202 key/value pairs across both languages transcribed exactly).
65
+ - **Wiring**: `ApplicationEnvironment` gained an additive `maxApi: MaxApi?` field (default `nil`).
66
+ `NavigationContainer` gained additive `maxApi`/`localize` params; on `.onAppear` it now calls
67
+ `maxApi?.getLanguage` and switches to `EN` only when the host reports it (mirrors Compose's
68
+ `LaunchedEffect(maxApi, resolvedLocalize)` + `parseLanguage`). `StackScreen` gained an additive
69
+ `screenName` param and a `.task(id: screenName)` that fires `trackScreenNavigated` immediately, then
70
+ `trackScreenDisplayed`/`trackScreenInteracted` after a 2s debounce — mirrors Compose's
71
+ `LaunchedEffect(name)` + `delay(2000ms)` exactly, including the once-computed push/back `action` value.
72
+ `ModalScreen`/`BottomSheetView` each gained `trackPopupDisplayed` in their existing `onAppear` guard.
73
+ `SnackBarView` intentionally gets **no** tracking call — confirmed Kotlin's `SnackBar.kt` doesn't call
74
+ `ScreenTracker` either, so this isn't a gap.
75
+ - **Demo-only verification aid**: `sample/iosApp/iosApp/Demo/ConsoleMaxApi.swift` (new, demo target only,
76
+ not shipped in the pod) implements `MaxApi` by logging every call; wired into `Home.swift`'s root
77
+ `NavigationContainer`. Added to the `iosApp` Xcode target via the `xcodeproj` Ruby gem (safer than
78
+ hand-editing `project.pbxproj`).
79
+ - **Verification**: `pod install` (new source files needed the Pods project regenerated to pick them up —
80
+ the podspec's `Application/**/*` glob already covered the new paths, just needed re-scanning), then both
81
+ `MoMoUIKits` and `iosApp` scheme builds — `** BUILD SUCCEEDED **`, zero signature-breakage. A live
82
+ simulator console-log smoke check was attempted (launch the demo app, capture stdout/stderr via
83
+ `simctl launch --stdout/--stderr` and via `log stream`) but did not surface `print()` output in this
84
+ sandboxed environment — GUI apps launched via `simctl` don't reliably pipe raw stdout the way a CLI
85
+ process does, independent of whether the tracking/localize code itself is correct. Not pursued further
86
+ since it's a documented "nice to have" per the plan, not the mandatory gate; the compile + dictionary-diff
87
+ verification already gives strong evidence the wiring is structurally correct.
88
+
89
+ ## Tier 3 — Component batches
90
+
91
+ ### Batch 1 — Static display atoms (Avatar, Badge, BadgeDot, BadgeRibbon, Divider, Icon, Image, Tag)
92
+
93
+ - **Avatar, Badge, BadgeDot, Divider**: full parity, no findings (only the already-deferred
94
+ `IsShowBaseLineDebug` debug border gap, consistent with Tier 1's decision to leave that undone).
95
+ - **BadgeRibbon**: `RoundedBadgeRibbon` (BadgeRibbon.kt:164-326) — a canvas-drawn ribbon shape sized to
96
+ its text — had **zero Swift port**. **Fixed**: added `ios/Badge/BadgeRibbon.swift`'s new
97
+ `RoundedBadgeRibbon` view, using SwiftUI `Canvas` + the file's existing `RoundedCorners` shape helper
98
+ for the per-corner-radius path math, and the file's pre-existing (previously unused)
99
+ `String.size(withAttributes:)` extension for text measurement — that extension being already present
100
+ but unused strongly suggests someone scaffolded this port before and didn't finish it. Colors
101
+ (`orange03`/`red01`) reference tokens instead of Kotlin's hardcoded hex literals. Flagged, not touched:
102
+ Swift's extra public `backgroundColor` param on `BadgeRibbon` (not in Kotlin's public API) — removing
103
+ would break existing callers for no parity benefit.
104
+ - **Icon**: two findings.
105
+ 1. Kotlin defaults `color` to the theme's text-default color (tinting every icon unless in
106
+ `noThemeIcons`); Swift defaults to `nil` (untinted). Changes rendering for most existing callers —
107
+ **flagged only**, not changed.
108
+ 2. Swift's public `accessibilityLabel` param was accepted but never applied anywhere in `body`. **Fixed**
109
+ — wired via `.accessibilityLabel(accessibilityLabel ?? "img|\(uri)")`, mirroring Kotlin's
110
+ auto-generated `contentDescription = "img|$iconUrl"` default while still honoring an explicit
111
+ override. Non-breaking (previously-dead prop becoming live is a strict improvement, not a behavior
112
+ change for existing callers who weren't setting it).
113
+ - **Image / ImageView**: several findings.
114
+ 1. Default `contentMode` differs (Kotlin crops by default, Swift fits) — changes default visual fit for
115
+ unconfigured callers. **Flagged only**, not changed.
116
+ 2. Hardcoded `Colors.black08` error-placeholder tint bypassing the theme (Kotlin uses
117
+ `theme.colors.text.disable`). **Fixed** — added `@Environment(\.appTheme)` and swapped in
118
+ `theme.colors.text.disable`.
119
+ 3. Missing `loading: Bool` toggle (to suppress the skeleton placeholder) and `onStateChanged` callback +
120
+ `ImageState` enum. **Fixed** — both added additively (defaults preserve current behavior:
121
+ `loading` defaults `true`, `onStateChanged` defaults `nil`).
122
+ 4. **Deferred, not fixed**: Kotlin's CDN size-optimization (`supportedDomains`/`sizeChart`/
123
+ `appendSizeQueryParam`, appending a `?size=` query param sized to the rendered width) and GIF
124
+ detection/`GifImage` support have no Swift equivalent. These are real feature gaps but substantially
125
+ larger scope than a parity bug fix (new URL-processing pipeline, new dependency for GIF playback) —
126
+ documented here rather than silently dropped.
127
+ - **Tag**: Kotlin's public `customColor: Color?` param has no Swift equivalent — **but confirmed dead code
128
+ even in the Kotlin source itself**: `Tag.kt:58-61`'s `if (primaryColors.contains(customColor))` branch
129
+ reassigns `tagColor`/`labelColor` to the exact values they already held, regardless of the branch outcome.
130
+ Porting an inert param for signature-parity's sake would replicate a no-op rather than fix a gap —
131
+ **not ported**, documented as an intentional skip.
132
+ - Verified via `MoMoUIKits` scheme build and full `iosApp` scheme build (exercises real Demo call sites) —
133
+ both `** BUILD SUCCEEDED **`.
134
+
135
+ ### Batch 2 — Typographic/status atoms (Text, Title, Tooltip, Information, ProgressInfo, Loader, Skeleton, BaselineView)
136
+
137
+ - **Title, Loader, BaselineView**: full parity, no findings.
138
+ - **Text/MomoText**: Kotlin's `Text` has a missing-ellipsis mode (`overflow = Clip` default, no truncation
139
+ indicator) that `MomoText`/SwiftUI's `truncationMode` can't express (always shows "…" when clipped) —
140
+ **flagged only**, would need a new API shape. Also missing `onTextLayout`, `inlineContent` (rich inline
141
+ spans), an `AnnotatedString` overload, and a `fontFamily` override — all real gaps but substantial new
142
+ capability (rich-text spans, layout measurement callbacks), not simple parity bugs. **Deferred**,
143
+ documented rather than partially implemented, consistent with the Image CDN/GIF deferral in Batch 1.
144
+ - **Tooltip**: two findings.
145
+ 1. Positioning architecture differs fundamentally (Compose: OS-level `Popup` with outside-tap dismiss +
146
+ no clipping; Swift: in-hierarchy overlay, no outside-tap dismiss, can be clipped by ancestors).
147
+ **Flagged only** — an architecture change, not a small fix.
148
+ 2. Missing width-yielding constraint on the 2-button secondary action (Compose:
149
+ `Modifier.weight(1f, fill=false)`). **Fixed** — added `.layoutPriority(-1)` to
150
+ `TooltipSecondaryButton` so it shrinks before the primary button under width pressure. Low practical
151
+ impact since both buttons already pre-truncate their title to 16 chars, but cheap and correct to add.
152
+ - **Information**: close-icon color was hardcoded to `Colors.black17` instead of following the theme
153
+ (Kotlin's icon has no explicit color, defaulting to `theme.colors.text.default`). Values coincide under
154
+ the default theme so no visible change today, but breaks for non-default themes. **Fixed** — added
155
+ `@Environment(\.appTheme)` and switched to `theme.colors.text.default`.
156
+ - **ProgressInfo**: two findings, both **fixed**.
157
+ 1. Vertical connector line color was hardcoded to the asset-catalog `Colors.primary` instead of
158
+ `theme.colors.primary.opacity(0.2)` — added `@Environment(\.appTheme)` to `ProgressInfoVertical` and
159
+ switched. Changes rendered output under non-default themes only (matches source-of-truth behavior).
160
+ 2. `progressInfoAccentColor` duplicated the `pinkMoMoBranding` hex as a raw literal (with a comment
161
+ already noting what it was meant to be) instead of referencing the token added in Tier 0 — swapped to
162
+ `Colors.pinkMoMoBranding`.
163
+ - **Skeleton**: two findings.
164
+ 1. Extra public `durationMillis`/`angleOfAxisY` params not in Kotlin's zero-arg `Skeleton()`. **Flagged
165
+ only** — existing Swift callers may depend on these, can't remove.
166
+ 2. Shimmer travel-distance formula differs from Kotlin's literal `durationMillis + widthOfShadowBrush`
167
+ (which mixes milliseconds with pixels). **Investigated, not changed**: the Swift file already carries
168
+ an explanatory comment showing this was a deliberate, reasoned substitution — Kotlin's formula is
169
+ dimensionally inconsistent and produces an arguably-unintended sweep distance/speed depending on
170
+ container width; Swift's `size.width + widthOfShadowBrush` is dimensionally consistent and always
171
+ produces a full-width sweep regardless of container size. Re-matching Kotlin's literal formula here
172
+ would reintroduce a questionable behavior rather than fix a real bug — left as is, documented rather
173
+ than silently reverted.
174
+ - Verified via `MoMoUIKits` scheme build — `** BUILD SUCCEEDED **`.
175
+
176
+ ### Button / IconButton / Input — fixes from the initial (pre-plan) exploration round
177
+
178
+ These three were audited before the formal plan existed; applying their fixes was deferred until now.
179
+
180
+ - **Button.swift**:
181
+ 1. Removed the dead unreachable `default:` case in `typographyStyle` (the switch over `.large/.medium/.small` was already exhaustive).
182
+ 2. **Fixed the press-squeeze animation**: previously alpha-only; restructured so `ButtonPressFeedbackStyle`
183
+ builds the full background+content layering itself (background as a `ZStack` layer with an animated
184
+ 0→2pt horizontal inset, content as `configuration.label` with the alpha fade) — mirrors Kotlin's
185
+ `animatedPadding`-on-background + separate content alpha exactly, rather than dimming the whole button
186
+ uniformly.
187
+ 3. **Added a `String`-based icon initializer** alongside the existing `AnyView`-based one, resolving
188
+ icons through the shared `Icon` component like Kotlin's `iconLeft`/`iconRight: String` params. Made
189
+ `iconLeft` required (no default) on this new overload specifically to avoid an `init` ambiguity — a
190
+ call like `Button(action: {})` would otherwise match both overloads since both have all-defaulted
191
+ params past `action`. Verified via a full `iosApp` build that no existing call site broke.
192
+ 4. Default title on the **new** String-icon overload matches Kotlin's `"Button"`; the original
193
+ `AnyView`-based init's default (`""`) was left untouched (Flag) — changing it would alter output for
194
+ any existing Swift caller omitting `title`.
195
+ - **IconButton.swift**: added the missing `IconSize.medium` (36pt box / 16pt icon, matching Kotlin exactly)
196
+ and the missing `IconShape` (`.circle`/`.square`) enum + param (default `.circle`, matching Kotlin).
197
+ Fixed `.small`'s box size from a fabricated `40` to the correct `28`. The previous `Radius.XL`
198
+ rounded-rect shape (which matched neither Kotlin variant) is replaced by a real `Circle()`/`Radius.S`
199
+ rounded-rect switch via a small hand-rolled `Shape` type eraser (`AnyShape` needs iOS 17+, deployment
200
+ target is 15). This does change default rendering for existing callers (true circle instead of a
201
+ large-radius rounded rect) — treated as a Fix rather than a Flag since the old shape was never a
202
+ deliberate design choice, just an unlabeled placeholder value matching neither source-of-truth variant.
203
+ Left the extra public `color` override alone (Flag, unchanged from earlier audit).
204
+ - **Input.swift** / **ErrorView.swift**:
205
+ 1. Wired the previously-dead `autofocus` prop via `@FocusState` on the plain `TextField` path and
206
+ `textField.becomeFirstResponder()` (dispatched to next run loop) on the `SecureInputField` path.
207
+ 2. Fixed the floating-label vertical offset from a hardcoded `-8` to `-(size == .small ? 48 : 56) / 2`,
208
+ matching Kotlin's `-size.values.height / 2` (24pt small / 28pt large instead of always 8pt).
209
+ 3. Fixed the `maxLength` trim race: the text binding's setter now trims atomically before publishing
210
+ (`let limited = limitText(newValue); self.text = limited`) instead of setting the raw value and
211
+ correcting it a render later via a separate `.onChange` — removed that now-redundant correction block.
212
+ 4. Fixed `ErrorView`'s hardcoded `Colors.red03`/`Colors.black12` to use `theme.colors.error.primary`/
213
+ `theme.colors.text.hint` (added `@Environment(\.appTheme)`).
214
+ 5. **Investigated, not changed**: the "bold styling swaps the whole typography token" finding turned out
215
+ to be a non-issue in practice — `TypographyStyle.bodyDefaultRegular` and `.actionSBold` share the same
216
+ 14pt font size and 20pt line height (differing only in weight), and `Input`'s `TextField`/`SecureField`
217
+ never applies `.lineSpacing()` at all, so the "different token" is functionally just a weight toggle
218
+ either way. Kotlin's own local text style (14sp/24sp, independent of the shared `Typography` object)
219
+ does differ in line-height from Swift's shared tokens (20pt), but that value has no visible effect on
220
+ a single-line field without explicit line-spacing — not touched.
221
+ 6. **Flagged, not changed**: the dual-control-type architecture for password visibility toggling
222
+ (`SecureInputField` UIViewRepresentable ↔ SwiftUI `TextField`, swapped based on `isPasswordHidden`)
223
+ carries a real risk of focus/cursor loss on toggle, vs. Kotlin's single `BasicTextField` with a
224
+ `VisualTransformation` swap. Fixing this properly means unifying both modes behind one control
225
+ (e.g. a single `UIViewRepresentable` toggling `isSecureTextEntry` in place) — a larger, riskier
226
+ rewrite than this pass's other fixes; documented rather than attempted.
227
+ 7. **Flagged, not changed**: `icon`/`iconColor` (Kotlin) vs `rightIcon`/`rightIconColor` (Swift) naming
228
+ — renaming the existing Swift params would break current callers for a cosmetic parity gain only.
229
+ - Verified via `MoMoUIKits` scheme build and full `iosApp` scheme build — both `** BUILD SUCCEEDED **`.
230
+
231
+ ### Batch 3 — Selection controls (CheckBox, Radio, Switch, Slider, Rating, Stepper, Steps, Chip)
232
+
233
+ - **Cross-cutting press-feedback gap — fixed.** Kotlin's `activeOpacityClickable` (fades the tappable
234
+ subtree to a target opacity while pressed) had no shared Swift equivalent; a private
235
+ `ActiveOpacityButtonStyle` existed only inside `Uploader.swift` (unshared). Extracted it to
236
+ `ios/Extensions/ActiveOpacityButtonStyle.swift` as a parameterized `ButtonStyle(activeOpacity:)`
237
+ (default 0.2, matching Kotlin's default), updated `Uploader.swift`'s 3 call sites to use the shared
238
+ version with its existing `0.5` value preserved (not a Batch 3 component; left its opacity value
239
+ untouched — that's Batch 6's scope). Wired the shared style into: `Checkbox` (0.8, matching Kotlin's
240
+ explicit override), `Radio`, `Chip`, `Stepper`'s +/- buttons, `Rating`'s star buttons (replacing
241
+ `.buttonStyle(.plain)`), and `Steps`'s two clickable-column variants (`column`/`row`, replacing
242
+ `.onTapGesture` with a conditional `Button` wrap so non-clickable steps still render with zero press
243
+ interaction, matching Kotlin's `if (clickable) activeOpacityClickable(...)`).
244
+ - **Checkbox**: was using SF Symbols (`"checkmark"`/`"minus"`, 12×12) instead of the shared `Icon`
245
+ component with the design-system icon set (`ic_checked`/`navigation_minus`, 20×20, matching Kotlin
246
+ exactly). **Fixed** — swapped to `Icon(source:size:color:)`, and converted the tap target from
247
+ `.onTapGesture` to a `Button` + the shared `ActiveOpacityButtonStyle`.
248
+ - **Switch — most significant fix in this batch.** Four real bugs, all internal/no signature change:
249
+ 1. **On-state color was pink instead of green** (`Colors.pink03` used for both track and inner dot when
250
+ `value == true`; Kotlin uses `green_03`/`green_09`). **Fixed.**
251
+ 2. **No disabled-specific palette** — Swift blanket-dimmed the whole control via `.opacity(0.4)`; Kotlin
252
+ uses distinct muted tokens (`green_09`/`black_05`) with no opacity overlay at all. **Fixed** — removed
253
+ the opacity dim, added a `(disabled, value)`-keyed color switch matching Kotlin's 4-way logic exactly.
254
+ 3. **Track size was 40×24 instead of Kotlin's 38×20**, and the thumb inset was a hardcoded `5` instead of
255
+ `Spacing.XS` (4). **Fixed** — both corrected to match.
256
+ 4. **`title`/`accessibilityId` params were entirely missing.** Re-reading Kotlin's `Switch.kt` closely:
257
+ these are **not** rendered as a visible label at all — they only feed an automation/test identifier
258
+ string (`"toggle_${title}_$value"`). Ported faithfully as two new optional params (default `""`,
259
+ non-breaking) building the same identifier string via `.accessibilityIdentifier(...)`, without adding
260
+ any visual label (there isn't one in the source of truth either).
261
+ Also fixed: the outer 14pt thumb-circle color didn't toggle with `value` at all (hardcoded `Colors.black01`
262
+ always) — now correctly switches `black01`/`black03` per Kotlin's `circleBackgroundColor`.
263
+ - **Radio, Slider, Rating, Steps**: faithful ports beyond the shared press-feedback gap above — no other
264
+ findings.
265
+ - **Stepper**: Kotlin's `Stepper` is uncontrolled (seeded once from `defaultValue`); Swift's `MomoStepper`
266
+ is a controlled `Binding<Int>` component. **Flagged only** — a deliberate-looking SwiftUI idiom, but a
267
+ genuine contract difference for anyone porting usage code 1:1; not changed since it'd be a breaking
268
+ redesign of the current, presumably-intentional Swift API.
269
+ - **Chip**: no `scaleSize(...)` applied to any dimension (height/horizontal padding/icon size/icon
270
+ spacing/corner radius), unlike every other component in this pass — Dynamic Type scaling silently didn't
271
+ apply to Chips. **Fixed** — wrapped all five dimensions in `scaleSize(...)` at the point of use (the
272
+ static `Dimensions` structs themselves stay unscaled, matching how Kotlin's raw dp constants are only
273
+ scaled at composition time). Converted `.onTapGesture` to `Button` + `ActiveOpacityButtonStyle` in the
274
+ same pass since Chip was also missing the press-feedback fix.
275
+ - Verified via `MoMoUIKits` scheme build and full `iosApp` scheme build — both `** BUILD SUCCEEDED **`.
276
+
277
+ ### Batch 4 — Text-entry family (InputDropDown, InputMoney, InputOTP, InputPhoneNumber, InputSearch, InputTextArea)
278
+
279
+ - **InputDropDown, InputMoney**: both had the same bug — `RenderRightIcon` used `if loading { ... } else if
280
+ !icon.isEmpty { ... }`, suppressing the icon whenever `loading == true`; Kotlin's `RenderRightIcon` is two
281
+ independent `if` blocks (both can render at once). **Fixed** in both files.
282
+ - **InputMoney**: also had the exact "floating-label offset doesn't scale with size" bug just fixed in base
283
+ `Input.swift` — hardcoded `-8` instead of `-(size == .small ? 48 : 56) / 2`. **Fixed.** (Its extra
284
+ `.fill(theme.colors.background.surface)` on the input box, absent in Kotlin, was left alone — it's a
285
+ proper theme-token addition consistent with how base `Input.swift`'s own input box is styled, not a bug.)
286
+ - **InputOTP**: Kotlin's free-form (`length == null`) branch references a stray top-level constant
287
+ (`MAX_LENGTH = 300` from `InputTextArea.kt`) instead of its own computed `maxLength` (`length ?: 10`) —
288
+ a genuine bug in the Kotlin source itself, meaning caret/spacer logic there never triggers in practice.
289
+ Swift's `InputOTP.swift` correctly uses its own 10-char `maxLength` and does **not** reproduce this bug.
290
+ **Flagged, not changed** — "fixing" Swift to match would mean deliberately re-introducing broken
291
+ behavior; this needs a Kotlin-side/product decision, not a mechanical Swift edit.
292
+ - **InputPhoneNumber**: two dead public params, both **fixed**.
293
+ 1. `required` was accepted by the initializer but never even stored as a property — completely discarded.
294
+ Added the missing `public var required: Bool` and wired it to append `" *"` to the placeholder when
295
+ set (mirroring how base `Input.swift` renders its own `required` marker; Kotlin's
296
+ `InputPhoneNumber.kt` has no `required` param at all, so there's no source-of-truth rendering to
297
+ match exactly — this follows the sibling component's convention instead).
298
+ 2. `autofocus` was stored but never wired to anything. Fixed via `@FocusState`, same pattern as base
299
+ `Input.swift`.
300
+ Also investigated the flagged divider-color "mismatch" (Kotlin hardcodes `Colors.black_04`; Swift uses
301
+ `theme.colors.border.default`): confirmed `defaultTheme.border.default = Colors.black_04` in Kotlin's own
302
+ `Theme.kt`, so under the default theme these are pixel-identical — Swift's theme-token approach is
303
+ actually more correct (Kotlin's hardcoded literal is the one that silently breaks under a non-default
304
+ theme). No change needed; not a real divergence today.
305
+ - **InputSearch**: Kotlin's `InputSearchProps` exposes `backgroundColor`, `clearCondition`, `onClearPress`,
306
+ and `leftPosition` with no Swift equivalents — a real but substantial API-surface gap. **Flagged, not
307
+ added** (larger scope than a mechanical fix). Did fix the narrower, clearly-broken piece: the clear
308
+ button previously fired **no callback at all** on tap (just `text = ""`); now also calls `onChangeText("")`
309
+ so clearing isn't a silent no-op, even without porting the dedicated `onClearPress`/`clearCondition` API.
310
+ - **InputTextArea**: three findings, all **fixed**.
311
+ 1. Missing clear ("x") button entirely (Kotlin shows one when focused + non-empty).
312
+ 2. `icon`/`iconColor`/`loading`/`onRightIconPressed` were declared, stored, and never referenced anywhere
313
+ in `body` — completely dead public props.
314
+ 3. Floating-label offset used `-height / 10` instead of `-height / 2` — the same offset-scaling bug
315
+ class as base `Input.swift`, but with an outright wrong divisor rather than a fixed constant.
316
+ Restructured the text-field row into an `HStack` (text field + clear button + loading/right-icon, mirroring
317
+ Kotlin's `Row` layout) to fit the new controls in.
318
+ - Verified via `MoMoUIKits` scheme build and full `iosApp` scheme build — both `** BUILD SUCCEEDED **`.
319
+
320
+ ### Batch 5 — Scroll/list/pagination (Carousel, Pagination family, LazyColumnWithBouncing, CupertinoOverscroll, ScaleSizeScope, TabView)
321
+
322
+ - **PaginationDot, PaginationNumber, PaginationWhiteDot, PaginationScroll**: faithful parity, no
323
+ significant findings (PaginationScroll's missing outer-layout-customization param was flagged but is
324
+ low value with no direct SwiftUI equivalent to Compose's `Modifier` — not added).
325
+ - **ScaleSizeScope**: confirmed no port needed — the Kotlin source itself is `@Deprecated`, a no-op
326
+ passthrough; `FontScaleStore`/`scaleSize` in `ios/Typography/` already provide the real functionality.
327
+ - **Carousel**: real perf bug, **fixed**. `HStack(ForEach(0..<virtualPageCount))` instantiated every
328
+ virtual page eagerly — with `loop=true`, `virtualPageCount = itemCount * 1000`, so thousands of slide
329
+ views were built up front (Compose's `HorizontalPager` is lazy). Fixed by windowing the rendered range
330
+ to `currentPage ± 3` and padding the HStack with `Color.clear` spacers sized to the skipped width, so
331
+ the existing offset math (`baseOffset = -currentPage * step + ...`) is completely unchanged — only the
332
+ render cost drops. Deferred, documented (not fixed): autoplay doesn't restart its interval when the user
333
+ stops scrolling (Kotlin's `LaunchedEffect(autoplayInterval, isUserScrolling)` does); Swift's extra
334
+ `enableSnap` param and symmetric-only `contentPadding` (vs Kotlin's asymmetric `PaddingValues`) are
335
+ flagged API differences, not touched.
336
+ - **TabView**: press feedback was plain `.onTapGesture` in both `CardTabBar` and `TabItemView` — Kotlin
337
+ uses `activeOpacityClickable` on both. **Fixed** — wired the shared `ActiveOpacityButtonStyle` (same
338
+ fix applied to CheckBox/Radio/Chip/Stepper/Rating/Steps in Batch 3) into both tap targets. Deferred,
339
+ documented: `userScrollEnabled: false` still animates a real page transition in Kotlin (only the swipe
340
+ gesture is disabled) but instant-swaps content with no transition in Swift — flagged, not changed since
341
+ it'd alter current output for existing callers of that flag. Also deferred: Kotlin lazily composes only
342
+ visited tab pages; Swift's `ForEach` builds `AnyView` content for every tab up front — a real perf gap,
343
+ same class as Carousel's, but not fixed this pass given the remaining batch scope.
344
+ - **LazyColumnWithBouncing, CupertinoOverscroll**: confirmed **zero Swift port exists** for either — the
345
+ custom Cupertino rubber-band overscroll container and the bespoke segmented-ring pull-to-refresh spinner
346
+ (with haptic-at-threshold behavior) have no iOS equivalent anywhere in `ios/`. Native `UIScrollView`/
347
+ `.refreshable` covers baseline bounce/refresh but not the distinctive spinner visual or haptics. This is
348
+ a real, substantial feature gap — flagged for a dedicated follow-up rather than attempted as part of this
349
+ batch (building a custom pull-to-refresh spinner + haptic sequencing is new-component-scale work, not a
350
+ parity fix).
351
+ - Verified via `MoMoUIKits` scheme build and full `iosApp` scheme build — both `** BUILD SUCCEEDED **`.
352
+
353
+ ### Batch 6 — Overlay & misc interaction (PopupPromotion, Collapse, Uploader, SuggestAction, Swipe)
354
+
355
+ - **Uploader**: its 3 `ActiveOpacityButtonStyle` call sites (deduplicated to the shared style in Batch 3)
356
+ hardcoded `activeOpacity: 0.5`, a value that was carried over from the old private duplicate without
357
+ ever being checked against source. Confirmed Kotlin's 3 call sites all omit `activeOpacity`, using the
358
+ modifier's own default (0.2). **Fixed** — dropped the explicit `0.5` at all 3 sites to use the shared
359
+ style's default.
360
+ - **Collapse**: three findings, all **fixed**.
361
+ 1. Header row used `.buttonStyle(.plain)` instead of the shared `ActiveOpacityButtonStyle` (Kotlin uses
362
+ `activeOpacityClickable`) — swapped.
363
+ 2. Missing `accessibilityId` entirely (Kotlin exposes it plus a 6-identifier hierarchy: `|touch`, `|img`,
364
+ `|text`, `|description-text`, `|sub-title-text`, `|icon`). Added the param (additive) and wired the
365
+ two most functionally significant identifiers — the outer container (`automationId`) and the header
366
+ touch target (`|touch`). **Deferred, documented**: the remaining 4 sub-element identifiers
367
+ (`|img`/`|text`/`|description-text`/`|sub-title-text`/`|icon`) were not wired individually — lower
368
+ marginal value for meaningfully more call-site plumbing across the header's sub-views.
369
+ 3. **Deferred, documented** (not fixed): expand/collapse uses `.transition(.move + .opacity)` (slide+fade)
370
+ instead of Kotlin's true `expandVertically`/`shrinkVertically` height animation — same 0.3s duration,
371
+ different visual feel. Fixing this properly needs `GeometryReader`-based height measurement; left as
372
+ a documented gap rather than attempted this pass.
373
+ - **PopupPromotion**: three findings, all **fixed**.
374
+ 1. Image content scale used `.aspectRatio(0.72, contentMode: .fit)` (letterboxes) instead of matching
375
+ Kotlin's `Modifier.fillMaxWidth().aspectRatio(0.72f)` + `ContentScale.FillBounds` (a fixed-ratio box
376
+ the image stretches to exactly fill, ignoring its own aspect ratio). Fixed by giving the image an
377
+ explicit `.frame(width:height:)` with no `.aspectRatio` modifier, which is SwiftUI's equivalent of
378
+ "stretch to fill" for a `.resizable()` image.
379
+ 2. Close-button circle was hardcoded `Colors.black01`/`Colors.black17`; Kotlin uses
380
+ `theme.colors.background.surface` (border + icon) / `theme.colors.text.default` (fill). Fixed —
381
+ added `@Environment(\.appTheme)` and swapped all three color usages.
382
+ 3. Kotlin explicitly disables system press-highlight (`indication = null`) on the close button; Swift had
383
+ no `.buttonStyle`, showing the default system highlight. Fixed with `.buttonStyle(.plain)`.
384
+ Deferred, documented: Kotlin renders nothing for an empty `source` string; Swift shows a full-width
385
+ `media_fail` icon — a real behavior difference, flagged rather than changed since it'd alter output for
386
+ existing empty-string callers.
387
+ - **SuggestAction**: close icon used plain `.onTapGesture` (no press feedback at all); Kotlin uses
388
+ `activeOpacityClickable`. **Fixed** — wrapped in a `Button` + the shared `ActiveOpacityButtonStyle`.
389
+ - **Swipe**: `SwipeCell.swift`'s `Swipe` view never read `@Environment(\.appTheme)` at all, hardcoding
390
+ `Colors.primary`/`Colors.error`/`Colors.card` for the left/right action defaults and content background;
391
+ Kotlin uses `theme.colors.primary`/`theme.colors.error.primary`/`theme.colors.background.surface`.
392
+ **Fixed** — added theme access and swapped all three. Deferred, documented (not changed): a
393
+ `minimumDistance: 8` drag-gesture threshold that Kotlin doesn't have (changes real-time drag feel for
394
+ existing callers — Flag); and a large chunk of unused public API (`SwipeCellModel.swift` entirely, plus
395
+ `SwipeCellModifier`/`.swipeCell(...)` in `SwipeCell.swift`) with zero callers anywhere in the repo and no
396
+ Kotlin counterpart — vestigial, unrelated to `Swipe.kt` parity. Left in place since removing public API
397
+ during a parity pass risks breaking unknown external consumers; flagged for a separate removal decision.
398
+ - Verified via `MoMoUIKits` scheme build and full `iosApp` scheme build — both `** BUILD SUCCEEDED **`.
399
+
400
+ ### Batch 7 — Localize-dependent (PopupDisplay, PopupNotify, TrustBanner)
401
+
402
+ Deferred from Tier 2 until the `Localize` subsystem landed (it now has, see Tier 2 above). All three
403
+ previously read language via an ad-hoc mechanism instead of any shared abstraction.
404
+
405
+ - **PopupDisplay.swift**: read `UserDefaults.standard.string(forKey: "language")` directly and looked up
406
+ an inline `["vi": "Mã lỗi: ", "en": "Error code: "]` dict — values that happen to be character-for-character
407
+ identical to `Localize`'s own `"errorCode"` dictionary entry. **Fixed** — added
408
+ `@EnvironmentObject private var localize: Localize` and replaced both with `localize.translate("errorCode")`.
409
+ - **PopupNotify.swift**: same pattern (`UserDefaults` read + inline `en`/`vi` ternary for the same error-code
410
+ prefix). **Fixed** — identical replacement with `localize.translate("errorCode")`.
411
+ - **TrustBanner.swift**: **not actually reading `UserDefaults` at all** — closer inspection found
412
+ `var language: String? = "vi"` hardcoded and never reassigned anywhere, meaning the banner's content/
413
+ learn-more text was **permanently stuck in Vietnamese regardless of device or app language** — a real,
414
+ live bug, not just an architecture smell. **Fixed** — removed the dead hardcoded property, added
415
+ `@EnvironmentObject private var localize: Localize`, and switched both dictionary lookups
416
+ (`content`/`subContent`, which are already `[String: String]` keyed by language code, matching
417
+ `Localize`'s own dictionary shape) to key off `localize.currentLanguage`.
418
+ - Verified via `MoMoUIKits` scheme build and full `iosApp` scheme build — both `** BUILD SUCCEEDED **`.
419
+
420
+ ### Batch 8 — DateTimePicker subsystem (DateTimePicker, DateTimePickerTypes, DateTimePickerUtils, WheelPicker) — final batch
421
+
422
+ - **DateTimePicker.swift, WheelPicker.swift**: both bypassed the theme system entirely — neither file read
423
+ `@Environment(\.appTheme)` at all, using static asset constants (`Colors.background`, `Colors.border`,
424
+ `Colors.text`) instead of `theme.colors.background.surface`/`theme.colors.border.default`/
425
+ `theme.colors.text.default`. Every other component in this pass correctly reads the theme; these two
426
+ were outliers. **Fixed** — added `@Environment(\.appTheme)` to `DateTimePicker`, `WheelPicker`, and the
427
+ private `WheelPickerItem`, and swapped all 5 hardcoded color usages across the two files. (Note:
428
+ `WheelPicker.swift`'s `Colors.blue10` background accent was confirmed intentional — matches Kotlin's own
429
+ hardcoded `Colors.blue_10`, not a theme bypass.)
430
+ - **DateTimePickerUtils.swift**: `getMinutes`'s interval guard (`max(1, interval)`) already correctly
431
+ avoids a Kotlin-source infinite-loop bug (Kotlin has no such guard) — confirmed as a case where Swift is
432
+ already better, not a gap. **Investigated, not changed**: `getDaysInMonth`/`getMonths`/`getYears` return
433
+ a single-element fallback array when given an inverted min/max range, where Kotlin's `for` loop would
434
+ produce an empty list. Judged this an intentional, reasonable defensive choice rather than a bug to
435
+ "fix" — an empty wheel-picker column (no scrollable values at all) is arguably a worse failure mode than
436
+ a one-item fallback, so matching Kotlin's literal empty-list behavior here would make the edge case
437
+ worse, not better. Left as is (same reasoning pattern as the Skeleton shimmer-formula judgment call in
438
+ Batch 2).
439
+ - **WheelPicker.swift**: **deferred, documented** (not fixed) — the wheel currently reports
440
+ `onChange(name, value)` continuously on every scroll-position update during an active drag, and never
441
+ animates a snap-to-center after release; Kotlin only fires `onChange` once scrolling settles
442
+ (`!isScrollInProgress`) and then animates the list to precisely center the selected row. Confirmed
443
+ real and would need restructuring the scroll-tracking to distinguish "dragging" from "settled" plus a
444
+ genuine snap-to-center animation — a materially bigger rewrite than this pass's other fixes, flagged for
445
+ a dedicated follow-up rather than attempted here.
446
+ - **DateTimePickerTypes.swift**: clean 1:1 port, no findings. No hardcoded locale strings found in this
447
+ subsystem needing a `Localize` hookup — the wheel columns render zero-padded numbers only, no month/
448
+ day names.
449
+ - Verified via `MoMoUIKits` scheme build and full `iosApp` scheme build — both `** BUILD SUCCEEDED **`.
450
+
451
+ **Post-pass fix — BottomSheet: keyboard avoidance, scrim alpha correction, drag-performance refactor**:
452
+ three issues reported by the user, all in `ios/Application/Navigation/Overplay/BottomSheet.swift`.
453
+ 1. **Keyboard avoidance (real gap)**: `BottomSheetView` is rendered via `NavigationContainer`'s own
454
+ `.overlay { OverplayHost(...) }`, structurally outside any pushed `StackScreen`, so it never inherited
455
+ `StackScreen`'s existing keyboard-avoidance (`ios/Application/StackScreen.swift:102,195-201`). Fixed by
456
+ adding the same `keyboardWillChangeFrameNotification`-based tracking directly to `BottomSheetView`, and
457
+ replacing (not adding to) the bottom-inset filler with the keyboard height while it's up.
458
+ 2. **Scrim alpha — my own research was wrong at first, caught by the user.** Initially reported "BottomSheet
459
+ (peak 0.3) intentionally differs from Modal (peak 0.6), not a bug" — but Kotlin's `BottomSheet.kt:67-69`
460
+ is **additive** (`backgroundAlpha + dynamicAlpha`, each terming out at 0.3), so its true steady-state peak
461
+ is 0.3+0.3=**0.6**, exactly matching `ModalScreen.kt`. The user directly re-checked the Kotlin source and
462
+ caught this before I did. Swift's `BottomSheet.swift` only implemented the single `dynamicAlpha` term
463
+ (peaking at 0.3) — genuinely under-dimmed by half. **Fixed**: split into two terms
464
+ (`backgroundAlpha` state, only touched by open()/close(); `dynamicAlpha` computed property tracking
465
+ drag position) matching Kotlin's model exactly — scrim now peaks at 0.6 at rest and only fades to a 0.3
466
+ floor while dragging (never fully transparent until the sheet has completely left the screen). Verified
467
+ by pixel-sampling a screenshot: `(102,102,102)` = black-at-60%-opacity over white, exact match.
468
+ 3. **Drag jank**: the raw per-touch update mechanism already matched Kotlin's unanimated `Animatable.snapTo`
469
+ 1:1 — not an animation-vs-no-animation gap. Root cause: two independent `@State` writes per touch-move
470
+ (`sheetOffset`, `scrimAlpha`) each forced the whole `body` (including `content()` and a `GeometryReader`)
471
+ to reconstruct at touch-move frequency. **Fixed** by refactoring to `@GestureState` for the live drag
472
+ value (isolated from `content()`'s reconstruction), combined with `sheetOffset` only for the settled
473
+ position — matching an idiom already established elsewhere in this codebase
474
+ (`ios/PopupView/PopupView.swift`'s `@GestureState dragState` + `lastDragPosition` capture-before-reset
475
+ pattern). **Honest limitation**: smoothness itself isn't screenshot-verifiable — confirmed clean compile
476
+ and correct rest/open/closed positions, but true "no more jank" confirmation needs the user's own
477
+ hands-on feel on a simulator/device, which I said explicitly rather than overclaim.
478
+ Verified via `MoMoUIKits` + full `iosApp` scheme builds, plus a temporary root-view-swap reproduction
479
+ (`BottomSheetView` wrapping an autofocused `Input`) — screenshotted keyboard-up state showing the sheet
480
+ correctly staying above the keyboard. Reverted the temporary harness cleanly (confirmed via `git diff`,
481
+ including restoring an accidental trailing-whitespace diff introduced by the revert itself).
482
+
483
+ **Follow-up — drag still jerked twice before settling.** The `@GestureState` refactor above didn't fully
484
+ fix the jank: the user reported a specific pattern (touch, drag, hold → 2 visible jerks → settles at the
485
+ correct finger position). No touch-injection tooling is available in this environment (no `idb`, no
486
+ XCUITest harness), so this couldn't be reproduced/observed directly — diagnosed by code inspection only.
487
+ Found two plausible races that both fit the exact symptom shape:
488
+ 1. `open()` animates `sheetOffset` 0-target over 350ms; if a drag starts before that settles, the live
489
+ `dragTranslation` value gets added on top of a `sheetOffset` that's *also* mid-animation — two
490
+ competing sources of motion for the same frame.
491
+ 2. The keyboard-driven `effectiveBottomInset` swap (issue 1 fix) animates the bottom filler's height,
492
+ which changes the sheet's total height; since the sheet is bottom-anchored, that shifts its *top* edge
493
+ — exactly where the drag handle sits — via layout, independent of the offset-driven motion from the
494
+ drag itself.
495
+ **Fixed** both: added `.onChanged` to the drag gesture that snaps `sheetOffset` to `0` (no animation) the
496
+ instant a drag begins, settling any in-flight open/keyboard-resize animation immediately rather than
497
+ having the live drag fight a moving target. Also (at the time) added `.onChange(of: geo.size.height)` to
498
+ the `sheetHeight`-measuring `GeometryReader`, reasoning that its `.onAppear`-only measurement would go
499
+ stale after the keyboard-driven resize changed the sheet's actual height.
500
+
501
+ **Follow-up #2 — that `.onChange` addition was itself a regression.** User reported a *continuous* up/down
502
+ jitter for the whole duration of a drag (not just a settle-in jerk at the start) — a materially different,
503
+ more severe symptom. Prime suspect: `sheet` is a computed property re-evaluated on every touch-move (since
504
+ `dragTranslation` changing forces `body` to re-render), which reconstructs the `GeometryReader` each time;
505
+ the newly-added `.onChange(of: geo.size.height)` closure writing to `sheetHeight` on every such
506
+ reconstruction is a plausible feedback-loop shape (state write → re-render → reconstruct → possible
507
+ re-fire → state write again) that fits a *continuous* oscillation far better than the one-time settle-in
508
+ case it was meant to fix. No `.animation()` (scoped or unscoped) exists anywhere in this file or
509
+ `NavigationContainer.swift`, ruling out the other classic cause of "gesture updates chase an animated
510
+ target." **Reverted** the `.onChange(of: geo.size.height)` addition back to `.onAppear`-only (accepting
511
+ the minor, lower-severity `sheetHeight`-staleness-after-keyboard-resize issue rather than risk
512
+ reintroducing a continuous jitter); kept the `.onChanged` drag-start snap fix, which is much lower-risk
513
+ (fires once per drag, a no-op on every subsequent call). Verified clean compile on both schemes only —
514
+ **still could not verify at runtime** whether the continuous jitter is actually resolved, since no
515
+ touch-injection tooling (no `idb`, no XCUITest harness) is available in this environment. Told the user
516
+ plainly rather than claim a fix that can't be felt from here, and asked for confirmation plus, if it
517
+ persists, more precise repro details (does `content()`'s makeup matter — e.g. an `Input` field present or
518
+ not — and does it happen from the very first touch-move or only after some movement).
519
+
520
+ **Follow-up #3 — confirmed still jittery even with plain-text content**, ruling out anything specific to
521
+ `Input`'s own internal measurement. Two more attempts, one of which caused a worse regression:
522
+ - **Tried and reverted**: `.drawingGroup()` on `sheet`, reasoning that `RoundedCorner`'s clip path (built
523
+ via a `UIBezierPath` bridge call) plus the rest of `sheet`'s layout was being rebuilt on every touch-move
524
+ (since `sheet` is a computed property re-evaluated whenever `dragTranslation` changes) regardless of
525
+ `content()`'s complexity — consistent with the plain-text repro. **This introduced a worse, more visible
526
+ regression**: the header's close icon rendered as a blank gray box instead of the "X" glyph, screenshotted
527
+ and confirmed before reverting. Likely cause: the icon is loaded via an async image library
528
+ (SDWebImageSwiftUI-backed `ImageView`) that doesn't composite correctly once flattened into a
529
+ `drawingGroup`'s rasterized Metal-backed layer. Reverted immediately; re-screenshotted to confirm the
530
+ icon renders correctly again post-revert.
531
+ - **Applied, not yet confirmed**: changed `DragGesture()` to `DragGesture(coordinateSpace: .global)`.
532
+ Reasoning: the gesture is attached to the drag handle, which is itself repositioned every frame by this
533
+ same gesture's own output (`sheet.offset(y: effectiveOffset)`, computed from `dragTranslation`) — the
534
+ default `.local` coordinate space ties gesture tracking to that same (continuously moving) view, a
535
+ known category of SwiftUI feedback-jitter source. `.global` anchors tracking to the window instead,
536
+ decoupling it from the view's own motion. Verified clean compile + no visual regression via screenshot
537
+ (icon renders fine, layout unchanged) — **have not been able to verify whether the continuous jitter
538
+ itself is resolved**, same tooling limitation as before (no touch-injection available in this
539
+ environment). Asked the user to retest.
540
+
541
+ **Confirmed fixed** — user retested and reported the jitter is gone. Root cause: `DragGesture()`'s default
542
+ `.local` coordinate space tied gesture tracking to the drag handle's own view, which was itself being
543
+ repositioned every frame by that same gesture's output — a feedback loop between "where the view is now"
544
+ and "where the gesture thinks the touch is relative to that view." Switching to
545
+ `.global` (anchoring tracking to the window instead of the moving view) resolved it. This is the actual
546
+ final state of the drag-jank issue from this whole BottomSheet follow-up thread; the `@GestureState`
547
+ refactor, the drag-start `sheetOffset` snap, and this coordinate-space fix together are what shipped —
548
+ `.onChange(of: geo.size.height)` and `.drawingGroup()` were both tried and reverted along the way (see
549
+ above) as they either regressed or didn't address the root cause.
550
+
551
+ ## Pass summary
552
+
553
+ All 8 Tier-3 batches (Batches 1–8) plus Tiers 0–2 (tokens, navigation core, Localize/ScreenTracker
554
+ subsystems) and the original Button/IconButton/Input fixes are complete. Every fix in this log was
555
+ verified via a real `xcodebuild` compile of both the `MoMoUIKits` library scheme and the full `iosApp`
556
+ demo scheme (catching any public-signature breakage against real `Demo/*.swift` call sites), plus targeted
557
+ value-diffs for the token and Localize dictionary ports. Deferred items (documented above, not silently
558
+ dropped) worth a dedicated follow-up pass: Text's rich-text/inline-content API, Tooltip's popup-based
559
+ positioning architecture, Input's dual-control-type password-toggle, TabView/Carousel's remaining
560
+ lazy-render gaps, LazyColumnWithBouncing/CupertinoOverscroll (zero port), Collapse's true height-animation,
561
+ WheelPicker's settle-only snap behavior, and Navigation.kt's per-screen `setOptions()` runtime API.
562
+
563
+ **Post-pass addendum — Localize demo screen**: added `sample/iosApp/iosApp/Demo/LocalizeDemo.swift`,
564
+ a SwiftUI port of the Compose sample's `sample/shared/.../screens/LocalizeUsage.kt` demo (same section
565
+ structure: language switch, `translate(key)` on kit defaults, missing-key fallback, `{placeholder}`
566
+ substitution, `translateData(vi:en:)`, `translateData(map:)`, `addTranslations` with app-specific keys,
567
+ and a realistic error-code + Cancel/Confirm example). Registered in `Home.swift`'s `ScreenUsages` list.
568
+ Verified at runtime (not just compiled) by temporarily swapping `NativeContentView`'s root to the new
569
+ screen, screenshotting via the simulator, and reverting — confirmed via `git diff` that the revert left
570
+ zero trace. Found and fixed one cosmetic layout bug during that check (a long key name like
571
+ `voucherRemindHour` overlapped its value in a fixed-width `HStack` row; switched `TranslationRow` to a
572
+ `VStack` layout).
573
+
574
+ **Post-pass fix — BottomTab pushed up above the bottom safe-area inset**: reported by the user
575
+ ("bottom tab đang bị đẩy lên cao hơn chiều cao của inset bottom"). Root cause traced to the
576
+ `Navigation.kt` gap already documented in Tier 1: Kotlin's `StackScreen.kt` skips its own bottom
577
+ navigation-bar padding when hosting a `BottomTab` (`MainContent(... isBottomTab = navigation.isBottomTabRoot)`,
578
+ `.padding(bottom = if (options.footerComponent != null || isBottomTab) 0.dp else navigationBar)`),
579
+ where `isBottomTabRoot` is set by `BottomTab.kt` itself calling `navigation.markAsBottomTabRoot()` on
580
+ mount. Swift's `StackScreen.swift` had no equivalent — its `contentBottomInset` only checked
581
+ `footer == nil`, so whenever `BottomTab` was hosted inside a pushed screen (as the "Bottom Tab" demo
582
+ entry is), the outer `StackScreen` added up to 21pt of extra bottom padding on top of `BottomTab`'s own
583
+ internal safe-area handling, squeezing it into less than the full screen height and leaving an
584
+ unreachable gap below the tab bar (`.ignoresSafeArea` can't undo a literal `.padding()` imposed by an
585
+ ancestor). **Fixed** without porting the full `Navigation` class: added a `PreferenceKey`
586
+ (`IsBottomTabRootKey` in `Components.swift`) that `BottomTab` sets via `.preference(...)` on appear and
587
+ `StackScreen` reads via `.onPreferenceChange(...)`, mirroring Kotlin's self-registering
588
+ `markAsBottomTabRoot()` mechanism (the closest existing precedent in this file is the
589
+ `HeaderRightWidthKey` preference already used for header-right measurement) rather than requiring every
590
+ call site to manually flag it. Verified by temporarily reproducing the exact push path from `Home.swift`
591
+ (`navigator.push({ BottomTabDemo() }, options: NavigationOptions(title: "Bottom Tab", headerType: .none,
592
+ scrollable: false))`) and screenshotting — tab bar now sits flush with the true bottom edge. Reverted the
593
+ temporary root-view change cleanly (confirmed via `git diff`).
594
+
595
+ **Post-pass fix — BottomSheet's bottom inset showed the backdrop color instead of the sheet's own
596
+ background**: reported by the user. `BottomSheetView.sheet` already had an explicit
597
+ `backgroundColor.frame(height: bottomInset)` filler row at the bottom of its `VStack` (matching the
598
+ sheet's own `theme.colors.background.surface`/`.default`), but nothing in the view made it reach the true
599
+ screen edge — so SwiftUI auto-reserved the real bottom safe-area padding *on top of* that filler, leaving
600
+ a gap below it where the dimmed scrim (rendered full-screen behind the sheet) showed through instead of
601
+ the sheet's background. Not a Kotlin parity gap — `BottomSheet.kt` has no equivalent inset handling at
602
+ all, confirming this filler was bespoke (and incomplete) iOS-only code.
603
+ **First attempt (did not work, verified by pixel-sampling the screenshot):** added
604
+ `.ignoresSafeArea(edges: .bottom)` to the nested `sheet` view specifically. Compiled fine and looked
605
+ plausible, but sampling actual pixel colors at the bottom of a screenshot showed a ~33pt strip of
606
+ `(178,178,178)` — exactly black-at-30%-opacity over white, i.e. the scrim alpha value — proving the gap
607
+ was untouched. `ignoresSafeArea` on a fixed-intrinsic-height child evidently doesn't reposition/resize it
608
+ the way it does for a flexible one.
609
+ **Actual fix:** moved `.ignoresSafeArea()` to wrap the *entire* outer `ZStack` in `body` (both scrim and
610
+ sheet together) instead of applying it separately per child, and removed the now-redundant modifier on
611
+ `sheet`. Re-verified the same way — pixel-sampled the new screenshot bottom strip: white
612
+ `(255,255,255)` all the way to the true edge, with only the OS-drawn home-indicator pill
613
+ (`(93,93,93)`→`(125,125,125)`, normal system chrome) visible on top, not a uniform scrim-gray band.
614
+ Reverted the temporary root-view-swap reproduction cleanly (confirmed via `git diff`) both times.
615
+
616
+ **Post-pass correction — floating-label offset fix from earlier in this pass was actually wrong**:
617
+ reported by the user ("floatingValue của các Input, bị lệch so với trước khi sửa"). Screenshotting
618
+ `InputDemo` showed the "Momo" floating label floating so high it overlapped completely unrelated content
619
+ (the "Secure Text Entry" section header above the input), confirming a real regression from the
620
+ `-(size == .small ? 48 : 56) / 2` change applied to `Input.swift`/`InputMoney.swift`/`InputTextArea.swift`
621
+ earlier in this pass (Batches 3/4 area).
622
+ **Root cause of the original mistake**: misread which layout anchor Kotlin's `offsetY = -size.values.height / 2`
623
+ is relative to. Kotlin's `BasicTextField` `decorationBox` centers its children (the `FloatingLabel` and the
624
+ input `Box`) vertically by default within the box-height-tall `BasicTextField` — so `-boxHeight/2` there
625
+ nets out to centering the label exactly on the box's top border (spans `-labelHeight/2 … +labelHeight/2`
626
+ around y=0). Swift's `ZStack(alignment: .topLeading)` anchors from the top instead, so applying the same
627
+ `-boxHeight/2` formula there is a completely different, much larger offset — hence the label flying
628
+ several times too far up. The original hardcoded `-8` (which I "fixed" away) was a reasonable
629
+ approximation of `-labelHeight/2`, not related to box height at all — reverting to a hardcoded constant
630
+ would have worked, but instead **measured the label's actual rendered height via a `GeometryReader`
631
+ background** (`floatingLabelHeight` state) and offset by `-floatingLabelHeight / 2` in all three files, so
632
+ it's correct regardless of font size/Dynamic Type scaling rather than relying on a magic constant tuned
633
+ for one specific rendered size. Verified by reproducing `InputDemo` via a temporary root-view swap,
634
+ screenshotting before/after, and confirming the label now sits correctly straddling each box's top border
635
+ with no overlap into unrelated content. Reverted the temporary change cleanly (confirmed via `git diff`).
636
+
637
+ **End-of-pass `pod lib lint` note**: `pod lib lint ios/native-kits.podspec --allow-warnings --quick` fails
638
+ with `ERROR | name: The name of the spec should match the name of the file` (spec name `MoMoUIKits` vs
639
+ filename `native-kits.podspec`). Confirmed pre-existing via `git diff` — neither podspec file was touched
640
+ anywhere in this pass, so this isn't a regression, just a packaging/naming convention issue outside a
641
+ Kotlin-vs-Swift parity pass's scope. Flagged here rather than silently ignored.