@momo-kits/native-kits 0.0.1-beta.4 → 0.2.0-kt22
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/README.md +176 -5
- package/ios/Application/ApplicationEnvironment.swift +50 -0
- package/ios/Application/Components.swift +263 -0
- package/ios/Application/ComposeApi.swift +22 -0
- package/ios/Application/FloatingButton.swift +172 -0
- package/ios/Application/HeaderRight.swift +271 -0
- package/ios/Application/Screen.swift +249 -0
- package/ios/Badge/Badge.swift +85 -0
- package/ios/Badge/BadgeDot.swift +31 -0
- package/ios/Badge/BadgeRibbon.swift +242 -0
- package/ios/Button/Button.swift +169 -89
- package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +126 -0
- package/ios/Checkbox/Checkbox.swift +81 -0
- package/ios/Chip/Chip.swift +96 -0
- package/ios/{Colors+Spacing → Colors+Radius+Spacing}/Colors.swift +21 -19
- package/ios/Colors+Radius+Spacing/Radius.swift +22 -0
- package/ios/Icon/Icon.swift +51 -0
- package/ios/Image/Image.swift +70 -0
- package/ios/Input/ErrorView.swift +55 -0
- package/ios/Input/Input.swift +159 -50
- package/ios/Input/InputPhoneNumber.swift +158 -0
- package/ios/Input/InputSearch.swift +238 -0
- package/ios/Input/InputTextArea.swift +242 -0
- package/ios/Lottie/LottieView.swift +86 -0
- package/ios/OTPKeyboard/KeyboardButton.swift +12 -4
- package/ios/OTPKeyboard/OTPKeyboard.swift +1 -1
- package/ios/Popup/PopupDisplay.swift +287 -0
- package/ios/Popup/PopupInput.swift +96 -0
- package/ios/Popup/PopupPromotion.swift +73 -0
- package/ios/PopupView/FullscreenPopup.swift +251 -0
- package/ios/{Popup → PopupView}/Modifiers.swift +6 -20
- package/ios/{Popup → PopupView}/PopupView.swift +38 -146
- package/ios/PopupView/Utils++.swift +281 -0
- package/ios/ScrollIndicator/ScrollIndicator.swift +110 -0
- package/ios/Swipeable/SwipeCell.swift +39 -31
- package/ios/Switch/Switch.swift +44 -0
- package/ios/Template/Logo/Logo.swift +75 -0
- package/ios/Template/TrustBanner/TrustBanner.swift +120 -0
- package/ios/Typography/Text.swift +140 -0
- package/ios/Typography/Typography.swift +70 -5
- package/ios/native-kits.podspec +20 -15
- package/package.json +6 -6
- package/CODE_OF_CONDUCT.md +0 -133
- package/CONTRIBUTING.md +0 -114
- package/LICENSE +0 -20
- package/android/build.gradle +0 -82
- package/android/gradle.properties +0 -5
- package/android/src/main/AndroidManifest.xml +0 -4
- package/android/src/main/java/com/momoplatform/nativekits/NativeKitsPackage.kt +0 -17
- package/android/src/main/java/com/momoplatform/nativekits/NativeKitsViewManager.kt +0 -20
- package/example/ios/Example/Assets.xcassets/AppIcon.appiconset/Contents.json +0 -63
- package/example/ios/Example/Assets.xcassets/Background.colorset/Contents.json +0 -38
- package/example/ios/Example/Assets.xcassets/Border.colorset/Contents.json +0 -38
- package/example/ios/Example/Assets.xcassets/Card.colorset/Contents.json +0 -38
- package/example/ios/Example/Assets.xcassets/Contents.json +0 -6
- package/example/ios/Example/Assets.xcassets/Error.colorset/Contents.json +0 -38
- package/example/ios/Example/Assets.xcassets/Primary.colorset/Contents.json +0 -38
- package/example/ios/Example/Assets.xcassets/PrimaryDark.colorset/Contents.json +0 -38
- package/example/ios/Example/Assets.xcassets/PrimaryLight.colorset/Contents.json +0 -38
- package/example/ios/Example/Assets.xcassets/Secondary.colorset/Contents.json +0 -38
- package/example/ios/Example/Assets.xcassets/Success.colorset/Contents.json +0 -38
- package/example/ios/Example/Assets.xcassets/Text.colorset/Contents.json +0 -38
- package/example/ios/Example/Assets.xcassets/TextSecondary.colorset/Contents.json +0 -38
- package/example/ios/Example/Assets.xcassets/Warning.colorset/Contents.json +0 -38
- package/example/ios/Example/ComponentDetail.swift +0 -35
- package/example/ios/Example/ComponentItem.swift +0 -24
- package/example/ios/Example/ComponentModel.swift +0 -33
- package/example/ios/Example/ContentView.swift +0 -53
- package/example/ios/Example/Example.entitlements +0 -10
- package/example/ios/Example/Foundation/ButtonDemo.swift +0 -80
- package/example/ios/Example/Foundation/Popup/ActionSheets.swift +0 -136
- package/example/ios/Example/Foundation/Popup/Floats.swift +0 -127
- package/example/ios/Example/Foundation/Popup/Popups.swift +0 -101
- package/example/ios/Example/Foundation/PopupDemo.swift +0 -366
- package/example/ios/Example/Foundation/TextInputDemo.swift +0 -31
- package/example/ios/Example/Foundation/TypographyDemo.swift +0 -53
- package/example/ios/Example/Foundation.swift +0 -26
- package/example/ios/Example/Info.plist +0 -18
- package/example/ios/Example/Library/SwipeableDemo.swift +0 -105
- package/example/ios/Example/Library.swift +0 -26
- package/example/ios/Example/Main.swift +0 -11
- package/example/ios/Example/Overview.swift +0 -20
- package/example/ios/Example/Preview Content/Preview Assets.xcassets/Contents.json +0 -6
- package/example/ios/Example/Resource/SFProText-Black.ttf +0 -0
- package/example/ios/Example/Resource/SFProText-Bold.ttf +0 -0
- package/example/ios/Example/Resource/SFProText-Heavy.ttf +0 -0
- package/example/ios/Example/Resource/SFProText-Light.ttf +0 -0
- package/example/ios/Example/Resource/SFProText-Medium.ttf +0 -0
- package/example/ios/Example/Resource/SFProText-Regular.ttf +0 -0
- package/example/ios/Example/Resource/SFProText-Semibold.ttf +0 -0
- package/example/ios/Example/Resource/SFProText-Thin.ttf +0 -0
- package/example/ios/Example/Resource/SFProText-Ultralight.ttf +0 -0
- package/example/ios/Example/Resource/foundation.json +0 -33
- package/example/ios/Example/Resource/library.json +0 -8
- package/example/ios/Example/Utils.swift +0 -30
- package/example/ios/Example/ViewModifier/DarkViewModifier.swift +0 -22
- package/example/ios/Example.xcodeproj/project.pbxproj +0 -570
- package/example/ios/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
- package/example/ios/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
- package/example/ios/Example.xcodeproj/project.xcworkspace/xcuserdata/wem.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- package/example/ios/Example.xcodeproj/xcuserdata/wem.xcuserdatad/xcschemes/xcschememanagement.plist +0 -14
- package/example/ios/Example.xcworkspace/contents.xcworkspacedata +0 -10
- package/example/ios/Example.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
- package/example/ios/Example.xcworkspace/xcuserdata/wem.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- package/example/ios/Example.xcworkspace/xcuserdata/wem.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +0 -6
- package/example/ios/Podfile +0 -6
- package/example/ios/Podfile.lock +0 -16
- package/example/ios/Pods/Local Podspecs/MoMoUIKits.podspec.json +0 -29
- package/example/ios/Pods/Manifest.lock +0 -16
- package/example/ios/Pods/Pods.xcodeproj/project.pbxproj +0 -661
- package/example/ios/Pods/Pods.xcodeproj/xcuserdata/wem.xcuserdatad/xcschemes/MoMoUIKits.xcscheme +0 -58
- package/example/ios/Pods/Pods.xcodeproj/xcuserdata/wem.xcuserdatad/xcschemes/Pods-Example.xcscheme +0 -58
- package/example/ios/Pods/Pods.xcodeproj/xcuserdata/wem.xcuserdatad/xcschemes/xcschememanagement.plist +0 -25
- package/example/ios/Pods/Target Support Files/MoMoUIKits/MoMoUIKits-dummy.m +0 -5
- package/example/ios/Pods/Target Support Files/MoMoUIKits/MoMoUIKits-prefix.pch +0 -12
- package/example/ios/Pods/Target Support Files/MoMoUIKits/MoMoUIKits-umbrella.h +0 -16
- package/example/ios/Pods/Target Support Files/MoMoUIKits/MoMoUIKits.debug.xcconfig +0 -12
- package/example/ios/Pods/Target Support Files/MoMoUIKits/MoMoUIKits.modulemap +0 -6
- package/example/ios/Pods/Target Support Files/MoMoUIKits/MoMoUIKits.release.xcconfig +0 -12
- package/example/ios/Pods/Target Support Files/Pods-Example/Pods-Example-acknowledgements.markdown +0 -3
- package/example/ios/Pods/Target Support Files/Pods-Example/Pods-Example-acknowledgements.plist +0 -29
- package/example/ios/Pods/Target Support Files/Pods-Example/Pods-Example-dummy.m +0 -5
- package/example/ios/Pods/Target Support Files/Pods-Example/Pods-Example-umbrella.h +0 -16
- package/example/ios/Pods/Target Support Files/Pods-Example/Pods-Example.debug.xcconfig +0 -14
- package/example/ios/Pods/Target Support Files/Pods-Example/Pods-Example.modulemap +0 -6
- package/example/ios/Pods/Target Support Files/Pods-Example/Pods-Example.release.xcconfig +0 -14
- package/ios/Colors+Spacing/Radius.swift +0 -11
- package/ios/Popup/Utils++.swift +0 -163
- package/ios/Theme.md +0 -18
- package/publish.sh +0 -28
- /package/ios/{Colors+Spacing → Colors+Radius+Spacing}/Spacing.swift +0 -0
package/README.md
CHANGED
|
@@ -1,6 +1,177 @@
|
|
|
1
|
-
##
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
## ComposeKit (Kotlin Multiplatform)
|
|
2
|
+
|
|
3
|
+
ComposeKit là bộ UI Kit đa nền tảng phục vụ Mini App/SDK của MoMo. Toàn bộ giao diện, điều hướng, token thiết kế và layer tích hợp với MaxAPI đều được viết bằng Kotlin Multiplatform để tái sử dụng cho Android, iOS (qua Compose Multiplatform) cũng như các host native.
|
|
4
|
+
|
|
5
|
+
### Mục tiêu & phạm vi
|
|
6
|
+
- Chuẩn hoá visual language của MoMo với kho component Compose đã mang sẵn màu sắc, typography, spacing, radius trong `vn.momo.compose.kit.const`.
|
|
7
|
+
- Cung cấp navigation stack thống nhất (`NavigationContainer`, `Navigator`, bottom tabs, modal/bottom sheet) hỗ trợ dynamic screen registry và MaxAPI dismiss/present.
|
|
8
|
+
- Cho phép host nhận context Mini App (`MiniAppContext`, `KitConfig`, `ApplicationContext`, `AppLanguage`) để tự động render Trust Banner, header assets, tracking.
|
|
9
|
+
- Đồng bộ hoá với native SwiftUI kit (`nativeSDK/ios`) nhằm cover các màn đang viết Swift/Objective‑C.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
### Cấu trúc thư mục
|
|
14
|
+
- `shared/`: module KMP chính. Chứa toàn bộ source `vn.momo.compose.kit.*`, compose resources (font SF Pro), logic publish Maven và podspec.
|
|
15
|
+
- `composeApp/`: sample Compose Multiplatform Android dùng để thử nhanh component, navigator, trust banner.
|
|
16
|
+
- `iosApp/`: project SwiftUI + CocoaPods để test framework iOS sinh ra từ `shared`.
|
|
17
|
+
- `nativeSDK/ios/`: Swift package + podspec `MoMoUIKits` (phụ thuộc `SDWebImageSwiftUI`, `SkeletonUI`) cho các màn hoàn toàn native.
|
|
18
|
+
- `publish_release.sh`, `.gitlab-ci.yml`: script tự bump patch version, chạy `:compose:publish...` khi commit có `[ci build]`.
|
|
19
|
+
- `build-output/`, `build/`: output tạm thời (không commit).
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
### Thành phần chính trong `shared`
|
|
24
|
+
|
|
25
|
+
**Design tokens & theming**
|
|
26
|
+
- `const/Colors.kt`, `Radius.kt`, `Spacing.kt`, `Typography.kt` định nghĩa token thống nhất.
|
|
27
|
+
- `const/Theme.kt` cung cấp `Theme`, `ColorScheme`, `AppTheme`, `AppThemeController`. Có sẵn `defaultTheme` & `darkTheme`, hỗ trợ inject assets (vd. header background từ `KitConfig`).
|
|
28
|
+
|
|
29
|
+
**Layout primitives & modifier**
|
|
30
|
+
- `layout/Card.kt`, `Section.kt`, `Item.kt` chứa khung section theo guideline Super App.
|
|
31
|
+
- `modifier/AutomationId`, `Conditional`, `Shadow`, `Size` giúp thêm accessibility id, điều kiện hoá modifier, shadow blur tuỳ nền.
|
|
32
|
+
|
|
33
|
+
**UI components**
|
|
34
|
+
- Buttons (`Button.kt`, `IconButton.kt`), typography helper (`Text.kt`, `Title.kt`), badge/tag (`Badge*`, `Tag.kt`), chip & switch (`Chip.kt`, `Switch.kt`), checkbox/radio.
|
|
35
|
+
- Input stack: `Input*` (OTP, PhoneNumber, Money, Search, DropDown, TextArea) với keyboard helpers.
|
|
36
|
+
- Pagination indicator (`PaginationDot/Number/Scroll/WhiteDot.kt`), skeleton loading (`Skeleton.kt`), trust UI (`TrustBanner.kt`).
|
|
37
|
+
- Popup/notify (`PopupNotify`, `PopupPromotion`), floating button, `LazyColumnWithBouncing`, Cupertino overscroll, `DateTimePicker` (Wheel UI + utils).
|
|
38
|
+
- Lottie animation (`components/animation/LottieAnimation`) dựng trên [Compottie](https://github.com/alexzhirkevich/compottie) để chạy animation JSON đồng nhất trên Android/iOS/Desktop.
|
|
39
|
+
|
|
40
|
+
**Navigation & application layer**
|
|
41
|
+
- `navigation/NavigationContainer`, `Navigation.kt`, `Navigator.kt`, `StackScreen.kt`, `BottomSheet.kt`, bottom tab helpers.
|
|
42
|
+
- `Navigator` hỗ trợ `push`, `replace`, `present`, `reset`, `pop(count)`, `showModal`, `showBottomSheet` với overlay registry (`OverplayComponentRegistry`).
|
|
43
|
+
- `application/NavigationContainer.kt`, `MiniAppContext`, `KitConfig`, `ApplicationContext`, `AppConfig`, `AppLanguage` cung cấp context cho header/trust banner, mapping sang map để gửi qua MaxAPI.
|
|
44
|
+
- `application/Screen.kt`, `Header*`, `Footer`, `FloatingButton` tạo template screen (đang deprecate, khuyến nghị dùng `NavigationContainer + StackScreen`).
|
|
45
|
+
|
|
46
|
+
**Platform utilities**
|
|
47
|
+
- `platform/Platform.kt` định nghĩa expect/actual cho `getPlatformName`, `BackHandler`, `getScreenHeight`, `NativePaint` mask filter. Android actual đi kèm `getAndroidBuildVersion`.
|
|
48
|
+
- `utils/Icons.kt`, `Resources.kt`, `getAppStatusBarHeight()` giúp truy xuất resource Compose đa nền tảng.
|
|
49
|
+
|
|
50
|
+
**Native bridge**
|
|
51
|
+
- `shared/shared.podspec` và `nativeSDK/ios/MoMoUIKits.podspec` cho phép embed vào iOS app via CocoaPods (deployment target 15.0, Swift 5). Phần native duy trì các SwiftUI component tương đương Compose (Button, Input, Popup, Trust Banner, Badge...).
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
### Yêu cầu môi trường
|
|
56
|
+
- JDK 17+, Gradle 8, Android Studio Hedgehog+ với Android SDK (compileSdk xem `libs.versions.toml`).
|
|
57
|
+
- Kotlin Multiplatform plugin + Compose Multiplatform (JetBrains 1.6.1+).
|
|
58
|
+
- Xcode 15, CocoaPods 1.12+ để build `iosApp` hoặc publish Pod.
|
|
59
|
+
- Truy cập mạng nội bộ GitLab Packages nếu cần publish/consume Maven repo `https://gitlab.mservice.com.vn/api/v4/projects/5400/packages/maven`.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
### Build & kiểm thử nhanh
|
|
64
|
+
|
|
65
|
+
| Tác vụ | Lệnh |
|
|
66
|
+
| --- | --- |
|
|
67
|
+
| Build thư viện KMP (AAR + klib + framework) | `./gradlew :compose:assembleRelease` |
|
|
68
|
+
| Publish local maven (nếu cần thử) | `./gradlew :compose:publishAllPublicationsToMavenLocal` |
|
|
69
|
+
| Sample Android | `./gradlew :composeApp:assembleDebug` |
|
|
70
|
+
| Sample iOS | mở `iosApp/iosApp.xcworkspace` rồi build trên Xcode |
|
|
71
|
+
|
|
72
|
+
Output Compose nằm trong `shared/build/outputs` và `shared/build/publications/*`. Debug fonts/resources ở `shared/src/commonMain/composeResources`.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
### Publish nội bộ (GitLab Packages)
|
|
77
|
+
1. Cập nhật `gradle.properties` nếu thay đổi group/artifact/version. Mặc định:
|
|
78
|
+
```
|
|
79
|
+
name=ComposeKit
|
|
80
|
+
group=vn.momo.kits
|
|
81
|
+
artifact.id=kits
|
|
82
|
+
version=0.1.2
|
|
83
|
+
url=https://gitlab.mservice.com.vn/api/v4/projects/5400/packages/maven
|
|
84
|
+
gitlab.user=upload_packages
|
|
85
|
+
gitlab.password=<token>
|
|
86
|
+
```
|
|
87
|
+
2. Chạy `./publish_release.sh`. Script sẽ:
|
|
88
|
+
- Đọc version hiện tại, query các version đã publish qua GitLab API.
|
|
89
|
+
- Bump patch (`X.Y.Z+1`) đến khi không trùng.
|
|
90
|
+
- Ghi lại `gradle.properties`, export `VERSION`, chạy `./gradlew clean` + `:compose:publishAllPublicationsToGitLabPackagesRepository`.
|
|
91
|
+
3. CI (`.gitlab-ci.yml`) tự động thực thi script này khi push với commit message chứa `[ci build]` (trừ khi branch khớp `engine/w*`).
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
### Tích hợp ComposeKit trong dự án KMP/Android
|
|
96
|
+
|
|
97
|
+
**Thêm dependency**
|
|
98
|
+
```kotlin
|
|
99
|
+
repositories {
|
|
100
|
+
maven { url = uri("https://gitlab.mservice.com.vn/api/v4/projects/5400/packages/maven") }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
commonMain.dependencies {
|
|
104
|
+
implementation("vn.momo.kits:kits:<version>")
|
|
105
|
+
}
|
|
6
106
|
```
|
|
107
|
+
|
|
108
|
+
**Khởi tạo navigation container**
|
|
109
|
+
```kotlin
|
|
110
|
+
val miniAppContext = MiniAppContext(appId = "...", appCode = "...", appIcon = "...", toolkitConfig = ...)
|
|
111
|
+
|
|
112
|
+
NavigationContainer(
|
|
113
|
+
initialScreen = { StackScreenExample() },
|
|
114
|
+
options = NavigationOptions(headerTitle = HeaderTitle.Default("Home")),
|
|
115
|
+
applicationContext = miniAppContext,
|
|
116
|
+
config = KitConfig(trustBanner = defaultBanner),
|
|
117
|
+
maxApi = maxApi, // IMaxApi implementation từ host
|
|
118
|
+
setNavigator = { navigator = it }
|
|
119
|
+
)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Bên trong screen, truy cập `LocalNavigator.current` để:
|
|
123
|
+
```kotlin
|
|
124
|
+
val navigator = LocalNavigator.current
|
|
125
|
+
navigator.push { DetailScreen() }
|
|
126
|
+
navigator.showBottomSheet(content = { BottomSheetContent() })
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Tuỳ biến theme bằng `AppThemeController`:
|
|
130
|
+
```kotlin
|
|
131
|
+
val themeController = AppThemeController.current
|
|
132
|
+
themeController(defaultTheme.copy(colors = defaultTheme.colors.copy(primary = Colors.pink_04)))
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`ApplicationContext.current`, `AppConfig.current`, `AppLanguage.current` giúp component (ví dụ `TrustBanner`) tự lấy dữ liệu trust banner và tracking `service_component_clicked`.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
### Tích hợp iOS / Native SDK
|
|
140
|
+
- `shared/shared.podspec` sinh framework Compose (static) mang tên theo `artifact.id` (`kits`). Add vào Podfile:
|
|
141
|
+
```ruby
|
|
142
|
+
pod 'kits', :path => '../shared'
|
|
143
|
+
```
|
|
144
|
+
- Với màn SwiftUI thuần native, dùng `nativeSDK/ios`:
|
|
145
|
+
```ruby
|
|
146
|
+
pod 'MoMoUIKits', :path => 'nativeSDK/ios'
|
|
147
|
+
```
|
|
148
|
+
Bộ này mirror hầu hết component Compose (Button, Input, Popup, TrustBanner...). `ApplicationEnvironment` chấp nhận `MiniAppContext`, `KitConfig` tương tự Kotlin.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
### Lưu ý khác
|
|
153
|
+
- `Screen` composable cũ được đánh dấu `@Deprecated`; sử dụng `NavigationContainer + StackScreen` để hưởng lợi dynamic route và animation.
|
|
154
|
+
- `DesignSystemWhiteList` trong `application/ApplicationContainer.kt` hỗ trợ fallback cho host cũ; đừng kích hoạt nếu không cần.
|
|
155
|
+
- Khi thêm tài nguyên mới, đặt trong `shared/src/commonMain/composeResources` để Compose Multiplatform plugin sinh accessor tự động.
|
|
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
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
### Animation Lottie đa nền tảng với Compottie
|
|
161
|
+
|
|
162
|
+
```kotlin
|
|
163
|
+
import io.github.alexzhirkevich.compottie.LottieCompositionSpec
|
|
164
|
+
import vn.momo.kits.components.animation.LottieAnimation
|
|
165
|
+
|
|
166
|
+
@Composable
|
|
167
|
+
fun SuccessState() {
|
|
168
|
+
LottieAnimation(
|
|
169
|
+
spec = LottieCompositionSpec.Url(
|
|
170
|
+
"https://assets10.lottiefiles.com/packages/lf20_jcikwtux.json"
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
- `shared/build.gradle.kts` đã bật `coreLibraryDesugaringEnabled` để tương thích `compottie-dot`/`compottie-network` với `minSdk = 24`.
|
|
177
|
+
- Nếu animation dùng asset/font nội bộ, kết hợp `compottie-resources` (`rememberResourcesAssetsManager`, `rememberResourcesFontManager`) để trỏ về `composeResources`.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
public protocol KitComposeApi {
|
|
4
|
+
func request(funcName: String, params: Any?) -> String
|
|
5
|
+
func request(funcName: String, params: Any?, onResponse: ((String) -> Void)?) -> String
|
|
6
|
+
func requestCallback(funcName: String, params: Any?, onResponse: ((String) -> Void)?)
|
|
7
|
+
func removeCallback(id: String)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
public class MiniAppContext {
|
|
11
|
+
public var appId: String = ""
|
|
12
|
+
public var appCode: String = ""
|
|
13
|
+
public var appName: Any? = nil
|
|
14
|
+
public var appIcon: String = ""
|
|
15
|
+
public var description: Any? = nil
|
|
16
|
+
public var support: [String: Any] = [:]
|
|
17
|
+
public var toolkitConfig: [String: Any] = [:]
|
|
18
|
+
public var providerId: String = "momo"
|
|
19
|
+
public var permissions: [[String: Any]] = []
|
|
20
|
+
|
|
21
|
+
public init(appId: String, appCode: String, appName: Any? = nil, appIcon: String, description: Any? = nil, support: [String: Any] = [:], toolkitConfig: [String: Any] = [:], providerId: String = "momo", permissions: [[String: Any]] = []) {
|
|
22
|
+
self.appId = appId
|
|
23
|
+
self.appCode = appCode
|
|
24
|
+
self.appName = appName
|
|
25
|
+
self.appIcon = appIcon
|
|
26
|
+
self.description = description
|
|
27
|
+
self.support = support
|
|
28
|
+
self.toolkitConfig = toolkitConfig
|
|
29
|
+
self.providerId = providerId
|
|
30
|
+
self.permissions = permissions
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public class KitConfig {
|
|
35
|
+
public var trustBanner: TrustBannerData? = nil
|
|
36
|
+
public var headerBar: String? = nil
|
|
37
|
+
public var headerGradient: String? = nil
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public class ApplicationEnvironment: ObservableObject {
|
|
41
|
+
let applicationContext: MiniAppContext?
|
|
42
|
+
let composeApi: KitComposeApi?
|
|
43
|
+
let config: KitConfig?
|
|
44
|
+
|
|
45
|
+
public init(applicationContext: MiniAppContext? = nil, composeApi: KitComposeApi? = nil, config: KitConfig? = nil) {
|
|
46
|
+
self.applicationContext = applicationContext
|
|
47
|
+
self.composeApi = composeApi
|
|
48
|
+
self.config = config
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Components.swift
|
|
3
|
+
// Pods
|
|
4
|
+
//
|
|
5
|
+
// Created by sophia on 24/1/25.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import SwiftUI
|
|
9
|
+
|
|
10
|
+
// MARK: - Enums
|
|
11
|
+
|
|
12
|
+
public enum TitlePosition {
|
|
13
|
+
case left, center
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public enum HeaderType {
|
|
17
|
+
case `default`, extended, none
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public enum AnimatedHeaderRatio {
|
|
21
|
+
case ratio16_9
|
|
22
|
+
case ratio1_1
|
|
23
|
+
case ratio3_2
|
|
24
|
+
|
|
25
|
+
var value: CGFloat {
|
|
26
|
+
switch self {
|
|
27
|
+
case .ratio16_9: return 16.0 / 9.0
|
|
28
|
+
case .ratio1_1: return 1.0
|
|
29
|
+
case .ratio3_2: return 3.0 / 2.0
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
// MARK: - Animated Header Config
|
|
36
|
+
|
|
37
|
+
public struct AnimatedHeader {
|
|
38
|
+
public var aspectRatio: AnimatedHeaderRatio = .ratio16_9
|
|
39
|
+
public var isSurface: Bool = true
|
|
40
|
+
public var composable: (_ scrollState: CGFloat) -> AnyView = { _ in AnyView(EmptyView()) }
|
|
41
|
+
|
|
42
|
+
public init(
|
|
43
|
+
aspectRatio: AnimatedHeaderRatio = .ratio16_9,
|
|
44
|
+
isSurface: Bool = true,
|
|
45
|
+
composable: @escaping (_ scrollState: CGFloat) -> AnyView = { _ in AnyView(EmptyView()) }
|
|
46
|
+
) {
|
|
47
|
+
self.aspectRatio = aspectRatio
|
|
48
|
+
self.isSurface = isSurface
|
|
49
|
+
self.composable = composable
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// MARK: - Utils
|
|
54
|
+
|
|
55
|
+
func safeAreaTopInset() -> CGFloat {
|
|
56
|
+
if #available(iOS 15.0, *) {
|
|
57
|
+
return UIApplication.shared.connectedScenes
|
|
58
|
+
.compactMap { ($0 as? UIWindowScene)?.keyWindow?.safeAreaInsets.top }
|
|
59
|
+
.first ?? 0
|
|
60
|
+
} else {
|
|
61
|
+
return UIApplication.shared.connectedScenes
|
|
62
|
+
.compactMap { ($0 as? UIWindowScene)?.windows.first?.safeAreaInsets.top }
|
|
63
|
+
.first ?? 0
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// MARK: - Header Background
|
|
68
|
+
|
|
69
|
+
public struct HeaderBackground: View {
|
|
70
|
+
var headerType: HeaderType = .default
|
|
71
|
+
var scrollState: CGFloat
|
|
72
|
+
var headerTransparent: Bool = false
|
|
73
|
+
var fullScreenContent: Bool = false
|
|
74
|
+
|
|
75
|
+
public init(
|
|
76
|
+
headerType: HeaderType = .default,
|
|
77
|
+
scrollState: CGFloat = 0,
|
|
78
|
+
headerTransparent: Bool = false,
|
|
79
|
+
fullScreenContent: Bool = false
|
|
80
|
+
) {
|
|
81
|
+
self.headerType = headerType
|
|
82
|
+
self.scrollState = scrollState
|
|
83
|
+
self.headerTransparent = headerTransparent
|
|
84
|
+
self.fullScreenContent = fullScreenContent
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public var body: some View {
|
|
88
|
+
|
|
89
|
+
let statusBarHeight = safeAreaTopInset()
|
|
90
|
+
let opacity = max(0, 1 - scrollState / 200)
|
|
91
|
+
let height = fullScreenContent ? 0 : statusBarHeight + 52
|
|
92
|
+
let backgroundColor = headerTransparent ? Color.clear : Colors.black01
|
|
93
|
+
|
|
94
|
+
Group {
|
|
95
|
+
switch headerType {
|
|
96
|
+
case .default:
|
|
97
|
+
ZStack(alignment: .bottom) {
|
|
98
|
+
Rectangle()
|
|
99
|
+
.fill(backgroundColor)
|
|
100
|
+
.opacity(headerTransparent ? 0 : opacity)
|
|
101
|
+
.frame(height: height)
|
|
102
|
+
.shadow(color: Colors.black20.opacity(0.2), radius: 10, x: 0, y: -2)
|
|
103
|
+
.background(backgroundColor)
|
|
104
|
+
.overlay(
|
|
105
|
+
headerTransparent ? AnyView(EmptyView()) :
|
|
106
|
+
AnyView(
|
|
107
|
+
Rectangle()
|
|
108
|
+
.fill(LinearGradient(
|
|
109
|
+
gradient: Gradient(colors: [
|
|
110
|
+
Color(red: 1, green: 0.8, blue: 0.87),
|
|
111
|
+
Color(red: 1, green: 0.8, blue: 0.87).opacity(0.5)
|
|
112
|
+
]),
|
|
113
|
+
startPoint: .top,
|
|
114
|
+
endPoint: .bottom))
|
|
115
|
+
.opacity(opacity)
|
|
116
|
+
.frame(height: height))
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if !headerTransparent {
|
|
120
|
+
Rectangle()
|
|
121
|
+
.fill(Color.black.opacity(0.1))
|
|
122
|
+
.frame(height: 1)
|
|
123
|
+
.frame(maxWidth: .infinity)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
case .extended:
|
|
128
|
+
ZStack {
|
|
129
|
+
if !headerTransparent {
|
|
130
|
+
Colors.black01
|
|
131
|
+
LinearGradient(
|
|
132
|
+
gradient: Gradient(colors: [
|
|
133
|
+
Color(red: 1, green: 0.8, blue: 0.87),
|
|
134
|
+
Color(red: 1, green: 0.8, blue: 0.87).opacity(0)
|
|
135
|
+
]),
|
|
136
|
+
startPoint: .top,
|
|
137
|
+
endPoint: .bottom
|
|
138
|
+
)
|
|
139
|
+
.opacity(opacity)
|
|
140
|
+
.frame(height: 154)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
case .none:
|
|
145
|
+
EmptyView()
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// MARK: - Header View
|
|
152
|
+
|
|
153
|
+
public struct Header: View {
|
|
154
|
+
var headerType: HeaderType = .default
|
|
155
|
+
var titlePosition: TitlePosition
|
|
156
|
+
var title: String
|
|
157
|
+
var headerRight: (()->any View)? = {HeaderRight()}
|
|
158
|
+
var goBack: (() -> Void)?
|
|
159
|
+
var opacity: CGFloat = 1
|
|
160
|
+
var animatedHeader: AnimatedHeader?
|
|
161
|
+
var scrollState: CGFloat = 0
|
|
162
|
+
var inputSearchProps: InputSearchProps?
|
|
163
|
+
var tintColor: Color?
|
|
164
|
+
|
|
165
|
+
public init(
|
|
166
|
+
headerType: HeaderType = .default,
|
|
167
|
+
titlePosition: TitlePosition,
|
|
168
|
+
title: String,
|
|
169
|
+
headerRight: (()->any View)? = {HeaderRight()},
|
|
170
|
+
goBack: (() -> Void)? = nil,
|
|
171
|
+
opacity: CGFloat = 1,
|
|
172
|
+
animatedHeader: AnimatedHeader? = nil,
|
|
173
|
+
scrollState: CGFloat = 0,
|
|
174
|
+
inputSearchProps: InputSearchProps? = nil,
|
|
175
|
+
tintColor: Color? = nil
|
|
176
|
+
) {
|
|
177
|
+
self.headerType = headerType
|
|
178
|
+
self.titlePosition = titlePosition
|
|
179
|
+
self.title = title
|
|
180
|
+
self.headerRight = headerRight
|
|
181
|
+
self.goBack = goBack
|
|
182
|
+
self.opacity = opacity
|
|
183
|
+
self.animatedHeader = animatedHeader
|
|
184
|
+
self.scrollState = scrollState
|
|
185
|
+
self.inputSearchProps = inputSearchProps
|
|
186
|
+
self.tintColor = tintColor
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
public var body: some View {
|
|
190
|
+
|
|
191
|
+
let backgroundButtonColor: Color = tintColor == Colors.black01 ? Colors.black20.opacity(0.6) : Colors.black01.opacity(0.6)
|
|
192
|
+
let borderColor: Color = tintColor == Colors.black01 ? Colors.black01.opacity(0.2) : Colors.black20.opacity(0.2)
|
|
193
|
+
|
|
194
|
+
if headerType != .none {
|
|
195
|
+
VStack(spacing: 0) {
|
|
196
|
+
ZStack {
|
|
197
|
+
// MARK: - Left and Right Controls
|
|
198
|
+
HStack {
|
|
199
|
+
if let goBack = goBack {
|
|
200
|
+
SwiftUI.Button(action: goBack) {
|
|
201
|
+
Circle()
|
|
202
|
+
.stroke(borderColor, lineWidth: 0.2)
|
|
203
|
+
.background(Circle().fill(backgroundButtonColor))
|
|
204
|
+
.frame(width: 28, height: 28)
|
|
205
|
+
.overlay(
|
|
206
|
+
Icon(source: "arrow-back", size: 20, color: tintColor ?? Colors.black17)
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
Spacer()
|
|
212
|
+
|
|
213
|
+
if let headerRight = headerRight {
|
|
214
|
+
AnyView(headerRight())
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
.padding(.horizontal, 12)
|
|
218
|
+
|
|
219
|
+
// MARK: - Title or Search
|
|
220
|
+
HStack {
|
|
221
|
+
if titlePosition == .left && goBack != nil {
|
|
222
|
+
Spacer().frame(width: 38)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if let inputProps = inputSearchProps {
|
|
226
|
+
InputSearch(
|
|
227
|
+
text: inputProps.$text,
|
|
228
|
+
buttonText: inputProps.buttonText,
|
|
229
|
+
showButtonText: inputProps.showButtonText,
|
|
230
|
+
showBorder: inputProps.showBorder,
|
|
231
|
+
placeholder: inputProps.placeholder,
|
|
232
|
+
onChangeText: inputProps.onChangeText,
|
|
233
|
+
onPressButtonText: inputProps.onPressButtonText,
|
|
234
|
+
error: inputProps.error,
|
|
235
|
+
disabled: inputProps.disabled,
|
|
236
|
+
icon: inputProps.icon,
|
|
237
|
+
iconColor: inputProps.iconColor,
|
|
238
|
+
onRightIconPressed: inputProps.onRightIconPressed,
|
|
239
|
+
onFocus: inputProps.onFocus,
|
|
240
|
+
onBlur: inputProps.onBlur,
|
|
241
|
+
loading: inputProps.loading,
|
|
242
|
+
fontWeight: inputProps.fontWeight,
|
|
243
|
+
keyboardType: inputProps.keyboardType
|
|
244
|
+
)
|
|
245
|
+
} else {
|
|
246
|
+
Text(title)
|
|
247
|
+
.font(.system(size: 17, weight: .bold))
|
|
248
|
+
.foregroundColor(tintColor ?? Colors.black17)
|
|
249
|
+
.frame(maxWidth: titlePosition == .center ? UIScreen.main.bounds.width - 252 : nil)
|
|
250
|
+
.frame(maxWidth: titlePosition == .center ? .infinity : UIScreen.main.bounds.width - 172, alignment: titlePosition == .center ? .center : .leading)
|
|
251
|
+
.lineLimit(1)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
Spacer()
|
|
255
|
+
}
|
|
256
|
+
.padding(.horizontal)
|
|
257
|
+
}
|
|
258
|
+
.background(Color.clear)
|
|
259
|
+
.frame(height: 52)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
public protocol ComposeApi {
|
|
4
|
+
func request(funcName: String, params: Any, _ completion: ((String) -> Void)?)
|
|
5
|
+
func request(funcName: String, params: Any) -> String
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Default implementation
|
|
9
|
+
public class ComposeApiImpl: ComposeApi {
|
|
10
|
+
public static let shared = ComposeApiImpl()
|
|
11
|
+
|
|
12
|
+
private init() {}
|
|
13
|
+
|
|
14
|
+
public func request(funcName: String, params: Any, _ completion: ((String) -> Void)?) {
|
|
15
|
+
// Implement your API request logic here
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public func request(funcName: String, params: Any) -> String {
|
|
19
|
+
// Implement your API request logic here
|
|
20
|
+
return ""
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
import Combine
|
|
3
|
+
|
|
4
|
+
public enum FABSize {
|
|
5
|
+
case small
|
|
6
|
+
case large
|
|
7
|
+
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
public enum FABPosition {
|
|
11
|
+
case right
|
|
12
|
+
case center
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public struct FabProps {
|
|
16
|
+
public let icon: String?
|
|
17
|
+
public let iconColor: Color?
|
|
18
|
+
public let label: String?
|
|
19
|
+
public let onClick: (() -> Void)?
|
|
20
|
+
public let containerColor: Color?
|
|
21
|
+
public let size: FABSize?
|
|
22
|
+
public let iconSize: CGFloat?
|
|
23
|
+
public let scrollOffset: CGFloat?
|
|
24
|
+
public let position: FABPosition?
|
|
25
|
+
public let bottomPadding: CGFloat?
|
|
26
|
+
public let renderComponent: (() -> AnyView)?
|
|
27
|
+
|
|
28
|
+
public init(
|
|
29
|
+
icon: String? = nil,
|
|
30
|
+
iconColor: Color? = .white,
|
|
31
|
+
label: String? = nil,
|
|
32
|
+
onClick: (() -> Void)? = nil,
|
|
33
|
+
containerColor: Color? = Colors.primary,
|
|
34
|
+
size: FABSize? = .small,
|
|
35
|
+
iconSize: CGFloat? = nil,
|
|
36
|
+
scrollOffset: CGFloat? = nil,
|
|
37
|
+
position: FABPosition? = .right,
|
|
38
|
+
bottomPadding: CGFloat? = nil,
|
|
39
|
+
renderComponent: (() -> AnyView)? = nil
|
|
40
|
+
) {
|
|
41
|
+
self.icon = icon
|
|
42
|
+
self.iconColor = iconColor
|
|
43
|
+
self.label = label
|
|
44
|
+
self.onClick = onClick
|
|
45
|
+
self.containerColor = containerColor
|
|
46
|
+
self.size = size
|
|
47
|
+
self.iconSize = iconSize
|
|
48
|
+
self.scrollOffset = scrollOffset
|
|
49
|
+
self.position = position
|
|
50
|
+
self.bottomPadding = bottomPadding
|
|
51
|
+
self.renderComponent = renderComponent
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public struct FloatingButton: View {
|
|
56
|
+
@State private var lastScrollOffset: CGFloat = 0
|
|
57
|
+
@State private var isExpanded: Bool = false
|
|
58
|
+
@State private var internalScrollOffset: CGFloat = 0
|
|
59
|
+
@State private var showText: Bool = true
|
|
60
|
+
@State private var fullWidth: CGFloat? = nil
|
|
61
|
+
@State private var currentWidth: CGFloat = 0
|
|
62
|
+
@State private var lastDirection: String? = nil
|
|
63
|
+
|
|
64
|
+
private let props: FabProps
|
|
65
|
+
private var keyboardOffset: CGFloat
|
|
66
|
+
private var scrollOffset: CGFloat
|
|
67
|
+
private var bottomPadding: CGFloat { props.bottomPadding ?? 12 + keyboardOffset}
|
|
68
|
+
private var position: FABPosition? { props.position ?? .right }
|
|
69
|
+
private var containerColor: Color? { props.containerColor ?? Colors.primary }
|
|
70
|
+
private var iconColor: Color? { props.iconColor ?? .white }
|
|
71
|
+
private var label: String? { props.label }
|
|
72
|
+
private var icon: String? { props.icon }
|
|
73
|
+
private var size: FABSize? { props.size }
|
|
74
|
+
private var iconSize: CGFloat? { props.iconSize ?? defaultIconSize}
|
|
75
|
+
private var onClick: (() -> Void)? { props.onClick }
|
|
76
|
+
private var renderComponent: (() -> AnyView)? { props.renderComponent }
|
|
77
|
+
|
|
78
|
+
public init(props: FabProps, keyboardOffset: CGFloat = 0, scrollOffset: CGFloat = 0) {
|
|
79
|
+
self.props = props
|
|
80
|
+
self.keyboardOffset = keyboardOffset
|
|
81
|
+
self.scrollOffset = scrollOffset
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private var height: CGFloat {
|
|
85
|
+
if(iconSize == nil) {
|
|
86
|
+
size == .small ? 36 : 48
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
iconSize!
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private var minWidth: CGFloat {
|
|
94
|
+
size == .small ? 36 : 48
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private var defaultIconSize: CGFloat {
|
|
98
|
+
size == .small ? 12 : 24
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
public var body: some View {
|
|
102
|
+
Group {
|
|
103
|
+
if ((icon?.isEmpty) == nil), let render = renderComponent {
|
|
104
|
+
render()
|
|
105
|
+
} else {
|
|
106
|
+
content
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
.onTapGesture {
|
|
110
|
+
onClick?()
|
|
111
|
+
}
|
|
112
|
+
.padding(Spacing.M)
|
|
113
|
+
.frame(height: height + (Spacing.M * 2))
|
|
114
|
+
.background(containerColor)
|
|
115
|
+
.cornerRadius(100)
|
|
116
|
+
.padding(.bottom, bottomPadding)
|
|
117
|
+
.frame(alignment: position == .center ? .center : .trailing)
|
|
118
|
+
.onAppear {
|
|
119
|
+
internalScrollOffset = scrollOffset
|
|
120
|
+
isExpanded = label != nil
|
|
121
|
+
}
|
|
122
|
+
.onReceive(Just(scrollOffset)) { newValue in
|
|
123
|
+
let direction = newValue > internalScrollOffset ? "down" : "up"
|
|
124
|
+
|
|
125
|
+
if direction != lastDirection {
|
|
126
|
+
lastDirection = direction
|
|
127
|
+
if direction == "down" {
|
|
128
|
+
withAnimation(.easeOut(duration: 0.1)) {
|
|
129
|
+
isExpanded = false
|
|
130
|
+
}
|
|
131
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
|
132
|
+
showText = false
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
showText = true
|
|
136
|
+
withAnimation(.easeIn(duration: 0.1)) {
|
|
137
|
+
isExpanded = true
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private var content: some View {
|
|
146
|
+
HStack(spacing: 0) {
|
|
147
|
+
ImageView(icon ?? "")
|
|
148
|
+
.scaledToFit()
|
|
149
|
+
.frame(width: iconSize, height: iconSize)
|
|
150
|
+
.foregroundColor(iconColor)
|
|
151
|
+
|
|
152
|
+
if let label = label {
|
|
153
|
+
if showText {
|
|
154
|
+
Spacer().frame(width: 8)
|
|
155
|
+
Text(label)
|
|
156
|
+
.foregroundColor(.white)
|
|
157
|
+
.font(.system(size: 16, weight: .bold))
|
|
158
|
+
.lineLimit(1)
|
|
159
|
+
.background(
|
|
160
|
+
GeometryReader { geo in
|
|
161
|
+
Color.clear.onAppear {
|
|
162
|
+
fullWidth = geo.size.width
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
)
|
|
166
|
+
.opacity(isExpanded ? 1 : 0)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|