@momo-kits/native-kits 0.163.1-beta.3-debug → 0.163.1-beta.5-debug
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +1 -1
- package/compose/build.gradle.kts +2 -1
- package/compose/build.gradle.kts.backup +1 -0
- package/compose/compose.podspec +1 -1
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/OsFontScale.android.kt +7 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +8 -10
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +3 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +56 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +11 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +40 -55
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +69 -26
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +3 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +21 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +3 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +22 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +28 -21
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +4 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +112 -115
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +11 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +3 -4
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +3 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +2 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/OsFontScale.kt +9 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +66 -0
- package/gradle/libs.versions.toml +2 -0
- package/gradle.properties +1 -1
- package/ios/Application/ApplicationEnvironment.swift +11 -1
- package/ios/Application/Components.swift +34 -44
- package/ios/Application/FloatingButton.swift +10 -9
- package/ios/Application/HeaderRight.swift +79 -45
- package/ios/Application/Navigation/BottomTab/BottomTab.swift +103 -0
- package/ios/Application/Navigation/BottomTab/BottomTabBar.swift +215 -0
- package/ios/Application/Navigation/BottomTab/CurvedContainer.swift +65 -0
- package/ios/Application/Navigation/HeaderBackProps.swift +66 -0
- package/ios/Application/Navigation/HeaderTitle.swift +57 -0
- package/ios/Application/Navigation/HeaderUser.swift +209 -0
- package/ios/Application/Navigation/NavigationContainer.swift +176 -0
- package/ios/Application/Navigation/NavigationOptions.swift +109 -0
- package/ios/Application/Navigation/Navigator.swift +364 -0
- package/ios/Application/Navigation/Overplay/BottomSheet.swift +188 -0
- package/ios/Application/Navigation/Overplay/ModalScreen.swift +68 -0
- package/ios/Application/Navigation/Overplay/SnackBar.swift +144 -0
- package/ios/Application/Navigation/component/Header.swift +178 -0
- package/ios/Application/Navigation/component/HeaderBackground.swift +83 -0
- package/ios/Application/Navigation/component/HeaderColor.swift +65 -0
- package/ios/Application/Screen.swift +2 -1
- package/ios/Application/StackScreen.swift +453 -0
- package/ios/Avatar/Avatar.swift +193 -0
- package/ios/Badge/Badge.swift +11 -14
- package/ios/Badge/BadgeRibbon.swift +12 -21
- package/ios/BaselineView/BaselineView.swift +163 -0
- package/ios/Button/Button.swift +61 -22
- package/ios/Carousel/Carousel.swift +181 -0
- package/ios/Checkbox/Checkbox.swift +14 -13
- package/ios/Chip/Chip.swift +23 -9
- package/ios/Collapse/Collapse.swift +182 -0
- package/ios/Colors+Radius+Spacing/Colors.swift +12 -12
- package/ios/Colors+Radius+Spacing/Theme.swift +295 -0
- package/ios/DateTimePicker/DateTimePicker.swift +210 -0
- package/ios/DateTimePicker/DateTimePickerTypes.swift +55 -0
- package/ios/DateTimePicker/DateTimePickerUtils.swift +167 -0
- package/ios/DateTimePicker/WheelPicker.swift +161 -0
- package/ios/Divider/Divider.swift +16 -0
- package/ios/Extensions/Color++.swift +32 -0
- package/ios/Icon/Icon.swift +6 -1
- package/ios/IconButton/IconButton.swift +112 -0
- package/ios/Input/Input.swift +41 -25
- package/ios/Input/InputPhoneNumber.swift +20 -19
- package/ios/Input/InputSearch.swift +100 -47
- package/ios/Input/InputTextArea.swift +66 -40
- package/ios/InputDropDown/InputDropDown.swift +198 -0
- package/ios/InputMoney/InputMoney.swift +335 -0
- package/ios/InputOTP/InputOTP.swift +210 -0
- package/ios/Loader/Loader.swift +113 -0
- package/ios/MoMoUIKits.podspec +5 -0
- package/ios/Pagination/PaginationDot.swift +61 -0
- package/ios/Pagination/PaginationNumber.swift +35 -0
- package/ios/Pagination/PaginationScroll.swift +101 -0
- package/ios/Pagination/PaginationWhiteDot.swift +37 -0
- package/ios/Popup/PopupDisplay.swift +8 -16
- package/ios/Popup/PopupInput.swift +2 -8
- package/ios/Popup/PopupNotify.swift +219 -0
- package/ios/ProgressInfo/ProgressInfo.swift +294 -0
- package/ios/Radio/Radio.swift +70 -0
- package/ios/Rating/Rating.swift +82 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/AccentColor.colorset/Contents.json +11 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/Background.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/Border.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/Card.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/Contents.json +6 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/Error.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/Primary.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/PrimaryDark.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/PrimaryLight.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/Secondary.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/Success.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/Text.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/TextSecondary.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/Contents.json +9 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/background-default.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/background-disable.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/background-pressed.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/background-selected.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/background-surface.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/background-tonal.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/border-default.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/border-disable.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/gradient.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/primary.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/secondary.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/success-container.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/success-primary.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/success-secondary.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/text-default.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/text-disable.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/text-hint.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/text-secondary.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/Warning.colorset/Contents.json +38 -0
- package/ios/Skeleton/Skeleton.swift +99 -0
- package/ios/Slider/Slider.swift +237 -0
- package/ios/Stepper/Stepper.swift +220 -0
- package/ios/Steps/Steps.swift +451 -0
- package/ios/SuggestAction/SuggestAction.swift +97 -0
- package/ios/Swipeable/SwipeCell.swift +185 -0
- package/ios/Swipeable/SwipeCellModel.swift +21 -0
- package/ios/TabView/TabView.swift +530 -0
- package/ios/Tag/Tag.swift +98 -0
- package/ios/Title/Title.swift +269 -0
- package/ios/Tooltip/Tooltip.swift +482 -0
- package/ios/Typography/FontScaleStore.swift +19 -0
- package/ios/Typography/Text.swift +86 -17
- package/ios/Typography/Typography.swift +12 -14
- package/ios/Uploader/Uploader.swift +183 -0
- package/ios/native-kits.podspec +61 -8
- package/ios-demo/MoMoUIKitsDemo.podspec +20 -0
- package/ios-demo/README.md +120 -0
- package/ios-demo/Resources/dot_loading.json +558 -0
- package/ios-demo/Resources/icon.json +4052 -0
- package/ios-demo/Resources/lottie_circle_loader.json +1 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/DemoScaffold.swift +48 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsBadgeDemoView.swift +25 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsButtonDemoView.swift +26 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsCheckboxDemoView.swift +19 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsChipDemoView.swift +33 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsInputDemoView.swift +48 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsPopupDemoView.swift +21 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsRadioDemoView.swift +17 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsTypographyDemoView.swift +28 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/MoMoUIKitsDemoContext.swift +31 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/MoMoUIKitsDemoFactory.swift +24 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/AnimatedHeaderDemo.swift +110 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/AvatarDemo.swift +93 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/BadgeDemo.swift +269 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/BaselineView.swift +117 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/BottomTabDemo.swift +89 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ButtonDemo.swift +100 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/CarouselDemo.swift +126 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/CheckboxDemo.swift +63 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ChipDemo.swift +20 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/CollapseDemo.swift +93 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +107 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DateTimePickerDemo.swift +124 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DividerDemo.swift +14 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/HeaderUserDemo.swift +49 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +278 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IconButtonDemo.swift +58 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ImageDemo.swift +78 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InformationDemo.swift +138 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputDemo.swift +111 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputDropDownDemo.swift +136 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputMoneyDemo.swift +107 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputOTPDemo.swift +73 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputPhoneNumberDemo.swift +100 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputSearchDemo.swift +32 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputTextAreaDemo.swift +57 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/LoaderDemo.swift +70 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +227 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PaginationDemo.swift +72 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupDisplayDemo.swift +175 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupNotifyDemo.swift +121 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupPromotionDemo.swift +76 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ProgressInfoDemo.swift +89 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/RadioDemo.swift +32 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/RatingDemo.swift +56 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SkeletonDemo.swift +32 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SliderDemo.swift +150 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SnackBarDemo.swift +138 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/StepperDemo.swift +103 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/StepsDemo.swift +94 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SuggestActionDemo.swift +79 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SwipeDemo.swift +152 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SwitchDemo.swift +33 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TabViewDemo.swift +190 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TagDemo.swift +34 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ThemeDemo.swift +182 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TitleDemo.swift +109 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TooltipDemo.swift +187 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TypographyDemo.swift +87 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/UploaderDemo.swift +131 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SwiftUIDemoBrowserView.swift +51 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/UIKitsDemoModels.swift +15 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/UIKitsDemoRegistry.swift +73 -0
- package/package.json +1 -1
- package/publish.sh +50 -0
- package/scripts/gen-ios-color.mjs +136 -0
- package/example/ios/Example.xcodeproj/xcuserdata/huynhdung.xcuserdatad/xcschemes/xcschememanagement.plist +0 -14
- package/example/ios/Example.xcworkspace/xcuserdata/huynhdung.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- package/example/ios/Example.xcworkspace/xcuserdata/huynhdung.xcuserdatad/xcschemes/xcschememanagement.plist +0 -5
- package/example/ios/Pods/Pods.xcodeproj/xcuserdata/huynhdung.xcuserdatad/xcschemes/MoMoUIKits.xcscheme +0 -58
- package/example/ios/Pods/Pods.xcodeproj/xcuserdata/huynhdung.xcuserdatad/xcschemes/Pods-Example.xcscheme +0 -58
- package/example/ios/Pods/Pods.xcodeproj/xcuserdata/huynhdung.xcuserdatad/xcschemes/SDWebImage.xcscheme +0 -58
- package/example/ios/Pods/Pods.xcodeproj/xcuserdata/huynhdung.xcuserdatad/xcschemes/SDWebImageSwiftUI.xcscheme +0 -58
- package/example/ios/Pods/Pods.xcodeproj/xcuserdata/huynhdung.xcuserdatad/xcschemes/SkeletonUI.xcscheme +0 -58
- package/example/ios/Pods/Pods.xcodeproj/xcuserdata/huynhdung.xcuserdatad/xcschemes/xcschememanagement.plist +0 -46
- package/ios/Application/FontScaleStore.swift +0 -59
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
//
|
|
2
|
+
// BottomTabBar.swift
|
|
3
|
+
// MoMoUIKits
|
|
4
|
+
//
|
|
5
|
+
// SwiftUI mirror of Compose's `BottomTabBar` (navigation/bottomtab/BottomTabBar.kt).
|
|
6
|
+
// Renders the row of tab items, an animated selection indicator, a top shadow,
|
|
7
|
+
// badges/dots, and an optional center floating button (with the curved notch).
|
|
8
|
+
//
|
|
9
|
+
|
|
10
|
+
import SwiftUI
|
|
11
|
+
|
|
12
|
+
// MARK: - Models
|
|
13
|
+
|
|
14
|
+
/// Mirrors Compose's `BottomTabItem`.
|
|
15
|
+
public struct BottomTabItem: Identifiable {
|
|
16
|
+
public let id = UUID()
|
|
17
|
+
public let name: String
|
|
18
|
+
public let label: String
|
|
19
|
+
public let icon: String
|
|
20
|
+
public let showDot: Bool
|
|
21
|
+
public let badgeLabel: String?
|
|
22
|
+
public let options: NavigationOptions?
|
|
23
|
+
public let screen: () -> AnyView
|
|
24
|
+
|
|
25
|
+
public init(
|
|
26
|
+
name: String,
|
|
27
|
+
label: String,
|
|
28
|
+
icon: String,
|
|
29
|
+
showDot: Bool = false,
|
|
30
|
+
badgeLabel: String? = nil,
|
|
31
|
+
options: NavigationOptions? = nil,
|
|
32
|
+
@ViewBuilder screen: @escaping () -> some View
|
|
33
|
+
) {
|
|
34
|
+
self.name = name
|
|
35
|
+
self.label = label
|
|
36
|
+
self.icon = icon
|
|
37
|
+
self.showDot = showDot
|
|
38
|
+
self.badgeLabel = badgeLabel
|
|
39
|
+
self.options = options
|
|
40
|
+
self.screen = { AnyView(screen()) }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// Mirrors Compose's `BottomTabFloatingButton`.
|
|
45
|
+
public struct BottomTabFloatingButton {
|
|
46
|
+
public let icon: String
|
|
47
|
+
public let label: String
|
|
48
|
+
public let onPress: () -> Void
|
|
49
|
+
|
|
50
|
+
public init(icon: String, label: String, onPress: @escaping () -> Void) {
|
|
51
|
+
self.icon = icon
|
|
52
|
+
self.label = label
|
|
53
|
+
self.onPress = onPress
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let bottomTabFloatingButtonWidth: CGFloat = 75
|
|
58
|
+
let BOTTOM_TAB_BAR_HEIGHT: CGFloat = 64
|
|
59
|
+
|
|
60
|
+
// MARK: - Tab bar
|
|
61
|
+
|
|
62
|
+
struct BottomTabBar: View {
|
|
63
|
+
let items: [BottomTabItem]
|
|
64
|
+
let floatingButton: BottomTabFloatingButton?
|
|
65
|
+
let selectedIndex: Int
|
|
66
|
+
let onTabSelected: (Int) -> Void
|
|
67
|
+
|
|
68
|
+
private var screenWidth: CGFloat { UIScreen.main.bounds.width }
|
|
69
|
+
private var mid: Int { items.count / 2 }
|
|
70
|
+
|
|
71
|
+
private var tabWidth: CGFloat {
|
|
72
|
+
if floatingButton != nil {
|
|
73
|
+
return (screenWidth - bottomTabFloatingButtonWidth) / CGFloat(items.count)
|
|
74
|
+
}
|
|
75
|
+
return screenWidth / CGFloat(items.count)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private var indicatorOffset: CGFloat {
|
|
79
|
+
let adjust: CGFloat = (floatingButton != nil && selectedIndex >= mid) ? bottomTabFloatingButtonWidth : 0
|
|
80
|
+
return tabWidth * CGFloat(selectedIndex) + tabWidth / 2 - screenWidth / 2 + adjust
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
var body: some View {
|
|
84
|
+
ZStack(alignment: .bottom) {
|
|
85
|
+
HStack(spacing: 0) {
|
|
86
|
+
if floatingButton == nil {
|
|
87
|
+
tabItems(from: 0, to: items.count)
|
|
88
|
+
} else {
|
|
89
|
+
tabItems(from: 0, to: mid)
|
|
90
|
+
Spacer()
|
|
91
|
+
.frame(width: bottomTabFloatingButtonWidth)
|
|
92
|
+
.padding(.horizontal, Spacing.XXS)
|
|
93
|
+
tabItems(from: mid, to: items.count)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
.frame(maxWidth: .infinity)
|
|
97
|
+
.frame(height: BOTTOM_TAB_BAR_HEIGHT)
|
|
98
|
+
.background(Colors.black01)
|
|
99
|
+
|
|
100
|
+
// Animated selection indicator.
|
|
101
|
+
RoundedRectangle(cornerRadius: 2)
|
|
102
|
+
.fill(Colors.primary)
|
|
103
|
+
.frame(width: 44, height: 2)
|
|
104
|
+
.offset(x: indicatorOffset, y: -BOTTOM_TAB_BAR_HEIGHT + 2)
|
|
105
|
+
.animation(.easeInOut(duration: 0.25), value: selectedIndex)
|
|
106
|
+
|
|
107
|
+
// Top shadow.
|
|
108
|
+
LinearGradient(
|
|
109
|
+
colors: [Color.black.opacity(0.05), .clear],
|
|
110
|
+
startPoint: .bottom,
|
|
111
|
+
endPoint: .top
|
|
112
|
+
)
|
|
113
|
+
.frame(height: 6)
|
|
114
|
+
.frame(maxWidth: .infinity)
|
|
115
|
+
.offset(y: -BOTTOM_TAB_BAR_HEIGHT)
|
|
116
|
+
|
|
117
|
+
if let floatingButton {
|
|
118
|
+
CenterFloatingButton(data: floatingButton)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@ViewBuilder
|
|
124
|
+
private func tabItems(from: Int, to: Int) -> some View {
|
|
125
|
+
ForEach(from..<to, id: \.self) { index in
|
|
126
|
+
TabBarItem(
|
|
127
|
+
item: items[index],
|
|
128
|
+
selected: index == selectedIndex,
|
|
129
|
+
onClick: { onTabSelected(index) }
|
|
130
|
+
)
|
|
131
|
+
.frame(maxWidth: .infinity)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// MARK: - Tab item
|
|
137
|
+
|
|
138
|
+
private struct TabBarItem: View {
|
|
139
|
+
let item: BottomTabItem
|
|
140
|
+
let selected: Bool
|
|
141
|
+
let onClick: () -> Void
|
|
142
|
+
|
|
143
|
+
private var tintColor: Color { selected ? Colors.primary : Colors.black10 }
|
|
144
|
+
|
|
145
|
+
/// Mirrors Compose's `formatLabel`: hide a badge that is the number "0".
|
|
146
|
+
private var badgeLabel: String? {
|
|
147
|
+
guard let label = item.badgeLabel else { return nil }
|
|
148
|
+
if let n = Int(label), n == 0 { return nil }
|
|
149
|
+
return label
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
var body: some View {
|
|
153
|
+
ZStack(alignment: .topTrailing) {
|
|
154
|
+
VStack(spacing: 0) {
|
|
155
|
+
Icon(source: item.icon, size: 28, color: tintColor)
|
|
156
|
+
.padding(Spacing.XS)
|
|
157
|
+
MomoText(
|
|
158
|
+
item.label,
|
|
159
|
+
typography: selected ? .labelXsMedium : .descriptionXsRegular,
|
|
160
|
+
color: tintColor,
|
|
161
|
+
textAlign: .center,
|
|
162
|
+
maxLines: 1
|
|
163
|
+
)
|
|
164
|
+
.truncationMode(.tail)
|
|
165
|
+
}
|
|
166
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
167
|
+
.padding(.horizontal, Spacing.S)
|
|
168
|
+
.padding(.vertical, Spacing.S)
|
|
169
|
+
|
|
170
|
+
if let badgeLabel {
|
|
171
|
+
if badgeLabel.isEmpty {
|
|
172
|
+
BadgeDot(size: .small)
|
|
173
|
+
.offset(x: -Spacing.S, y: Spacing.XS)
|
|
174
|
+
} else {
|
|
175
|
+
Badge(label: badgeLabel)
|
|
176
|
+
.offset(x: -Spacing.XS, y: 0)
|
|
177
|
+
}
|
|
178
|
+
} else if item.showDot {
|
|
179
|
+
BadgeDot(size: .small)
|
|
180
|
+
.offset(x: -Spacing.S, y: Spacing.XS)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
184
|
+
.padding(.horizontal, Spacing.XXS)
|
|
185
|
+
.contentShape(Rectangle())
|
|
186
|
+
.onTapGesture { onClick() }
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// MARK: - Center floating button
|
|
191
|
+
|
|
192
|
+
private struct CenterFloatingButton: View {
|
|
193
|
+
let data: BottomTabFloatingButton
|
|
194
|
+
|
|
195
|
+
var body: some View {
|
|
196
|
+
VStack(spacing: 0) {
|
|
197
|
+
CurvedContainer()
|
|
198
|
+
Circle()
|
|
199
|
+
.fill(Colors.primary)
|
|
200
|
+
.frame(width: 48, height: 48)
|
|
201
|
+
.overlay(Icon(source: data.icon, size: 28, color: .white))
|
|
202
|
+
Spacer().frame(height: 4)
|
|
203
|
+
MomoText(data.label, typography: .labelXsMedium, color: Colors.black01, textAlign: .center, maxLines: 1)
|
|
204
|
+
.frame(maxWidth: .infinity)
|
|
205
|
+
.frame(height: 14)
|
|
206
|
+
.background(Colors.primary)
|
|
207
|
+
.clipShape(RoundedRectangle(cornerRadius: Radius.XL))
|
|
208
|
+
}
|
|
209
|
+
.frame(width: bottomTabFloatingButtonWidth)
|
|
210
|
+
.padding(.horizontal, Spacing.XXS)
|
|
211
|
+
.padding(.vertical, Spacing.S)
|
|
212
|
+
.contentShape(Rectangle())
|
|
213
|
+
.onTapGesture { data.onPress() }
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
//
|
|
2
|
+
// CurvedContainer.swift
|
|
3
|
+
// MoMoUIKits
|
|
4
|
+
//
|
|
5
|
+
// SwiftUI mirror of Compose's `CurvedContainer` (navigation/bottomtab/CurvedContainer.kt).
|
|
6
|
+
// Draws the surface-colored notch that sits behind the center floating button,
|
|
7
|
+
// carving a circular cut-out into the tab bar's top edge.
|
|
8
|
+
//
|
|
9
|
+
|
|
10
|
+
import SwiftUI
|
|
11
|
+
|
|
12
|
+
struct CurvedContainer: View {
|
|
13
|
+
var circleSize: CGFloat = 48
|
|
14
|
+
var tabBarItemIconSize: CGFloat = 28
|
|
15
|
+
var circleOverSize: CGFloat = 2
|
|
16
|
+
var width: CGFloat = 71
|
|
17
|
+
var height: CGFloat = 16
|
|
18
|
+
var color: Color = Colors.black01
|
|
19
|
+
|
|
20
|
+
var body: some View {
|
|
21
|
+
let circlePositionTop = circleSize - tabBarItemIconSize - 10
|
|
22
|
+
let circleOverTop = circlePositionTop + circleOverSize
|
|
23
|
+
let circleR = circleSize / 2
|
|
24
|
+
let circleOverR = circleR + circleOverSize
|
|
25
|
+
let centerX = width / 2
|
|
26
|
+
|
|
27
|
+
CurvedShape(circleOverTop: circleOverTop, centerX: centerX, circleOverR: circleOverR, width: width)
|
|
28
|
+
.fill(color)
|
|
29
|
+
.frame(width: width, height: height)
|
|
30
|
+
.offset(y: 24)
|
|
31
|
+
.zIndex(-1)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// Mirrors Compose's `getPath`: a flat bar with a smooth circular dip in the
|
|
36
|
+
/// middle, sized to cradle the floating button.
|
|
37
|
+
private struct CurvedShape: Shape {
|
|
38
|
+
let circleOverTop: CGFloat
|
|
39
|
+
let centerX: CGFloat
|
|
40
|
+
let circleOverR: CGFloat
|
|
41
|
+
let width: CGFloat
|
|
42
|
+
|
|
43
|
+
func path(in rect: CGRect) -> Path {
|
|
44
|
+
var path = Path()
|
|
45
|
+
path.move(to: CGPoint(x: 0, y: circleOverTop))
|
|
46
|
+
path.addLine(to: CGPoint(x: centerX - circleOverR - 40, y: circleOverTop))
|
|
47
|
+
|
|
48
|
+
path.addCurve(
|
|
49
|
+
to: CGPoint(x: centerX, y: 0),
|
|
50
|
+
control1: CGPoint(x: centerX - circleOverR, y: circleOverTop),
|
|
51
|
+
control2: CGPoint(x: centerX - circleOverR, y: 0)
|
|
52
|
+
)
|
|
53
|
+
path.addCurve(
|
|
54
|
+
to: CGPoint(x: centerX + circleOverR + 40, y: circleOverTop),
|
|
55
|
+
control1: CGPoint(x: centerX + circleOverR, y: 0),
|
|
56
|
+
control2: CGPoint(x: centerX + circleOverR, y: circleOverTop)
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
path.addLine(to: CGPoint(x: width, y: circleOverTop))
|
|
60
|
+
path.addLine(to: CGPoint(x: width, y: 60))
|
|
61
|
+
path.addLine(to: CGPoint(x: 0, y: 60))
|
|
62
|
+
path.closeSubpath()
|
|
63
|
+
return path
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
//
|
|
2
|
+
// HeaderBackProps.swift
|
|
3
|
+
// MoMoUIKits
|
|
4
|
+
//
|
|
5
|
+
// SwiftUI mirror of Compose's `HeaderBackProps` + `PopupNotifyProps`/`PopupAction`.
|
|
6
|
+
// When `preventBack` is set, `Navigator.onBackSafe` shows a confirm popup
|
|
7
|
+
// before actually leaving the screen — same behavior as Compose.
|
|
8
|
+
//
|
|
9
|
+
|
|
10
|
+
import SwiftUI
|
|
11
|
+
|
|
12
|
+
/// Mirrors Compose's `PopupAction` (a titled button with an optional handler).
|
|
13
|
+
public struct PopupAction {
|
|
14
|
+
public let title: String
|
|
15
|
+
public let onPress: (() -> Void)?
|
|
16
|
+
|
|
17
|
+
public init(title: String, onPress: (() -> Void)? = nil) {
|
|
18
|
+
self.title = title
|
|
19
|
+
self.onPress = onPress
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/// Mirrors Compose's `PopupNotifyProps` as consumed by `headerBackProps.preventBack`.
|
|
24
|
+
public struct PreventBackProps {
|
|
25
|
+
public var image: String
|
|
26
|
+
public var title: String
|
|
27
|
+
public var description: String
|
|
28
|
+
public var error: String
|
|
29
|
+
public var primary: PopupAction?
|
|
30
|
+
public var secondary: PopupAction?
|
|
31
|
+
public var buttonDirection: PopupButtonDirection
|
|
32
|
+
public var isShowCloseIcon: Bool
|
|
33
|
+
public var onIconClose: (() -> Void)?
|
|
34
|
+
|
|
35
|
+
public init(
|
|
36
|
+
image: String = "",
|
|
37
|
+
title: String = "",
|
|
38
|
+
description: String = "",
|
|
39
|
+
error: String = "",
|
|
40
|
+
primary: PopupAction? = nil,
|
|
41
|
+
secondary: PopupAction? = nil,
|
|
42
|
+
buttonDirection: PopupButtonDirection = .row,
|
|
43
|
+
isShowCloseIcon: Bool = true,
|
|
44
|
+
onIconClose: (() -> Void)? = nil
|
|
45
|
+
) {
|
|
46
|
+
self.image = image
|
|
47
|
+
self.title = title
|
|
48
|
+
self.description = description
|
|
49
|
+
self.error = error
|
|
50
|
+
self.primary = primary
|
|
51
|
+
self.secondary = secondary
|
|
52
|
+
self.buttonDirection = buttonDirection
|
|
53
|
+
self.isShowCloseIcon = isShowCloseIcon
|
|
54
|
+
self.onIconClose = onIconClose
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// Mirrors Compose's `HeaderBackProps`.
|
|
59
|
+
public struct HeaderBackProps {
|
|
60
|
+
/// When non-nil, a confirm popup is shown before the back navigation runs.
|
|
61
|
+
public var preventBack: PreventBackProps?
|
|
62
|
+
|
|
63
|
+
public init(preventBack: PreventBackProps? = nil) {
|
|
64
|
+
self.preventBack = preventBack
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
//
|
|
2
|
+
// HeaderTitle.swift
|
|
3
|
+
// MoMoUIKits
|
|
4
|
+
//
|
|
5
|
+
// SwiftUI mirror of Compose's `sealed class HeaderTitle` (navigation/component/Header.kt).
|
|
6
|
+
// The header can show either a plain title (`.default`) or a rich "user" title
|
|
7
|
+
// with an avatar group, subtitle, inline icons and a status dot (`.user`).
|
|
8
|
+
// (Compose's `Location`/`Journey` variants are not rendered yet, so they are
|
|
9
|
+
// omitted here for parity with the implemented `HeaderUser`.)
|
|
10
|
+
//
|
|
11
|
+
|
|
12
|
+
import SwiftUI
|
|
13
|
+
|
|
14
|
+
/// Data backing a `.user` header title (mirrors `HeaderTitle.User`).
|
|
15
|
+
public struct HeaderUserData {
|
|
16
|
+
public var title: String
|
|
17
|
+
public var subTitle: String?
|
|
18
|
+
public var image: [String]?
|
|
19
|
+
public var dotColor: Color?
|
|
20
|
+
public var tintColor: Color?
|
|
21
|
+
public var onPress: (() -> Void)?
|
|
22
|
+
public var icons: [String]
|
|
23
|
+
public var isLoading: Bool
|
|
24
|
+
|
|
25
|
+
public init(
|
|
26
|
+
title: String,
|
|
27
|
+
subTitle: String? = nil,
|
|
28
|
+
image: [String]? = nil,
|
|
29
|
+
dotColor: Color? = nil,
|
|
30
|
+
tintColor: Color? = nil,
|
|
31
|
+
onPress: (() -> Void)? = nil,
|
|
32
|
+
icons: [String] = [],
|
|
33
|
+
isLoading: Bool = false
|
|
34
|
+
) {
|
|
35
|
+
self.title = title
|
|
36
|
+
self.subTitle = subTitle
|
|
37
|
+
self.image = image
|
|
38
|
+
self.dotColor = dotColor
|
|
39
|
+
self.tintColor = tintColor
|
|
40
|
+
self.onPress = onPress
|
|
41
|
+
self.icons = icons
|
|
42
|
+
self.isLoading = isLoading
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// Mirrors Compose's `HeaderTitle` sealed class.
|
|
47
|
+
public enum HeaderTitle {
|
|
48
|
+
case `default`(String)
|
|
49
|
+
case user(HeaderUserData)
|
|
50
|
+
|
|
51
|
+
/// Plain title text, when this is a `.default` title (used by the legacy
|
|
52
|
+
/// string-based `Header`/`NavigationOptions` paths).
|
|
53
|
+
public var defaultTitle: String? {
|
|
54
|
+
if case let .default(text) = self { return text }
|
|
55
|
+
return nil
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
//
|
|
2
|
+
// HeaderUser.swift
|
|
3
|
+
// MoMoUIKits
|
|
4
|
+
//
|
|
5
|
+
// SwiftUI mirror of Compose's `HeaderUser` (navigation/component/HeaderUser.kt).
|
|
6
|
+
// Renders a `HeaderTitle.User`: an avatar group (1..n avatars with overflow),
|
|
7
|
+
// a title with optional inline icons, and an optional subtitle. Shows a
|
|
8
|
+
// skeleton shimmer placeholder while `isLoading`.
|
|
9
|
+
//
|
|
10
|
+
|
|
11
|
+
import SwiftUI
|
|
12
|
+
|
|
13
|
+
@available(iOS 16.0, *)
|
|
14
|
+
public struct HeaderUserView: View {
|
|
15
|
+
private let data: HeaderUserData
|
|
16
|
+
|
|
17
|
+
public init(_ data: HeaderUserData) {
|
|
18
|
+
self.data = data
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private let avatarSize: CGFloat = Spacing.XXL // 32
|
|
22
|
+
|
|
23
|
+
private var maxWidth: CGFloat {
|
|
24
|
+
UIScreen.main.bounds.width - 172
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public var body: some View {
|
|
28
|
+
if data.isLoading {
|
|
29
|
+
shimmer
|
|
30
|
+
} else {
|
|
31
|
+
content
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private var content: some View {
|
|
36
|
+
HStack(spacing: 0) {
|
|
37
|
+
HStack(spacing: 0) {
|
|
38
|
+
ZStack(alignment: .bottomTrailing) {
|
|
39
|
+
AvatarGroup(urls: data.image ?? [], title: data.title, size: avatarSize)
|
|
40
|
+
if let dotColor = data.dotColor {
|
|
41
|
+
Circle()
|
|
42
|
+
.fill(dotColor)
|
|
43
|
+
.frame(width: 10, height: 10)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
.frame(width: avatarSize, height: avatarSize)
|
|
47
|
+
|
|
48
|
+
Spacer().frame(width: 6)
|
|
49
|
+
|
|
50
|
+
VStack(alignment: .leading, spacing: 0) {
|
|
51
|
+
HStack(spacing: 0) {
|
|
52
|
+
MomoText(data.title, typography: .actionXsBold, color: data.tintColor ?? Colors.black17, maxLines: 1)
|
|
53
|
+
.truncationMode(.tail)
|
|
54
|
+
|
|
55
|
+
if !data.icons.isEmpty {
|
|
56
|
+
Spacer().frame(width: 6)
|
|
57
|
+
HStack(spacing: 0) {
|
|
58
|
+
ForEach(Array(data.icons.enumerated()), id: \.offset) { _, icon in
|
|
59
|
+
ImageView(icon)
|
|
60
|
+
.frame(width: 16, height: 16)
|
|
61
|
+
.clipShape(RoundedRectangle(cornerRadius: 4))
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if let subTitle = data.subTitle, !subTitle.isEmpty {
|
|
68
|
+
Spacer().frame(height: 2)
|
|
69
|
+
MomoText(subTitle, typography: .descriptionXsRegular, color: data.tintColor ?? Colors.black17, maxLines: 1)
|
|
70
|
+
.truncationMode(.tail)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
.frame(maxWidth: maxWidth, alignment: .leading)
|
|
75
|
+
|
|
76
|
+
Spacer(minLength: 0)
|
|
77
|
+
}
|
|
78
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
79
|
+
.padding(.vertical, Spacing.XS)
|
|
80
|
+
.contentShape(Rectangle())
|
|
81
|
+
.onTapGesture { data.onPress?() }
|
|
82
|
+
.allowsHitTesting(data.onPress != nil)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private var shimmer: some View {
|
|
86
|
+
HStack(spacing: Spacing.M / 2) {
|
|
87
|
+
Circle()
|
|
88
|
+
.fill(Colors.black05)
|
|
89
|
+
.frame(width: avatarSize, height: avatarSize)
|
|
90
|
+
VStack(alignment: .leading, spacing: Spacing.XXS) {
|
|
91
|
+
RoundedRectangle(cornerRadius: 8)
|
|
92
|
+
.fill(Colors.black05)
|
|
93
|
+
.frame(width: 120, height: 18)
|
|
94
|
+
RoundedRectangle(cornerRadius: 8)
|
|
95
|
+
.fill(Colors.black05)
|
|
96
|
+
.frame(width: 120, height: 12)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
100
|
+
.padding(.vertical, Spacing.XS)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// MARK: - Avatar group
|
|
105
|
+
|
|
106
|
+
/// Mirrors Compose's `AvatarGroup`: lays out 1..4 avatars in a fixed collage,
|
|
107
|
+
/// falls back to initials when empty, and shows a "+n" overflow tile beyond 4.
|
|
108
|
+
@available(iOS 16.0, *)
|
|
109
|
+
private struct AvatarGroup: View {
|
|
110
|
+
let urls: [String]
|
|
111
|
+
let title: String
|
|
112
|
+
let size: CGFloat
|
|
113
|
+
|
|
114
|
+
private var child: CGFloat {
|
|
115
|
+
switch urls.count {
|
|
116
|
+
case 0, 1, 2: return 24
|
|
117
|
+
default: return 16
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
var body: some View {
|
|
122
|
+
switch urls.count {
|
|
123
|
+
case 0:
|
|
124
|
+
InitialsAvatar(name: title, size: size)
|
|
125
|
+
case 1:
|
|
126
|
+
avatar(urls[0]).frame(width: size, height: size)
|
|
127
|
+
case 2:
|
|
128
|
+
ZStack {
|
|
129
|
+
avatar(urls[0]).frame(width: child, height: child)
|
|
130
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
|
|
131
|
+
avatar(urls[1]).frame(width: child, height: child)
|
|
132
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomLeading)
|
|
133
|
+
}
|
|
134
|
+
.frame(width: size, height: size)
|
|
135
|
+
case 3:
|
|
136
|
+
ZStack {
|
|
137
|
+
avatar(urls[0]).frame(width: child, height: child)
|
|
138
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing)
|
|
139
|
+
avatar(urls[1]).frame(width: child, height: child)
|
|
140
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
|
141
|
+
avatar(urls[2]).frame(width: child, height: child)
|
|
142
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomLeading)
|
|
143
|
+
}
|
|
144
|
+
.frame(width: size, height: size)
|
|
145
|
+
.padding(2)
|
|
146
|
+
case 4:
|
|
147
|
+
ZStack {
|
|
148
|
+
avatar(urls[0]).frame(width: child, height: child)
|
|
149
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomLeading)
|
|
150
|
+
avatar(urls[1]).frame(width: child, height: child)
|
|
151
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing)
|
|
152
|
+
avatar(urls[2]).frame(width: child, height: child)
|
|
153
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
|
|
154
|
+
avatar(urls[3]).frame(width: child, height: child)
|
|
155
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
|
156
|
+
}
|
|
157
|
+
.frame(width: size, height: size)
|
|
158
|
+
.padding(1)
|
|
159
|
+
default:
|
|
160
|
+
ZStack {
|
|
161
|
+
avatar(urls[0]).frame(width: child, height: child)
|
|
162
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomLeading)
|
|
163
|
+
avatar(urls[1]).frame(width: child, height: child)
|
|
164
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
|
|
165
|
+
avatar(urls[2]).frame(width: child, height: child)
|
|
166
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
|
167
|
+
overflowTile(more: urls.count - 3)
|
|
168
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing)
|
|
169
|
+
}
|
|
170
|
+
.frame(width: size, height: size)
|
|
171
|
+
.padding(1)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private func avatar(_ url: String) -> some View {
|
|
176
|
+
ImageView(url)
|
|
177
|
+
.aspectRatio(contentMode: .fill)
|
|
178
|
+
.clipShape(Circle())
|
|
179
|
+
.overlay(Circle().stroke(Colors.black05, lineWidth: 0.5))
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private func overflowTile(more: Int) -> some View {
|
|
183
|
+
MomoText("+\(more)", typography: .descriptionXsRegular, color: Colors.pink03, textAlign: .center, maxLines: 1)
|
|
184
|
+
.frame(width: child, height: child)
|
|
185
|
+
.background(Colors.pink09)
|
|
186
|
+
.clipShape(Circle())
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
@available(iOS 16.0, *)
|
|
191
|
+
private struct InitialsAvatar: View {
|
|
192
|
+
let name: String
|
|
193
|
+
let size: CGFloat
|
|
194
|
+
|
|
195
|
+
private var initials: String {
|
|
196
|
+
let words = name.trimmingCharacters(in: .whitespaces)
|
|
197
|
+
.split(whereSeparator: { $0.isWhitespace })
|
|
198
|
+
.suffix(2)
|
|
199
|
+
return words.compactMap { $0.first.map { String($0).uppercased() } }.joined()
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
var body: some View {
|
|
203
|
+
MomoText(initials, typography: .descriptionXsRegular, color: Colors.pink03, textAlign: .center, maxLines: 1)
|
|
204
|
+
.frame(width: size, height: size)
|
|
205
|
+
.background(Colors.pink09)
|
|
206
|
+
.clipShape(Circle())
|
|
207
|
+
.overlay(Circle().stroke(Colors.black05, lineWidth: 0.5))
|
|
208
|
+
}
|
|
209
|
+
}
|