@momo-kits/native-kits 0.113.3-webview.2 → 0.114.1-beta.1

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.
@@ -0,0 +1,226 @@
1
+ package vn.momo.kits.components.dateTimePicker
2
+
3
+ import androidx.compose.runtime.Composable
4
+ import androidx.compose.runtime.remember
5
+ import kotlinx.datetime.Clock
6
+ import kotlinx.datetime.DatePeriod
7
+ import kotlinx.datetime.LocalDateTime
8
+ import kotlinx.datetime.TimeZone
9
+ import kotlinx.datetime.minus
10
+ import kotlinx.datetime.number
11
+ import kotlinx.datetime.plus
12
+ import kotlinx.datetime.toLocalDateTime
13
+
14
+ /**
15
+ * Format a LocalDateTime object into a string
16
+ */
17
+ fun formatLocalDateTime(dateTime: LocalDateTime, format: String = "yyyy-MM-dd HH:mm"): String {
18
+ return when (format) {
19
+ "yyyy-MM-dd HH:mm" -> "${dateTime.year}-${paddingNum(dateTime.month.number)}-${paddingNum(dateTime.dayOfMonth)} ${paddingNum(dateTime.hour)}:${paddingNum(dateTime.minute)}"
20
+ "dd-MM-yyyy" -> "${paddingNum(dateTime.dayOfMonth)}-${paddingNum(dateTime.month.number)}-${dateTime.year}"
21
+ "HH:mm" -> "${paddingNum(dateTime.hour)}:${paddingNum(dateTime.minute)}"
22
+ else -> "${dateTime.year}-${paddingNum(dateTime.month.number)}-${paddingNum(dateTime.dayOfMonth)} ${paddingNum(dateTime.hour)}:${paddingNum(dateTime.minute)}"
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Pad a number with a leading zero if it's less than 10
28
+ */
29
+ fun paddingNum(num: Int): String {
30
+ return if (num > 9) num.toString() else "0$num"
31
+ }
32
+
33
+ /**
34
+ * Get a list of days in a month
35
+ */
36
+ fun getDaysInMonth(
37
+ year: Int,
38
+ month: Int,
39
+ minDate: LocalDateTime,
40
+ maxDate: LocalDateTime
41
+ ): List<String> {
42
+ val daysInMonth = when (month) {
43
+ 1, 3, 5, 7, 8, 10, 12 -> 31
44
+ 4, 6, 9, 11 -> 30
45
+ 2 -> if (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)) 29 else 28
46
+ else -> throw IllegalArgumentException("Invalid month: $month")
47
+ }
48
+
49
+ var startDay = 1
50
+ var endDay = daysInMonth
51
+
52
+ if (year == minDate.year && month == minDate.month.number) {
53
+ startDay = minDate.dayOfMonth
54
+ }
55
+
56
+ if (year == maxDate.year && month == maxDate.month.number) {
57
+ endDay = maxDate.dayOfMonth
58
+ }
59
+
60
+ val days = mutableListOf<String>()
61
+ for (day in startDay..endDay) {
62
+ days.add(paddingNum(day))
63
+ }
64
+
65
+ return days
66
+ }
67
+
68
+ /**
69
+ * Get a list of months
70
+ */
71
+ fun getMonths(
72
+ minDate: LocalDateTime,
73
+ maxDate: LocalDateTime,
74
+ currentYear: Int
75
+ ): List<String> {
76
+ var startMonth = 1
77
+ var endMonth = 12
78
+
79
+ if (currentYear == minDate.year) {
80
+ startMonth = minDate.month.number
81
+ }
82
+
83
+ if (currentYear == maxDate.year) {
84
+ endMonth = maxDate.month.number
85
+ }
86
+
87
+ val months = mutableListOf<String>()
88
+ for (month in startMonth..endMonth) {
89
+ months.add(paddingNum(month))
90
+ }
91
+
92
+ return months
93
+ }
94
+
95
+ /**
96
+ * Get a list of years
97
+ */
98
+ fun getYears(minDate: LocalDateTime, maxDate: LocalDateTime): List<String> {
99
+ val startYear = minDate.year
100
+ val endYear = maxDate.year
101
+ val years = mutableListOf<String>()
102
+
103
+ for (year in startYear..endYear) {
104
+ years.add(year.toString())
105
+ }
106
+
107
+ return years
108
+ }
109
+
110
+ /**
111
+ * Get a list of hours
112
+ */
113
+ fun getHours(hourMode: Int): List<String> {
114
+ val hours = mutableListOf<String>()
115
+ for (i in 0 until hourMode) {
116
+ val hour = if (hourMode == 12) i + 1 else i
117
+ hours.add(paddingNum(hour))
118
+ }
119
+ return hours
120
+ }
121
+
122
+ /**
123
+ * Get a list of minutes
124
+ */
125
+ fun getMinutes(interval: Int): List<String> {
126
+ val minutes = mutableListOf<String>()
127
+ var min = 0
128
+ while (min < 60) {
129
+ minutes.add(paddingNum(min))
130
+ min += interval
131
+ }
132
+ return minutes
133
+ }
134
+
135
+ /**
136
+ * Time mode for 12-hour clock
137
+ */
138
+ val timeMode = listOf("AM", "PM")
139
+
140
+ /**
141
+ * Get today's date
142
+ */
143
+ fun getCurrentDateTime(): LocalDateTime {
144
+ return Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
145
+ }
146
+
147
+ /**
148
+ * Create a date that's a certain number of years before or after the current date
149
+ */
150
+ fun createRelativeDate(years: Int): LocalDateTime {
151
+ val currentDateTime = getCurrentDateTime()
152
+ val period = DatePeriod(years = if (years < 0) -years else years)
153
+
154
+ val adjustedDate = if (years < 0) {
155
+ currentDateTime.date.minus(period)
156
+ } else {
157
+ currentDateTime.date.plus(period)
158
+ }
159
+
160
+ return LocalDateTime(
161
+ adjustedDate.year,
162
+ adjustedDate.monthNumber,
163
+ adjustedDate.dayOfMonth,
164
+ currentDateTime.hour,
165
+ currentDateTime.minute
166
+ )
167
+ }
168
+
169
+ @Composable
170
+ internal fun getDateComponents(
171
+ format: String,
172
+ currentDate: PickerData,
173
+ effectiveMinDate: LocalDateTime,
174
+ effectiveMaxDate: LocalDateTime,
175
+ minuteInterval: Int,
176
+ ): List<DateComponent> {
177
+ val formatParts = remember(format) {
178
+ format.split(Regex("[^A-Za-z]+"))
179
+ }
180
+
181
+ val isOnlyHour = remember(formatParts) {
182
+ formatParts.size == 1 && formatParts[0] == "HH"
183
+ }
184
+
185
+ val dayData = remember(currentDate.year, currentDate.month, effectiveMinDate, effectiveMaxDate) {
186
+ getDaysInMonth(currentDate.year, currentDate.month, effectiveMinDate, effectiveMaxDate)
187
+ }
188
+
189
+ val monthData = remember(currentDate.year, effectiveMinDate, effectiveMaxDate) {
190
+ getMonths(effectiveMinDate, effectiveMaxDate, currentDate.year)
191
+ }
192
+
193
+ val yearData = remember(effectiveMinDate, effectiveMaxDate) {
194
+ getYears(effectiveMinDate, effectiveMaxDate)
195
+ }
196
+
197
+ val hourData = remember(isOnlyHour) {
198
+ getHours(if (isOnlyHour) 12 else 24)
199
+ }
200
+
201
+ val minData = remember(minuteInterval) {
202
+ getMinutes(minuteInterval)
203
+ }
204
+
205
+ val dateComponents = remember(formatParts, dayData, monthData, yearData, hourData, minData, isOnlyHour) {
206
+ val components = mutableListOf<DateComponent>()
207
+
208
+ formatParts.forEach { part ->
209
+ when (part) {
210
+ "DD" -> components.add(DateComponent("day", dayData))
211
+ "MM" -> components.add(DateComponent("month", monthData))
212
+ "YYYY" -> components.add(DateComponent("year", yearData))
213
+ "HH" -> components.add(DateComponent("hour", hourData))
214
+ "mm" -> components.add(DateComponent("minute", minData))
215
+ }
216
+ }
217
+
218
+ if (isOnlyHour) {
219
+ components.add(DateComponent("timeMode", timeMode))
220
+ }
221
+
222
+ components
223
+ }
224
+
225
+ return dateComponents
226
+ }
@@ -0,0 +1,201 @@
1
+ package vn.momo.kits.components.dateTimePicker
2
+
3
+ import androidx.compose.animation.core.tween
4
+ import androidx.compose.foundation.background
5
+ import androidx.compose.foundation.border
6
+ import androidx.compose.foundation.gestures.Orientation
7
+ import androidx.compose.foundation.gestures.animateScrollBy
8
+ import androidx.compose.foundation.gestures.draggable
9
+ import androidx.compose.foundation.gestures.rememberDraggableState
10
+ import androidx.compose.foundation.gestures.scrollBy
11
+ import androidx.compose.foundation.layout.Box
12
+ import androidx.compose.foundation.layout.fillMaxWidth
13
+ import androidx.compose.foundation.layout.height
14
+ import androidx.compose.foundation.lazy.LazyColumn
15
+ import androidx.compose.foundation.lazy.rememberLazyListState
16
+ import androidx.compose.foundation.shape.RoundedCornerShape
17
+ import androidx.compose.runtime.Composable
18
+ import androidx.compose.runtime.LaunchedEffect
19
+ import androidx.compose.runtime.derivedStateOf
20
+ import androidx.compose.runtime.getValue
21
+ import androidx.compose.runtime.remember
22
+ import androidx.compose.runtime.rememberCoroutineScope
23
+ import androidx.compose.ui.Alignment
24
+ import androidx.compose.ui.Modifier
25
+ import androidx.compose.ui.draw.alpha
26
+ import androidx.compose.ui.draw.scale
27
+ import androidx.compose.ui.platform.LocalDensity
28
+ import androidx.compose.ui.unit.Dp
29
+ import androidx.compose.ui.unit.dp
30
+ import androidx.compose.ui.zIndex
31
+ import kotlinx.coroutines.CoroutineScope
32
+ import kotlinx.coroutines.launch
33
+ import vn.momo.kits.components.Text
34
+ import vn.momo.kits.const.AppTheme
35
+ import vn.momo.kits.const.Colors
36
+ import vn.momo.kits.const.Radius
37
+ import vn.momo.kits.const.Typography
38
+ import kotlin.math.abs
39
+ import kotlin.math.roundToInt
40
+
41
+ private val wheelItemHeight = 42.dp
42
+ private val wheelPickerBorderWidth = 1.dp
43
+
44
+ private fun onDragStopped(
45
+ listState: androidx.compose.foundation.lazy.LazyListState,
46
+ itemHeight: Float,
47
+ coroutineScope: CoroutineScope
48
+ ) {
49
+
50
+ coroutineScope.launch {
51
+ val targetPosition = listState.firstVisibleItemScrollOffset / itemHeight
52
+ val targetOffset = if (targetPosition.roundToInt() == targetPosition.toInt()) {
53
+ 0f
54
+ } else {
55
+ if (targetPosition - targetPosition.toInt() > 0.5f) {
56
+ (1 - (targetPosition - targetPosition.toInt())) * itemHeight
57
+ } else {
58
+ -(targetPosition - targetPosition.toInt()) * itemHeight
59
+ }
60
+ }
61
+
62
+ listState.animateScrollBy(targetOffset, tween(150))
63
+ }
64
+ }
65
+
66
+ @Composable
67
+ fun WheelPicker(
68
+ name: String,
69
+ data: List<String>,
70
+ selectedData: String,
71
+ onChange: (String, String) -> Unit,
72
+ modifier: Modifier = Modifier
73
+ ) {
74
+ val coroutineScope = rememberCoroutineScope()
75
+ val listState = rememberLazyListState()
76
+ val itemHeightPx = with(LocalDensity.current) { wheelItemHeight.toPx() }
77
+
78
+ val paddedData = remember(data) {
79
+ listOf("", "") + data + listOf("", "")
80
+ }
81
+
82
+ val initialIndex = remember(selectedData, paddedData) {
83
+ paddedData.indexOf(selectedData).coerceAtLeast(2)
84
+ }
85
+
86
+ LaunchedEffect(initialIndex) {
87
+ listState.scrollToItem(initialIndex - 2)
88
+ }
89
+
90
+ val selectedIndex by remember {
91
+ derivedStateOf {
92
+ (listState.firstVisibleItemIndex + 2).coerceIn(2, paddedData.size - 3)
93
+ }
94
+ }
95
+
96
+ LaunchedEffect(listState.isScrollInProgress) {
97
+ if (!listState.isScrollInProgress) {
98
+ val centeredIndex = selectedIndex
99
+ if (centeredIndex in 2 until paddedData.size - 2) {
100
+ onChange(name, paddedData[centeredIndex])
101
+ }
102
+ }
103
+ }
104
+
105
+ Box(
106
+ modifier = modifier
107
+ .border(
108
+ width = wheelPickerBorderWidth,
109
+ color = AppTheme.current.colors.border.default,
110
+ shape = RoundedCornerShape(Radius.S)
111
+ )
112
+ .background(
113
+ color = AppTheme.current.colors.background.surface,
114
+ )
115
+ ) {
116
+ LazyColumn(
117
+ state = listState,
118
+ modifier = Modifier
119
+ .fillMaxWidth()
120
+ .draggable(
121
+ orientation = Orientation.Vertical,
122
+ state = rememberDraggableState { delta ->
123
+ coroutineScope.launch {
124
+ listState.scrollBy(-delta)
125
+ }
126
+ },
127
+ onDragStopped = {
128
+ onDragStopped(
129
+ listState = listState,
130
+ itemHeight = itemHeightPx,
131
+ coroutineScope = coroutineScope
132
+ )
133
+ }
134
+ )
135
+ ) {
136
+ items(paddedData.size) { index ->
137
+ WheelPickerItem(
138
+ text = paddedData[index],
139
+ isSelected = index == selectedIndex,
140
+ index = index,
141
+ listState = listState,
142
+ itemHeight = wheelItemHeight
143
+ )
144
+ }
145
+ }
146
+
147
+ Box(
148
+ modifier = Modifier
149
+ .fillMaxWidth()
150
+ .height(wheelItemHeight)
151
+ .background(Colors.blue_10)
152
+ .align(Alignment.Center)
153
+ .alpha(0.2f)
154
+ .zIndex(-2f)
155
+ )
156
+ }
157
+ }
158
+
159
+ @Composable
160
+ fun WheelPickerItem(
161
+ text: String,
162
+ isSelected: Boolean,
163
+ index: Int,
164
+ listState: androidx.compose.foundation.lazy.LazyListState,
165
+ itemHeight: Dp
166
+ ) {
167
+ val firstVisibleIndex = listState.firstVisibleItemIndex
168
+ val visibleItemOffset = listState.firstVisibleItemScrollOffset
169
+
170
+ val viewportHeight = 5 * itemHeight.value
171
+ val itemHeightValue = itemHeight.value
172
+
173
+ val absoluteOffset =
174
+ (index - firstVisibleIndex) * itemHeightValue - (visibleItemOffset / itemHeightValue)
175
+ val relativePosition = absoluteOffset / viewportHeight
176
+
177
+ val distance = abs(2 - relativePosition)
178
+
179
+ val opacity = when {
180
+ distance <= 1 -> 1f
181
+ distance <= 2 -> 0.8f
182
+ distance <= 3 -> 0.4f
183
+ else -> 0.4f
184
+ }
185
+
186
+ val scale = if (isSelected) 1f else 0.87f
187
+
188
+ Box(
189
+ modifier = Modifier
190
+ .fillMaxWidth()
191
+ .height(itemHeight)
192
+ .alpha(opacity),
193
+ contentAlignment = Alignment.Center
194
+ ) {
195
+ Text(
196
+ text = text,
197
+ style = Typography.actionS,
198
+ modifier = Modifier.scale(scale)
199
+ )
200
+ }
201
+ }
@@ -13,12 +13,12 @@ public class MiniAppContext {
13
13
  public var appName: Any? = nil
14
14
  public var appIcon: String = ""
15
15
  public var description: Any? = nil
16
- public var support: Any? = nil
17
- public var toolkitConfig: Any? = nil
18
- public var providerId: String = ""
19
- public var permissions: Any? = nil
20
-
21
- public init(appId: String, appCode: String, appName: Any? = nil, appIcon: String, description: Any? = nil, support: Any? = nil, toolkitConfig: Any? = nil, providerId: String, permissions: 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
22
  self.appId = appId
23
23
  self.appCode = appCode
24
24
  self.appName = appName
@@ -31,12 +31,20 @@ public class MiniAppContext {
31
31
  }
32
32
  }
33
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
+
34
40
  public class ApplicationEnvironment: ObservableObject {
35
41
  let applicationContext: MiniAppContext?
36
- let composeApi: KitComposeApi
37
-
38
- public init(applicationContext: MiniAppContext?, composeApi: KitComposeApi) {
42
+ let composeApi: KitComposeApi?
43
+ let config: KitConfig?
44
+
45
+ public init(applicationContext: MiniAppContext? = nil, composeApi: KitComposeApi? = nil, config: KitConfig? = nil) {
39
46
  self.applicationContext = applicationContext
40
47
  self.composeApi = composeApi
48
+ self.config = config
41
49
  }
42
50
  }
@@ -6,7 +6,7 @@ public struct HeaderRightData {
6
6
  var useMore: Bool
7
7
  var tools: [ToolGroup]
8
8
  var toolCallback: ((String) -> Void)?
9
-
9
+
10
10
  public init(
11
11
  useShortcut: Bool = false,
12
12
  useMore: Bool = false,
@@ -25,7 +25,7 @@ public struct Tool {
25
25
  var icon: String
26
26
  var showBadge: Bool
27
27
  var name: [String: String]
28
-
28
+
29
29
  public init(
30
30
  key: String,
31
31
  icon: String,
@@ -37,7 +37,7 @@ public struct Tool {
37
37
  self.showBadge = showBadge
38
38
  self.name = name
39
39
  }
40
-
40
+
41
41
  func toMap() -> [String: Any] {
42
42
  return [
43
43
  "key": key,
@@ -51,7 +51,7 @@ public struct Tool {
51
51
  public struct ToolGroup {
52
52
  var title: [String: String]
53
53
  var items: [Tool]
54
-
54
+
55
55
  public init(
56
56
  title: [String: String] = [:],
57
57
  items: [Tool]
@@ -59,7 +59,7 @@ public struct ToolGroup {
59
59
  self.title = title
60
60
  self.items = items
61
61
  }
62
-
62
+
63
63
  func toMap() -> [String: Any] {
64
64
  return [
65
65
  "title": title,
@@ -79,7 +79,7 @@ public struct HeaderRight: View {
79
79
  self.headerRight = headerRight
80
80
  }
81
81
  var headerRight: HeaderRightData?
82
-
82
+
83
83
  public var body: some View {
84
84
  HStack(alignment: .center) {
85
85
  ToolkitHeaderRight(headerRight: headerRight)
@@ -92,17 +92,17 @@ struct ToolkitHeaderRight: View {
92
92
  @State private var isFavorite: Bool = false
93
93
  @State private var isLoading: Bool = false
94
94
  @EnvironmentObject private var environment: ApplicationEnvironment
95
-
95
+
96
96
  // MARK: - Actions
97
97
  private func onPressShortcut() {
98
- environment.composeApi.request(
98
+ environment.composeApi?.request(
99
99
  funcName: "onToolAction",
100
100
  params: ["item": ["key": "onFavorite"]]
101
101
  ) { _ in
102
102
  isFavorite.toggle()
103
103
  }
104
104
  }
105
-
105
+
106
106
  private func onPressHelpCenter() {
107
107
  let context = environment.applicationContext
108
108
  let paramMap: [String: Any] = [
@@ -112,20 +112,19 @@ struct ToolkitHeaderRight: View {
112
112
  "icon": context?.appIcon,
113
113
  "description": context?.description
114
114
  ]
115
- environment.composeApi.request(
115
+ environment.composeApi?.request(
116
116
  funcName: "showHelpCenter",
117
117
  params: paramMap
118
118
  ) { _ in }
119
119
  }
120
-
120
+
121
121
  private func onPressClose() {
122
- environment.composeApi.request(
122
+ environment.composeApi?.request(
123
123
  funcName: "dismiss",
124
124
  params: ""
125
125
  ) { _ in }
126
126
  }
127
-
128
-
127
+
129
128
  private func onPressMore() {
130
129
  let context = environment.applicationContext
131
130
  let params: [String: Any] = [
@@ -136,13 +135,13 @@ struct ToolkitHeaderRight: View {
136
135
  "name": context?.appName,
137
136
  "icon": context?.appIcon,
138
137
  "description": context?.description,
139
- "support": context?.support,
140
- "toolkitConfig": context?.toolkitConfig,
141
- "providerId": context?.providerId,
142
- "permissions": context?.permissions
138
+ "support": context?.support,
139
+ "toolkitConfig": context?.toolkitConfig,
140
+ "providerId": context?.providerId,
141
+ "permissions": context?.permissions
143
142
  ]
144
143
  ]
145
- environment.composeApi.request(
144
+ environment.composeApi?.request(
146
145
  funcName: "showTools",
147
146
  params: params
148
147
  ) { response in
@@ -157,12 +156,12 @@ struct ToolkitHeaderRight: View {
157
156
  }
158
157
  }
159
158
  }
160
-
159
+
161
160
  private func getNavigationButtonConfig() -> NavigationButtonConfig {
162
161
  let totalTools = headerRight?.tools.reduce(0) { $0 + $1.items.count } ?? 0
163
162
  var icon = isFavorite ? "pin_star_checked" : "pin_star"
164
163
  var onClickHandler: () -> Void = onPressShortcut
165
-
164
+
166
165
  if totalTools > 1 || headerRight?.useMore == true {
167
166
  icon = "navigation_more_icon"
168
167
  onClickHandler = onPressMore
@@ -172,16 +171,16 @@ struct ToolkitHeaderRight: View {
172
171
  headerRight?.toolCallback?(singleTool.key)
173
172
  }
174
173
  }
175
-
174
+
176
175
  return NavigationButtonConfig(icon: icon, onPress: onClickHandler)
177
176
  }
178
-
177
+
179
178
  var body: some View {
180
179
  let navButtonConfig = getNavigationButtonConfig()
181
180
  let showBadge = headerRight?.tools.contains { group in
182
181
  group.items.contains { $0.showBadge }
183
182
  } ?? false
184
-
183
+
185
184
  HStack(alignment: .center) {
186
185
  if headerRight?.useShortcut == true {
187
186
  NavigationButton(
@@ -191,18 +190,18 @@ struct ToolkitHeaderRight: View {
191
190
  onClick: navButtonConfig.onPress
192
191
  )
193
192
  }
194
-
193
+
195
194
  HStack(alignment: .center, spacing: 0) {
196
195
  Icon(source: "help_center", size: 20)
197
196
  .padding(4)
198
197
  .onTapGesture {
199
198
  onPressHelpCenter()
200
199
  }
201
-
200
+
202
201
  Rectangle()
203
202
  .fill(Colors.black20)
204
203
  .frame(width: 0.5, height: 12)
205
-
204
+
206
205
  Icon(source: "16_navigation_close_circle", size: 20, color: Colors.black17)
207
206
  .padding(4)
208
207
  .onTapGesture {
@@ -226,7 +225,7 @@ struct NavigationButton: View {
226
225
  var icon: String
227
226
  var showBadge: Bool
228
227
  var onClick: () -> Void
229
-
228
+
230
229
  var body: some View {
231
230
  ZStack {
232
231
  Circle()
@@ -235,9 +234,9 @@ struct NavigationButton: View {
235
234
  Circle()
236
235
  .stroke(Color.black.opacity(0.2), lineWidth: 0.2)
237
236
  )
238
-
237
+
239
238
  Icon(source: icon, size: 16)
240
-
239
+
241
240
  if showBadge {
242
241
  BadgeDot(size: .small)
243
242
  .offset(x: -2, y: -2)