@momo-kits/native-kits 0.300.3 → 0.300.4
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 +51 -7
- package/README.md +2 -2
- package/ios/Application/ApplicationEnvironment.swift +24 -6
- package/ios/Application/DesignSystemKits.swift +100 -0
- package/ios/Application/DesignSystemKitsPayload.swift +68 -0
- package/ios/Application/MaxApi.swift +30 -3
- package/ios/Avatar/Avatar.swift +4 -3
- package/ios/Badge/Badge.swift +6 -4
- package/ios/Badge/BadgeDot.swift +1 -1
- package/ios/Badge/BadgeRibbon.swift +8 -2
- package/ios/Button/Button.swift +15 -12
- package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +64 -18
- package/ios/Carousel/Carousel.swift +29 -12
- package/ios/Checkbox/Checkbox.swift +7 -3
- package/ios/Chip/Chip.swift +5 -4
- package/ios/Collapse/Collapse.swift +70 -19
- package/ios/Colors+Radius+Spacing/Colors.swift +10 -1
- package/ios/Colors+Radius+Spacing/Radius.swift +6 -0
- package/ios/DateTimePicker/DateTimePicker.swift +54 -20
- package/ios/DateTimePicker/WheelPicker.swift +103 -105
- package/ios/Divider/Divider.swift +37 -4
- package/ios/Extensions/Color++.swift +38 -9
- package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
- package/ios/Extensions/SkeletonUICompat.swift +11 -0
- package/ios/Icon/Icon.swift +21 -23
- package/ios/Icon/Icons.swift +1329 -0
- package/ios/IconButton/IconButton.swift +15 -7
- package/ios/Image/Image.swift +7 -23
- package/ios/Information/Information.swift +27 -9
- package/ios/Input/ErrorView.swift +2 -2
- package/ios/Input/Input.swift +171 -148
- package/ios/Input/InputPhoneNumber.swift +10 -9
- package/ios/Input/InputSearch.swift +107 -78
- package/ios/Input/InputTextArea.swift +133 -148
- package/ios/InputDropDown/InputDropDown.swift +10 -7
- package/ios/InputMoney/InputMoney.swift +32 -11
- package/ios/InputOTP/InputOTP.swift +22 -12
- package/ios/Layout/Card.swift +50 -0
- package/ios/Layout/GridContext.swift +187 -3
- package/ios/Layout/Item.swift +5 -4
- package/ios/Layout/ScreenLayout.swift +415 -0
- package/ios/Layout/Section.swift +4 -100
- package/ios/Loader/Loader.swift +5 -9
- package/ios/Lottie/LottieView.swift +14 -2
- package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +169 -29
- package/ios/Navigation/BottomTab/BottomTab.swift +332 -0
- package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +18 -7
- package/ios/Navigation/BottomTab/DuoBarCompression.swift +104 -0
- package/ios/Navigation/BottomTab/DuoTabBar.swift +329 -0
- package/ios/{Application → Navigation/Component}/FloatingButton.swift +43 -54
- package/ios/Navigation/Component/Header.swift +545 -0
- package/ios/Navigation/Component/HeaderBackground.swift +154 -0
- package/ios/Navigation/Component/HeaderJourney.swift +73 -0
- package/ios/Navigation/Component/HeaderLocation.swift +48 -0
- package/ios/Navigation/Component/HeaderRight.swift +425 -0
- package/ios/Navigation/Component/HeaderTitle.swift +36 -0
- package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +21 -24
- package/ios/Navigation/Component/SideBar.swift +183 -0
- package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +8 -8
- package/ios/Navigation/HostBackGestureBridge.swift +78 -0
- package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +9 -2
- package/ios/Navigation/Navigation.swift +204 -0
- package/ios/Navigation/NavigationContainer.swift +195 -0
- package/ios/{Application/Navigation → Navigation}/Navigator.swift +207 -107
- package/ios/Navigation/StackScreen.swift +930 -0
- package/ios/Navigation/StackTransition.swift +400 -0
- package/ios/Navigation/SwipeBackHaptic.swift +33 -0
- package/ios/{Application/Navigation → Navigation}/Tracking/ScreenTracker.swift +12 -5
- package/ios/OTPKeyboard/OTPKeyboard.swift +8 -11
- package/ios/Pagination/PaginationScroll.swift +2 -2
- package/ios/Popup/PopupDisplay.swift +72 -135
- package/ios/Popup/PopupInput.swift +10 -13
- package/ios/Popup/PopupLayout.swift +138 -0
- package/ios/Popup/PopupNotify.swift +59 -69
- package/ios/Popup/PopupPromotion.swift +31 -47
- package/ios/PopupView/FullscreenPopup.swift +13 -19
- package/ios/PopupView/PopupView.swift +1 -1
- package/ios/PopupView/Utils++.swift +0 -8
- package/ios/ProgressInfo/ProgressInfo.swift +4 -2
- package/ios/Radio/Radio.swift +2 -1
- package/ios/Rating/Rating.swift +9 -1
- package/ios/Resources/dot_loading.json +558 -0
- package/ios/Resources/lottie_circle_loader.json +1 -0
- package/ios/ScrollIndicator/ScrollIndicator.swift +11 -10
- package/ios/Skeleton/Skeleton.swift +22 -27
- package/ios/Slider/Slider.swift +16 -18
- package/ios/Stepper/Stepper.swift +7 -3
- package/ios/Steps/Steps.swift +8 -6
- package/ios/SuggestAction/SuggestAction.swift +5 -0
- package/ios/Swipeable/SwipeCell.swift +10 -6
- package/ios/Switch/Switch.swift +22 -20
- package/ios/TabView/TabView.swift +102 -21
- package/ios/Tag/Tag.swift +15 -4
- package/ios/Template/Logo/Logo.swift +2 -1
- package/ios/Template/TrustBanner/TrustBanner.swift +109 -55
- package/ios/Title/Title.swift +6 -17
- package/ios/Tooltip/Tooltip.swift +29 -14
- package/ios/Typography/FontScale.swift +17 -14
- package/ios/Typography/Text.swift +156 -20
- package/ios/Typography/Typography.swift +2 -2
- package/ios/Uploader/Uploader.swift +3 -1
- package/ios/native-kits.podspec +2 -3
- package/ios-demo/MoMoUIKitsDemo.podspec +1 -3
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +80 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +2 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupDisplayDemo.swift +1 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupNotifyDemo.swift +2 -2
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupPromotionDemo.swift +2 -0
- package/package.json +1 -1
- package/publish.sh +19 -2
- package/scripts/gen-ios-icons.mjs +36 -0
- package/ios/Application/Components.swift +0 -270
- package/ios/Application/ComposeApi.swift +0 -22
- package/ios/Application/HeaderRight.swift +0 -306
- package/ios/Application/Navigation/BottomTab/BottomTab.swift +0 -104
- package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
- package/ios/Application/Navigation/HeaderTitle.swift +0 -57
- package/ios/Application/Navigation/NavigationContainer.swift +0 -213
- package/ios/Application/Navigation/NavigationOptions.swift +0 -109
- package/ios/Application/Navigation/NavigatorManager.swift +0 -22
- package/ios/Application/Navigation/component/Header.swift +0 -178
- package/ios/Application/Navigation/component/HeaderBackground.swift +0 -83
- package/ios/Application/Navigation/component/HeaderColor.swift +0 -65
- package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +0 -11
- package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
- package/ios/Application/Navigation/component/NavigationHeaderRight.swift +0 -196
- package/ios/Application/Screen.swift +0 -250
- package/ios/Application/StackScreen.swift +0 -492
- package/ios/Typography/FontScaleStore.swift +0 -58
- package/ios/Typography/KitDesignSystem.swift +0 -17
- /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
package/CLAUDE.md
CHANGED
|
@@ -18,7 +18,7 @@ Guidance for working in this repo. Keep it accurate — update it when structure
|
|
|
18
18
|
## Source layout (`compose/src/commonMain/kotlin/vn/momo/kits/`)
|
|
19
19
|
|
|
20
20
|
- `components/` — ~50 UI components, **one file per component** (`Button.kt`, `Input*.kt`, `PopupNotify.kt`, …).
|
|
21
|
-
- `application/` — app-level context & config: `Context.kt` (CompositionLocals, `MiniAppContext`, `DesignSystemConfig`), `Localize.kt` (i18n: `LocalizationObject`, `Localize`, default dictionary), `
|
|
21
|
+
- `application/` — app-level context & config: `Context.kt` (CompositionLocals, `MiniAppContext`, `DesignSystemConfig`), `Localize.kt` (i18n: `LocalizationObject`, `Localize`, default dictionary), `LiteScreen.kt` (standalone lightweight screen with its own header), `WidgetContainer.kt`.
|
|
22
22
|
- `navigation/` — `NavigationContainer.kt` (**canonical entry point**), `Navigator.kt`, `Navigation.kt`, `StackScreen.kt`, `bottomtab/`, `component/`, `tracking/`.
|
|
23
23
|
- `const/` — design tokens: `Theme.kt`, `Colors.kt`, `Typography.kt`, `Spacing.kt`, `Radius.kt`.
|
|
24
24
|
- `layout/`, `modifier/`, `utils/`.
|
|
@@ -35,18 +35,55 @@ Run `./gradlew :compose:testAndroidHostTest` for the request ownership tests in
|
|
|
35
35
|
./gradlew :sample:androidApp:installDebug # run demo on device/emulator
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
The SwiftUI kit (`ios/`, pod `MoMoUIKits`) compiles on its own through the sample's Pods project (`pod install` in `sample/iosApp` first when files are added):
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
xcodebuild -project sample/iosApp/Pods/Pods.xcodeproj -scheme MoMoUIKits -sdk iphonesimulator \
|
|
42
|
+
-destination 'generic/platform=iOS Simulator' build
|
|
43
|
+
```
|
|
44
|
+
|
|
38
45
|
Publishing is automated (`publish.sh` + GitLab CI on `[ci build]` / `main`). Do not publish manually unless asked: `sh publish.sh beta` / `sh publish.sh release` (Maven→GitLab + NPM).
|
|
39
46
|
|
|
40
47
|
## Architecture essentials
|
|
41
48
|
|
|
42
|
-
- **Entry point:** wrap content in `NavigationContainer(...)` from `navigation/`. It builds the `Navigator`, takes a host-supplied `Localize` (falls back to an empty one), and provides the CompositionLocals below.
|
|
49
|
+
- **Entry point:** wrap content in `NavigationContainer(...)` from `navigation/`. It builds the `Navigator`, takes a host-supplied `Localize` (falls back to an empty one), and provides the CompositionLocals below. The legacy `Screen` / `ApplicationContainer` / `ComposeApi` stack has been removed on both sides — don't reintroduce it.
|
|
43
50
|
- **Configuration flows via CompositionLocals**, not params. Public ones: `AppTheme`, `AppStatusBar`, `AppNavigationBar` (`const/Theme.kt`); `ApplicationContext`, `AppConfig`, `AppLanguage`, `LocalLocalize` (`application/`); `LocalNavigator`, `LocalMaxApi` (`navigation/`). Read with `X.current`.
|
|
44
|
-
-
|
|
51
|
+
- **`DesignSystemKits`** (`application/DesignSystemKits.kt`) is the single app-wide holder for the host-owned design-system inputs: font scale (`userScaleRate` / `useOSScaleRate`), whitelist (`isWhiteList`) and the remote `DesignSystemConfig`, bundled as `DesignSystemKitsConfigs`. The host calls `DesignSystemKits.init(source)` once at startup with its `DesignSystemKitsSource` (sync `getConfigs()` seed + `configsFlow`); `init` is idempotent. Without a source, call `setConfigs(...)` directly. Read with `getConfigs()` (snapshot state, recomposes); non-Compose observers use `configsFlow`.
|
|
52
|
+
- **SwiftUI twin:** `ios/Application/DesignSystemKits.swift` mirrors it field for field (`DesignSystemKitsConfigs` with `designSystemConfig: KitConfig?`, `setConfigs` / `getConfigs`), plus the OS Dynamic Type factor (`osFontScale`). There is no `init(source:)` (Swift reserves `init`): the host observes its own bridge and pushes. A host that can't share Swift types (the KMP bridge) calls `DesignSystemKits.shared.setConfigs(json:)` (`ios/Application/DesignSystemKitsPayload.swift`); the JSON field names are a contract with momo-app's serializer (`HostDesignSystemKits.observeConfigsJson`, payload classes in `DesignSystemData.kt`) — change both together. Invalid JSON is ignored. The SwiftUI kit can't link the Kotlin `kits` framework (a second Kotlin/Native runtime would hold a second, never-initialized singleton), so the render-time logic — `FontScale` formula, Dynamic Type observation — is intentionally duplicated in Swift; only values flow across.
|
|
53
|
+
- **Design-system config** (`trustBanner`, `headerBar`, `headerGradient`): `NavigationContainer` provides it as `LocalDesignSystemConfig` (param `designSystemConfig` → parent → `DesignSystemKits`) and applies `headerBar` as the header image when the theme has none.
|
|
45
54
|
- **Platform code** goes through `expect`/`actual` in `platform/` — never reference Android/iOS APIs from `commonMain` directly.
|
|
46
55
|
- **Native bridge:** typed `IMaxApi` from NativeMaxApi `0.3.1` (`vn.momo.maxapi`, exposed as `LocalMaxApi`) is the host-app bridge. Toolkit uses `ToolkitOptions` to preserve custom groups and system-tool policy; pending callbacks are cancelled when their composition is disposed. Containers borrow the host client and must not close it.
|
|
47
56
|
- Navigation uses a runtime `DynamicScreenRegistry`; overlays (modal/bottom-sheet/snackbar) go through `Navigator` + `OverplayComponentRegistry`.
|
|
48
57
|
- **iOS swipe-back:** the host owns the only edge-pan recogniser and replays it through `HostBackGestureBridge` (`navigation/HostBackGestureBridge.kt`; the kit framework is never exported, so the host reaches it through its own Koin-provided facade): `checkCanPop` / `canPopInner` / `pop` route the back into the kit stack, `started` / `progressed` / `completed` / `cancelled` seek the NavHost transition, `shouldUseFloatingArrowBack` is set by bottom tabs. `NavigationContainer` attaches itself automatically (`AttachHostBackGesture`, iOS only, detached by identity on dispose) — hosts need no wrapper and no `setNavigator`. `maxApi` is a required parameter of `NavigationContainer` — every container, nested or root, is handed its host bridge explicitly.
|
|
49
58
|
|
|
59
|
+
## SwiftUI kit (`ios/`)
|
|
60
|
+
|
|
61
|
+
- **Layout mirrors Compose:** `ios/Navigation/` follows `navigation/` file for file (`NavigationContainer`, `Navigator`, `Navigation` (options + per-screen `Navigation`), `StackScreen` (incl. `NavigationFooter`), `StackTransition`, `SwipeBackHaptic`, `BottomSheet`, `ModalScreen`, `HostBackGestureBridge`, `BottomTab/`, `Component/`, `Tracking/`); `ios/Layout/ScreenLayout.swift` mirrors `layout/`. `ios/Application/` keeps only context (`ApplicationEnvironment`), `Localize*` and `MaxApi` (Compose `application/`). Change a navigation piece on both sides together.
|
|
62
|
+
- **File basenames must be unique across the pod** — Swift fails with "filename used twice" even in different folders. New folders under `ios/` must be added to `source_files` in `ios/native-kits.podspec`.
|
|
63
|
+
- Screens update their own chrome at runtime through `@EnvironmentObject var navigation: Navigation` → `setOptions { $0.title = … }` (Compose `LocalNavigation`); bottom tabs keep one `Navigation` per tab. The host back bridge is `HostBackGestureBridge.shared` (`NavigatorManager` is a deprecated alias); a bottom tab off its first tab routes back to the first tab (`Navigator.tabBackHandler`, keyed by the hosting screen).
|
|
64
|
+
- Everything, navigation shell included, supports the pod's minimum **iOS 15** — no `@available` gates on public types. iOS 16+ APIs only behind `#available` with an iOS 15 path (`Layout`: `BottomSheet` `CapHeight`, `GridFlow` → `_VariadicView` flow row).
|
|
65
|
+
- **Usage is `NavigationContainer` + `Navigator` + `Navigation` only**; the kit owns the stack, transitions, overplays and back gesture. There is no `NavigationStack`: `StackTransition.swift` holds `StackHost` (representable) + `StackController`, the NavHost counterpart. Each entry gets its own `UIHostingController`, kept while on the stack and removed from the window while covered (state kept, onDisappear / onAppear like UINavigationController). Motion is a SwiftUI `offset` per card driven by one `coverage` value on a display link (a UIKit transform would move the safe area); it is Compose `StackTransition` 1:1 (RN `forStackTransitionWithOverlay`: linear 300ms slide, 0.3 parallax, `pink09` scrim to 0.6, 8pt edge shadow to 0.3). Card roots get the container environment via `.environment(\.self, …)` with the kit environment re-applied on top, so host environment objects reach screens.
|
|
66
|
+
- One back stack, root first (`Navigator.entries`, Compose's back stack): `present` appends a dialog entry that rises (FastOutSlowIn 300ms) and later pushes stack on top of it; `dismiss` drops the topmost dialog and everything above it (slides down whole); `popToRoot` returns to the active flow's root. Overplays (modal / bottom sheet / snackbar) render inside the screen they were shown over (`OverplayItem.screenId`, Compose `currentRootId`), so a later push slides over them.
|
|
67
|
+
- Host back gesture: `HostBackGestureBridge.started` / `progressed` / `completed` / `cancelled` scrub the pop (`Navigator.backStarted` …) only for a plain pop; tab back, `onBackHandler`, `preventBack`, an open overplay or a presented top wait for the release and go through `onBackSafe`. `SwipeBackHaptic` ticks at 50pt (`MaxApi.triggerEventVibration`, default = system light impact).
|
|
68
|
+
- Icons are compiled in: `ios/Icon/Icons.swift` is generated from Compose `utils/Icons.kt` by `node scripts/gen-ios-icons.mjs` — don't hand-edit it. A host `icon.json` in the main bundle only adds keys the kit lacks.
|
|
69
|
+
- Bundled assets (colors, Lottie JSON) live in `ios/Resources` and ship in the `MoMoUIKitsResources` bundle; load them through `MoMoUIKitsResources.bundle`, never `Bundle.main`.
|
|
70
|
+
|
|
71
|
+
## Screen layout / iPhone Duo (port of RN `useScreenLayout`, momo-kits !664)
|
|
72
|
+
|
|
73
|
+
- `layout/ScreenLayout.kt`: `getScreenLayout()` returns window size, raw top/bottom + side insets, `contentWidth`, header geometry, the iPhone Duo `sideBar` and `duoTopInset`. `NavigationContainer` computes it once and provides `LocalScreenLayout`; reactive to fold/unfold and rotation (window size + `WindowInsets`). Metrics (`DuoMetrics`, 72pt iPhone header floor, 20pt status-bar threshold) mirror RN `Layout/utils.ts` — change both together.
|
|
74
|
+
- iPhone-only: everything gates on `isPhoneIdiom()` (`platform/`), never on "iOS", so iPad windows keep the regular layout. Two detectors, as in RN: `isIPhoneDuo` (window aspect < 1.6 or asymmetric side insets) drives layout; `isIPhoneDuoDevice` (Apple's foldable check: a `.division` reserved region, queried with include-inactive; Swift also latches on `UIHingeInteraction`) drives popups, modal and bottom sheet.
|
|
75
|
+
- Header height is `headerHeight` (not `AppStatusBar + HEADER_HEIGHT`); the bar row is `headerBarHeight` and starts `headerStatusBarHeight` below the window top. When the status bar is off the top edge (iPhone landscape, iPhone Duo) `headerStatusBarHeight` is 0 so the title stays centered — except on the Duo outer display: in portrait the header is 76pt and the row starts at 14pt so the title centers on the camera punchout; in landscape the row centers on the camera when it sits at the top end of the strip (else on the back button) and the header reaches 8pt below the back button.
|
|
76
|
+
- iPhone Duo inner display in portrait (status bar on top): the header row shares the status bar band — `headerHeight` 94pt (2 × the clock digits' y 47), `headerStatusBarHeight` 0 — and `ScreenLayout.statusBarClearance` keeps it between the clock / battery clusters (from reserved regions, else 128pt on the right). Back and toolkit use the 48pt side bar controls laid out horizontally (`ToolkitLayout.SideBarRow`); the collapsed search and a centered title stay clear of the clusters. Port of momo-kits `90af33ba2`.
|
|
77
|
+
- iPhone Duo with a side strip ≥ 60pt: back + toolkit leave the bar for `HeaderSideBar` (`navigation/component/SideBar.kt`, 48pt glass controls); custom header-right stays in the bar. `sideBar.top` is 170pt in portrait, 120pt on the inner display in landscape (height ≥ 600), 24pt on the outer display in landscape, or below the header when the status bar is on top; UIKit's `statusBarFrame` is useless on Duo. Unlike RN the column is clamped above footer (24pt gap) / rail (12pt gap).
|
|
78
|
+
- 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.
|
|
79
|
+
- 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.
|
|
80
|
+
- Bar compression (`bottomtab/DuoBarCompression.kt`, HIG "Vertical controls"): when toolbar + rail don't fit in the strip, one is compressed — the toolkit into a `navigation_menu` button, the rail into one button showing the current tab — and tapping it swaps them; `BottomTab(compressionBehavior)` picks which starts compressed. If neither fits beside the other compressed, both rest compressed and a tap opens one for a moment. Side bar columns report their full height (0 when the screen puts nothing there) so the decision follows each screen's toolkit.
|
|
81
|
+
- Glass header: on Duo with a side bar, Default/Extended headers without search and with a `ScrollState` let content scroll under the bar (`usesDuoGlassHeader`); the bar fades in a translucent surface + hairline over 16pt (Compose has no backdrop blur — RN's fallback look), the gradient leaves with the content.
|
|
82
|
+
- Quick return header: glass headers and Animated headers (with a `ScrollState`) on the Duo side bar layout slide out with the content and back in on a reverse scroll; a half-shown header settles in or out once the scroll stops. The side bar is drawn apart from the header, so it stays put.
|
|
83
|
+
- Overlays on Duo: popups cap at 440pt wide and the room between `duoTopInset` and the bottom inset (body scrolls, actions stay); modal centers inside that room; bottom sheet uses the window height, centers at width ≥ 600 and pads by side insets in landscape.
|
|
84
|
+
- Font scale deliberately uses the short side (`min(w, h)`), unlike RN which reads the window width — landscape must not inflate every font.
|
|
85
|
+
- `StackScreen` pads content/footer/search/FAB by the side insets and provides an inset-consumed layout to its content; a bottom-tab root stays full width (tab bar + each tab's StackScreen handle insets). Grid columns use `contentWidth`.
|
|
86
|
+
|
|
50
87
|
## i18n / Localize (port of RN `Localize`)
|
|
51
88
|
|
|
52
89
|
- `application/Localize.kt`: everything i18n. `LocalizationObject` is a data class with required `vi` / `en` maps (`LocalizationObject(vi = mapOf(…), en = mapOf(…))`). `Localize` constructors take a `LocalizationObject` or a `(vi, en)` map pair; the constructor **merges** the kit defaults with the host data (host wins on key clash). Methods: `translate(key)` (missing/empty → returns the key), `translateData(map)` / `translateData(vi, en)`, `addTranslations` (existing keys win), `changeLanguage(lang)`. Default language `"vi"`, `"en"` only on exact match; language is snapshot state so a switch recomposes consumers.
|
|
@@ -69,9 +106,9 @@ The SwiftUI port differs from Compose in one important way: **it follows the hos
|
|
|
69
106
|
|
|
70
107
|
One formula, both platforms: the device-width scale (`width / 375`, capped at `+5pt`) and the OS Dynamic Type scale (capped at `1.5×`) are computed independently and the **larger wins**. When `useOSScaleRate` is false neither applies — the size is just multiplied by `userScaleRate`. Max font scale is fixed at 1.5; the old per-call/per-scope override is deprecated and ignored on both sides.
|
|
71
108
|
|
|
72
|
-
- **Compose** — `const/Typography.kt` (`scaleSize` overloads for `Float`/`Dp`/`TextUnit`/`TextStyle`). The `TextUnit` overload divides by `density.fontScale` so Compose's own `sp` scaling isn't applied twice. Inputs: `LocalContext.current` (`MiniAppContext.userScaleRate` / `useOSScaleRate`, per-field) falling back to
|
|
73
|
-
- **SwiftUI** — `ios/Typography/FontScale.swift`. `FontScale` is an immutable snapshot with `callAsFunction`, so a view reads `@Environment(\.momoFontScale) private var fontScale` and calls `fontScale(16)`. `
|
|
74
|
-
- **Never set `\.momoFontScale` directly** — same trap as `\.localize`. Inject via `.momoFontScale(_:context:)`, which holds
|
|
109
|
+
- **Compose** — `const/Typography.kt` (`scaleSize` overloads for `Float`/`Dp`/`TextUnit`/`TextStyle`). The `TextUnit` overload divides by `density.fontScale` so Compose's own `sp` scaling isn't applied twice. Inputs: `LocalContext.current` (`MiniAppContext.userScaleRate` / `useOSScaleRate`, per-field) falling back to `DesignSystemKits.getConfigs()`. On iOS, `platform/OsFontScale.ios.kt` observes `UIContentSizeCategoryDidChangeNotification` and overrides `LocalDensity` — Compose MP otherwise reads `fontScale` once at `ComposeUIViewController` creation. Wired at `NavigationContainer`.
|
|
110
|
+
- **SwiftUI** — `ios/Typography/FontScale.swift`. `FontScale` is an immutable snapshot with `callAsFunction`, so a view reads `@Environment(\.momoFontScale) private var fontScale` and calls `fontScale(16)`. `DesignSystemKits.shared` is the `ObservableObject` holding the host inputs and the OS factor (it observes `UIContentSizeCategory.didChangeNotification`); the host pushes them with `setConfigs(_:)`.
|
|
111
|
+
- **Never set `\.momoFontScale` directly** — same trap as `\.localize`. Inject via `.momoFontScale(_:context:)`, which holds `DesignSystemKits` as `@ObservedObject`; that is what re-publishes the snapshot. It also injects `\.designSystemKitsConfigs`, which `MomoText` (whitelist font) and `TrustBanner` (fallback after `ApplicationEnvironment.config`) read so a host push re-renders them. `NavigationContainer` does it (passing `MiniAppContext` so the per-mini-app override applies); a bare `UIHostingController` root must too. The environment default reads the live store, so an un-injected subtree still gets correct values — just not live ones.
|
|
75
112
|
- The global `scaleSize(_:)` and `Font.appFont(size:)` remain for call sites that can't read the environment (UIKit bridges, free functions). They are **not** reactive. `Font.header_default_bold` & friends are computed `static var`s for the same reason — as stored `static let`s they froze the scale at first access. `UIViewRepresentable`s should read `context.environment.momoFontScale` instead, and re-apply the font in `updateUIView`, not only in `makeUIView`.
|
|
76
113
|
|
|
77
114
|
## Code conventions
|
|
@@ -95,5 +132,12 @@ Prefer one tight line at the surprising spot over a paragraph. Match the (low) c
|
|
|
95
132
|
## Gotchas
|
|
96
133
|
|
|
97
134
|
- **Resource qualifier drift:** the generated Compose `Res` namespace is `vn.momo.uikits.resources`, but `utils/Resources.kt` and `platform/Platform.ios.kt` reference `vn.momo.compose.resources`. Keep bundled strings/dictionaries as plain Kotlin (the `defaultLanguage` map in `Localize.kt`), not `composeResources/values`, to avoid iOS framework resource-loading issues.
|
|
98
|
-
-
|
|
135
|
+
- `LiteScreen` keeps its own `application.HeaderType` enum (`DEFAULT`/`EXTENDED`/`NONE`), unrelated to the sealed `navigation.component.HeaderType`; its toolkit is the navigation `Toolkit` (through `LocalMaxApi`).
|
|
99
136
|
- iOS strings/maps must survive Kotlin/Native framework packaging — keep them compiled-in, not loaded from resource bundles.
|
|
137
|
+
|
|
138
|
+
## Code comments
|
|
139
|
+
- Only comment when necessary: non-obvious logic, workarounds, or the "why" behind a decision.
|
|
140
|
+
- Don't comment on what the code already says clearly (no "// increment i", no restating function names).
|
|
141
|
+
- Keep comments short: one line when possible, plain and easy to understand.
|
|
142
|
+
- No long comment blocks, no line-by-line narration, no lines that overflow the editor width.
|
|
143
|
+
- Don't leave comments about the change itself (e.g. "// updated this", "// new logic").
|
package/README.md
CHANGED
|
@@ -150,8 +150,8 @@ themeController(defaultTheme.copy(colors = defaultTheme.colors.copy(primary = Co
|
|
|
150
150
|
---
|
|
151
151
|
|
|
152
152
|
### Lưu ý khác
|
|
153
|
-
- `Screen`
|
|
154
|
-
- `
|
|
153
|
+
- `Screen` / `ApplicationContainer` cũ (Compose và SwiftUI) đã bị gỡ; dùng `NavigationContainer + StackScreen`.
|
|
154
|
+
- `KitDesignSystem.update(isWhiteList = …)` (`const/KitDesignSystem.kt`) bật font design system mới cho host trong whitelist; đừng kích hoạt nếu không cần.
|
|
155
155
|
- Khi thêm tài nguyên mới, đặt trong `shared/src/commonMain/composeResources` để Compose Multiplatform plugin sinh accessor tự động.
|
|
156
156
|
- Repo không bundle SDK bên thứ ba ngoại trừ `vn.momo.corex.maxapi`, Compose/Coil/Ktor **và Compottie** cho animation.
|
|
157
157
|
|
|
@@ -17,8 +17,8 @@ public class MiniAppContext {
|
|
|
17
17
|
public var toolkitConfig: [String: Any] = [:]
|
|
18
18
|
public var providerId: String = "momo"
|
|
19
19
|
public var permissions: [[String: Any]] = []
|
|
20
|
-
// Per-mini-app font-scale override; nil falls back to
|
|
21
|
-
// MiniAppContext.userScaleRate / useOSScaleRate, which win per field over
|
|
20
|
+
// Per-mini-app font-scale override; nil falls back to DesignSystemKits. Mirrors Compose's
|
|
21
|
+
// MiniAppContext.userScaleRate / useOSScaleRate, which win per field over DesignSystemKits.
|
|
22
22
|
public var userScaleRate: CGFloat? = nil
|
|
23
23
|
public var useOSScaleRate: Bool? = nil
|
|
24
24
|
|
|
@@ -37,10 +37,17 @@ public class MiniAppContext {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
public var
|
|
43
|
-
public var
|
|
40
|
+
// Value type so DesignSystemKitsConfigs can dedupe host pushes; mirrors Compose's DesignSystemConfig.
|
|
41
|
+
public struct KitConfig: Equatable {
|
|
42
|
+
public var trustBanner: TrustBannerData?
|
|
43
|
+
public var headerBar: String?
|
|
44
|
+
public var headerGradient: String?
|
|
45
|
+
|
|
46
|
+
public init(trustBanner: TrustBannerData? = nil, headerBar: String? = nil, headerGradient: String? = nil) {
|
|
47
|
+
self.trustBanner = trustBanner
|
|
48
|
+
self.headerBar = headerBar
|
|
49
|
+
self.headerGradient = headerGradient
|
|
50
|
+
}
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
public class ApplicationEnvironment: ObservableObject {
|
|
@@ -59,6 +66,17 @@ public class ApplicationEnvironment: ObservableObject {
|
|
|
59
66
|
}
|
|
60
67
|
}
|
|
61
68
|
|
|
69
|
+
extension ApplicationEnvironment {
|
|
70
|
+
/// Closes the mini app: through `composeApi` as before, else `maxApi` (Compose `maxApi.dismiss`).
|
|
71
|
+
func requestDismiss() {
|
|
72
|
+
if let composeApi {
|
|
73
|
+
composeApi.requestCallback(funcName: "dismiss", params: nil, onResponse: nil)
|
|
74
|
+
} else {
|
|
75
|
+
maxApi?.dismiss(completion: nil)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
62
80
|
private struct ApplicationEnvironmentKey: EnvironmentKey {
|
|
63
81
|
static let defaultValue = ApplicationEnvironment()
|
|
64
82
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// File: ios/Application/DesignSystemKits.swift
|
|
2
|
+
// Created At: 2026-10-02 14:17:29 +07:00
|
|
3
|
+
// Created By: AI
|
|
4
|
+
// AI Agent: Claude Code
|
|
5
|
+
// Model: claude-opus-5-5
|
|
6
|
+
|
|
7
|
+
// AI-GENERATED START: app-wide design-system holder mirroring Compose's DesignSystemKits
|
|
8
|
+
import SwiftUI
|
|
9
|
+
|
|
10
|
+
/// Host-owned design-system inputs; mirrors Compose's `DesignSystemKitsConfigs` field for field.
|
|
11
|
+
public struct DesignSystemKitsConfigs: Equatable {
|
|
12
|
+
public var userScaleRate: CGFloat
|
|
13
|
+
public var useOSScaleRate: Bool
|
|
14
|
+
public var isWhiteList: Bool
|
|
15
|
+
public var designSystemConfig: KitConfig?
|
|
16
|
+
|
|
17
|
+
public init(
|
|
18
|
+
userScaleRate: CGFloat = 1,
|
|
19
|
+
useOSScaleRate: Bool = true,
|
|
20
|
+
isWhiteList: Bool = false,
|
|
21
|
+
designSystemConfig: KitConfig? = nil
|
|
22
|
+
) {
|
|
23
|
+
self.userScaleRate = userScaleRate
|
|
24
|
+
self.useOSScaleRate = useOSScaleRate
|
|
25
|
+
self.isWhiteList = isWhiteList
|
|
26
|
+
self.designSystemConfig = designSystemConfig
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// App-wide holder for the SwiftUI kit, mirroring Compose's `DesignSystemKits`. The kit is sandboxed from the
|
|
31
|
+
/// host, so the host pushes `setConfigs(_:)` at startup and on every change; the OS Dynamic Type factor is
|
|
32
|
+
/// tracked here. Views re-render on change as long as the subtree got `.momoFontScale()` (`NavigationContainer`
|
|
33
|
+
/// applies it). Swift reserves `init`, so there is no `init(source:)` twin: the host observes and pushes.
|
|
34
|
+
public final class DesignSystemKits: ObservableObject {
|
|
35
|
+
public static let shared = DesignSystemKits()
|
|
36
|
+
|
|
37
|
+
@Published private var configs = DesignSystemKitsConfigs()
|
|
38
|
+
@Published public private(set) var osFontScale: CGFloat = DesignSystemKits.currentOsFontScale()
|
|
39
|
+
|
|
40
|
+
private init() {
|
|
41
|
+
NotificationCenter.default.addObserver(
|
|
42
|
+
self,
|
|
43
|
+
selector: #selector(contentSizeCategoryDidChange),
|
|
44
|
+
name: UIContentSizeCategory.didChangeNotification,
|
|
45
|
+
object: nil
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public func setConfigs(_ configs: DesignSystemKitsConfigs) {
|
|
50
|
+
onMain {
|
|
51
|
+
// Re-assigning an unchanged value would still publish and invalidate every kit subtree.
|
|
52
|
+
guard self.configs != configs else { return }
|
|
53
|
+
self.configs = configs
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public func getConfigs() -> DesignSystemKitsConfigs {
|
|
58
|
+
configs
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public var fontScale: FontScale {
|
|
62
|
+
FontScale(
|
|
63
|
+
userScaleRate: configs.userScaleRate,
|
|
64
|
+
useOSScaleRate: configs.useOSScaleRate,
|
|
65
|
+
osFontScale: osFontScale
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@objc private func contentSizeCategoryDidChange() {
|
|
70
|
+
let next = Self.currentOsFontScale()
|
|
71
|
+
guard next != osFontScale else { return }
|
|
72
|
+
osFontScale = next
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// Same factor Compose's iOS `ProvideOsFontScale` derives from
|
|
76
|
+
/// `UIFont.preferredFont(forTextStyle: .body).pointSize / 17`, so both iOS surfaces scale alike.
|
|
77
|
+
static func currentOsFontScale() -> CGFloat {
|
|
78
|
+
let base = UIFont.systemFont(ofSize: UIFont.labelFontSize)
|
|
79
|
+
return UIFontMetrics.default.scaledFont(for: base).pointSize / base.pointSize
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// Synchronous when already on main — an async hop would defer the change by a frame.
|
|
83
|
+
private func onMain(_ work: @escaping () -> Void) {
|
|
84
|
+
if Thread.isMainThread { work() } else { DispatchQueue.main.async(execute: work) }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private struct DesignSystemKitsConfigsKey: EnvironmentKey {
|
|
89
|
+
/// Computed, never stored: an un-injected subtree still reads the live holder, just not reactively.
|
|
90
|
+
static var defaultValue: DesignSystemKitsConfigs { DesignSystemKits.shared.getConfigs() }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public extension EnvironmentValues {
|
|
94
|
+
/// Injected by `.momoFontScale()`; never set it directly (same trap as `\.momoFontScale`).
|
|
95
|
+
var designSystemKitsConfigs: DesignSystemKitsConfigs {
|
|
96
|
+
get { self[DesignSystemKitsConfigsKey.self] }
|
|
97
|
+
set { self[DesignSystemKitsConfigsKey.self] = newValue }
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// AI-GENERATED END: app-wide design-system holder mirroring Compose's DesignSystemKits
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// File: ios/Application/DesignSystemKitsPayload.swift
|
|
2
|
+
// Created At: 2026-10-02 15:40:00 +07:00
|
|
3
|
+
// Created By: AI
|
|
4
|
+
// AI Agent: Claude Code
|
|
5
|
+
// Model: claude-opus-5-5
|
|
6
|
+
|
|
7
|
+
// AI-GENERATED START: JSON entry point for hosts that can't share Swift types with the kit
|
|
8
|
+
import Foundation
|
|
9
|
+
|
|
10
|
+
public extension DesignSystemKits {
|
|
11
|
+
/// Applies configs pushed as JSON by a host that can't share Swift types with the kit (e.g. the KMP bridge).
|
|
12
|
+
/// Invalid JSON is ignored and the current configs are kept.
|
|
13
|
+
func setConfigs(json: String) {
|
|
14
|
+
guard let data = json.data(using: .utf8),
|
|
15
|
+
let payload = try? JSONDecoder().decode(DesignSystemKitsPayload.self, from: data) else { return }
|
|
16
|
+
setConfigs(payload.configs)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Wire format; field names match the host's serializer (momo-app HostDesignSystemKits / DesignSystemData.kt).
|
|
21
|
+
private struct DesignSystemKitsPayload: Decodable {
|
|
22
|
+
let userScaleRate: CGFloat
|
|
23
|
+
let useOSScaleRate: Bool
|
|
24
|
+
let isWhiteList: Bool
|
|
25
|
+
let designSystemConfig: KitConfigPayload?
|
|
26
|
+
|
|
27
|
+
var configs: DesignSystemKitsConfigs {
|
|
28
|
+
DesignSystemKitsConfigs(
|
|
29
|
+
userScaleRate: userScaleRate,
|
|
30
|
+
useOSScaleRate: useOSScaleRate,
|
|
31
|
+
isWhiteList: isWhiteList,
|
|
32
|
+
designSystemConfig: designSystemConfig?.kitConfig
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private struct KitConfigPayload: Decodable {
|
|
38
|
+
let trustBanner: TrustBannerPayload?
|
|
39
|
+
let headerBar: String?
|
|
40
|
+
let headerGradient: String?
|
|
41
|
+
|
|
42
|
+
var kitConfig: KitConfig {
|
|
43
|
+
KitConfig(trustBanner: trustBanner?.trustBannerData, headerBar: headerBar, headerGradient: headerGradient)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private struct TrustBannerPayload: Decodable {
|
|
48
|
+
let content: [String: String]
|
|
49
|
+
let subContent: [String: String]
|
|
50
|
+
let pciImage: String?
|
|
51
|
+
let sslImage: String?
|
|
52
|
+
let urlConfig: String?
|
|
53
|
+
let icons: [String]?
|
|
54
|
+
let momoImage: String?
|
|
55
|
+
|
|
56
|
+
var trustBannerData: TrustBannerData {
|
|
57
|
+
TrustBannerData(
|
|
58
|
+
content: content,
|
|
59
|
+
subContent: subContent,
|
|
60
|
+
pciImage: pciImage,
|
|
61
|
+
momoImage: momoImage,
|
|
62
|
+
sslImage: sslImage,
|
|
63
|
+
icons: icons,
|
|
64
|
+
urlConfig: urlConfig ?? ""
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// AI-GENERATED END: JSON entry point for hosts that can't share Swift types with the kit
|
|
@@ -5,14 +5,41 @@
|
|
|
5
5
|
// Swift mirror of the host-app bridge exposed on Kotlin as `vn.momo.maxapi.IMaxApi`
|
|
6
6
|
// (`LocalMaxApi`). `IMaxApi` itself is an external compiled dependency with no
|
|
7
7
|
// source in this repo, so this protocol intentionally exposes only the methods
|
|
8
|
-
// this kit's Kotlin source actually calls
|
|
9
|
-
//
|
|
8
|
+
// this kit's Kotlin source actually calls — not a full guess at IMaxApi's real surface.
|
|
9
|
+
// Callbacks carry IMaxApi's `["response": …]` payload.
|
|
10
10
|
//
|
|
11
11
|
|
|
12
|
-
import
|
|
12
|
+
import UIKit
|
|
13
13
|
|
|
14
14
|
public protocol MaxApi {
|
|
15
15
|
func track(eventName: String, params: [String: Any?])
|
|
16
16
|
func dismiss(completion: (() -> Void)?)
|
|
17
17
|
func getLanguage(callback: @escaping ([String: Any?]?) -> Void)
|
|
18
|
+
/// `type` is the IMaxApi vibration name (`"light"`, …).
|
|
19
|
+
func triggerEventVibration(_ type: String)
|
|
20
|
+
// Toolkit (Compose `HeaderRight.Toolkit`).
|
|
21
|
+
func isFavoriteApp(_ params: [String: Any?], callback: @escaping ([String: Any?]?) -> Void)
|
|
22
|
+
func onToolAction(_ params: [String: Any?], callback: @escaping ([String: Any?]?) -> Void)
|
|
23
|
+
func showTools(_ params: [String: Any?], callback: @escaping ([String: Any?]?) -> Void)
|
|
24
|
+
func showHelpCenter(_ params: [String: Any?], callback: @escaping ([String: Any?]?) -> Void)
|
|
25
|
+
/// Back to the host home, closing every mini app.
|
|
26
|
+
func dismissAll(completion: (() -> Void)?)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public extension MaxApi {
|
|
30
|
+
/// Hosts that don't bridge haptics still get the system light tick.
|
|
31
|
+
func triggerEventVibration(_ type: String) {
|
|
32
|
+
UIImpactFeedbackGenerator(style: .light).impactOccurred()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Hosts that don't bridge the toolkit keep it inert, as before these joined the protocol.
|
|
36
|
+
func isFavoriteApp(_ params: [String: Any?], callback: @escaping ([String: Any?]?) -> Void) {}
|
|
37
|
+
func onToolAction(_ params: [String: Any?], callback: @escaping ([String: Any?]?) -> Void) {}
|
|
38
|
+
func showTools(_ params: [String: Any?], callback: @escaping ([String: Any?]?) -> Void) {}
|
|
39
|
+
func showHelpCenter(_ params: [String: Any?], callback: @escaping ([String: Any?]?) -> Void) {}
|
|
40
|
+
|
|
41
|
+
/// Without a home to return to, closing this mini app is the closest thing.
|
|
42
|
+
func dismissAll(completion: (() -> Void)?) {
|
|
43
|
+
dismiss(completion: completion)
|
|
44
|
+
}
|
|
18
45
|
}
|
package/ios/Avatar/Avatar.swift
CHANGED
|
@@ -74,12 +74,12 @@ public struct Avatar: View {
|
|
|
74
74
|
.clipShape(RoundedRectangle(cornerRadius: cornerRadius))
|
|
75
75
|
.overlay(
|
|
76
76
|
RoundedRectangle(cornerRadius: cornerRadius)
|
|
77
|
-
.
|
|
77
|
+
.strokeBorder(theme.colors.border.default, lineWidth: 1)
|
|
78
78
|
)
|
|
79
79
|
|
|
80
80
|
// Badge icons — outside clip so they are not cut by border
|
|
81
81
|
if shouldShowIconTop {
|
|
82
|
-
ImageView(LOGO_MOMO_URL)
|
|
82
|
+
ImageView(LOGO_MOMO_URL, contentMode: .fill)
|
|
83
83
|
.frame(width: badgeSizeDp, height: badgeSizeDp)
|
|
84
84
|
.clipShape(RoundedRectangle(cornerRadius: badgeCornerRadius))
|
|
85
85
|
.offset(x: 1, y: -1)
|
|
@@ -92,7 +92,7 @@ public struct Avatar: View {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
if shouldShowIconBottom {
|
|
95
|
-
ImageView(iconSupport)
|
|
95
|
+
ImageView(iconSupport, contentMode: .fill)
|
|
96
96
|
.frame(width: badgeSizeDp, height: badgeSizeDp)
|
|
97
97
|
.clipShape(RoundedRectangle(cornerRadius: badgeCornerRadius))
|
|
98
98
|
.offset(x: 1, y: 1)
|
|
@@ -107,6 +107,7 @@ public struct Avatar: View {
|
|
|
107
107
|
.frame(width: size.sizeDp, height: size.sizeDp)
|
|
108
108
|
.accessibilityElement(children: .ignore)
|
|
109
109
|
.accessibilityLabel(automationId)
|
|
110
|
+
.accessibilityIdentifier(automationId)
|
|
110
111
|
}
|
|
111
112
|
|
|
112
113
|
// MARK: Internal
|
package/ios/Badge/Badge.swift
CHANGED
|
@@ -33,7 +33,8 @@ public struct Badge: View {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
private func formatTitle(_ text: String) -> String {
|
|
36
|
-
|
|
36
|
+
// Digits too long for Int are well over 99.
|
|
37
|
+
if isNumber(text), (Int(text) ?? .max) > 99 {
|
|
37
38
|
return "99+"
|
|
38
39
|
}
|
|
39
40
|
return text
|
|
@@ -54,14 +55,15 @@ public struct Badge: View {
|
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
public var body: some View {
|
|
57
|
-
MomoText(formatTitle(label), typography: .actionXxsBold, color: Colors.black01)
|
|
58
|
+
MomoText(formatTitle(label), typography: .actionXxsBold, color: Colors.black01, maxLines: 1)
|
|
58
59
|
.padding(.horizontal, Spacing.XS)
|
|
59
|
-
.frame(minWidth: fontScale(16)
|
|
60
|
+
.frame(minWidth: fontScale(16))
|
|
61
|
+
.frame(height: fontScale(16))
|
|
60
62
|
.background(badgeColor)
|
|
61
63
|
.cornerRadius(Radius.M)
|
|
62
64
|
.overlay(
|
|
63
65
|
RoundedRectangle(cornerRadius: Radius.M)
|
|
64
|
-
.
|
|
66
|
+
.strokeBorder(Colors.black01, lineWidth: 1)
|
|
65
67
|
)
|
|
66
68
|
}
|
|
67
69
|
}
|
package/ios/Badge/BadgeDot.swift
CHANGED
|
@@ -160,7 +160,6 @@ public struct BadgeRibbon: View {
|
|
|
160
160
|
MomoText(label, typography: .labelXsMedium, color: Colors.black01, maxLines: 1)
|
|
161
161
|
.rotationEffect(rotation)
|
|
162
162
|
.frame(height: RibbonConstants.skewBodyHeight)
|
|
163
|
-
.padding(.horizontal, 8)
|
|
164
163
|
.background(backgroundColor)
|
|
165
164
|
|
|
166
165
|
rightTail
|
|
@@ -204,7 +203,9 @@ public struct BadgeRibbon: View {
|
|
|
204
203
|
/// rotation) — the label gets the same scale applied a second time so it stays right-reading
|
|
205
204
|
/// while only the ribbon shape flips.
|
|
206
205
|
public struct RoundedBadgeRibbon: View {
|
|
206
|
+
@Environment(\.appTheme) private var theme
|
|
207
207
|
@Environment(\.momoFontScale) private var fontScale
|
|
208
|
+
@Environment(\.designSystemKitsConfigs) private var designSystemKitsConfigs
|
|
208
209
|
let text: String
|
|
209
210
|
let position: RibbonPosition
|
|
210
211
|
|
|
@@ -224,7 +225,12 @@ public struct RoundedBadgeRibbon: View {
|
|
|
224
225
|
|
|
225
226
|
private var measuredTextSize: CGSize {
|
|
226
227
|
let fontSize = fontScale(TypographyStyle.labelXsMedium.fontSize)
|
|
227
|
-
let font =
|
|
228
|
+
let font = momoUIFont(
|
|
229
|
+
size: fontSize,
|
|
230
|
+
weight: .medium,
|
|
231
|
+
family: theme.font,
|
|
232
|
+
isWhiteList: designSystemKitsConfigs.isWhiteList
|
|
233
|
+
)
|
|
228
234
|
return text.size(withAttributes: [.font: font])
|
|
229
235
|
}
|
|
230
236
|
|
package/ios/Button/Button.swift
CHANGED
|
@@ -51,15 +51,17 @@ public extension ButtonType {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
func contentColor(loading: Bool, theme: Theme) -> Color {
|
|
54
|
+
let color: Color
|
|
54
55
|
switch self {
|
|
55
|
-
case .disabled:
|
|
56
|
-
case .primary:
|
|
57
|
-
case .secondary:
|
|
58
|
-
case .outline:
|
|
59
|
-
case .tonal:
|
|
60
|
-
case .danger:
|
|
61
|
-
case .text:
|
|
56
|
+
case .disabled: color = theme.colors.text.disable
|
|
57
|
+
case .primary: color = Colors.black01
|
|
58
|
+
case .secondary: color = theme.colors.text.default
|
|
59
|
+
case .outline: color = theme.colors.primary
|
|
60
|
+
case .tonal: color = theme.colors.primary
|
|
61
|
+
case .danger: color = Colors.black01
|
|
62
|
+
case .text: color = theme.colors.primary
|
|
62
63
|
}
|
|
64
|
+
return color.opacity(loading ? 0.75 : 1)
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
var borderColor: Color? {
|
|
@@ -219,9 +221,8 @@ public struct Button: View {
|
|
|
219
221
|
let color: Color? = useTintColor ? textColor : nil
|
|
220
222
|
|
|
221
223
|
if forceLoading {
|
|
222
|
-
LottieView(name: "lottie_circle_loader", loopMode: .loop)
|
|
224
|
+
LottieView(name: "lottie_circle_loader", loopMode: .loop, tintColor: color.map { UIColor($0) })
|
|
223
225
|
.frame(width: size.iconSize, height: size.iconSize)
|
|
224
|
-
.colorMultiply(color ?? Colors.black01)
|
|
225
226
|
} else if !icon.isEmpty {
|
|
226
227
|
Icon(source: icon, size: size.iconSize, color: color)
|
|
227
228
|
}
|
|
@@ -229,8 +230,8 @@ public struct Button: View {
|
|
|
229
230
|
}
|
|
230
231
|
|
|
231
232
|
/// Mirrors Compose's press feedback: the background shrinks via an animated
|
|
232
|
-
/// horizontal inset
|
|
233
|
-
///
|
|
233
|
+
/// horizontal inset and the whole button (background included) fades to 0.5,
|
|
234
|
+
/// matching `animatedPadding`/`alpha` in Button.kt.
|
|
234
235
|
private struct ButtonPressFeedbackStyle: ButtonStyle {
|
|
235
236
|
let isEnabled: Bool
|
|
236
237
|
let background: Color
|
|
@@ -245,7 +246,6 @@ private struct ButtonPressFeedbackStyle: ButtonStyle {
|
|
|
245
246
|
|
|
246
247
|
configuration.label
|
|
247
248
|
.modifier(HugContentOnLegacyOS(isFull: isFull))
|
|
248
|
-
.opacity(pressed ? 0.5 : 1)
|
|
249
249
|
.background(
|
|
250
250
|
RoundedRectangle(cornerRadius: radius)
|
|
251
251
|
.fill(background)
|
|
@@ -257,6 +257,9 @@ private struct ButtonPressFeedbackStyle: ButtonStyle {
|
|
|
257
257
|
)
|
|
258
258
|
.frame(height: height)
|
|
259
259
|
.clipShape(RoundedRectangle(cornerRadius: radius))
|
|
260
|
+
// `.text` has a clear background, which SwiftUI does not hit-test.
|
|
261
|
+
.contentShape(RoundedRectangle(cornerRadius: radius))
|
|
262
|
+
.opacity(pressed ? 0.5 : 1)
|
|
260
263
|
.animation(.easeInOut(duration: 0.1), value: pressed)
|
|
261
264
|
}
|
|
262
265
|
}
|