@momo-kits/native-kits 0.162.21-debug → 0.162.22-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/compose/build.gradle.kts +1 -1
- package/compose/compose.podspec +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +3 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +26 -7
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +3 -1
- package/gradle.properties +1 -1
- package/ios/Application/ApplicationEnvironment.swift +4 -2
- package/ios/Application/MaxApi.swift +18 -0
- package/ios/Application/Navigation/NavigationContainer.swift +21 -3
- package/ios/Application/Navigation/Navigator.swift +50 -29
- package/ios/Application/Navigation/Overplay/ModalScreen.swift +6 -0
- package/ios/Application/Navigation/Tracking/ScreenTracker.swift +117 -0
- package/ios/Application/StackScreen.swift +41 -0
- package/ios/Badge/BadgeRibbon.swift +80 -0
- package/ios/Button/Button.swift +67 -11
- package/ios/Carousel/Carousel.swift +16 -1
- package/ios/Checkbox/Checkbox.swift +14 -20
- package/ios/Chip/Chip.swift +35 -26
- package/ios/Collapse/Collapse.swift +11 -1
- package/ios/DateTimePicker/DateTimePicker.swift +4 -2
- package/ios/DateTimePicker/WheelPicker.swift +7 -3
- package/ios/Extensions/ActiveOpacityButtonStyle.swift +14 -0
- package/ios/Icon/Icon.swift +5 -1
- package/ios/IconButton/IconButton.swift +35 -7
- package/ios/Image/Image.swift +16 -3
- package/ios/Information/Information.swift +3 -1
- package/ios/Input/ErrorView.swift +3 -1
- package/ios/Input/Input.swift +31 -8
- package/ios/Input/InputPhoneNumber.swift +14 -3
- package/ios/Input/InputSearch.swift +7 -1
- package/ios/Input/InputTextArea.swift +78 -34
- package/ios/InputDropDown/InputDropDown.swift +4 -1
- package/ios/InputMoney/InputMoney.swift +16 -4
- package/ios/Popup/PopupDisplay.swift +3 -4
- package/ios/Popup/PopupNotify.swift +3 -4
- package/ios/Popup/PopupPromotion.swift +16 -8
- package/ios/ProgressInfo/ProgressInfo.swift +4 -2
- package/ios/Radio/Radio.swift +12 -13
- package/ios/Rating/Rating.swift +1 -1
- package/ios/Stepper/Stepper.swift +2 -2
- package/ios/Steps/Steps.swift +22 -6
- package/ios/SuggestAction/SuggestAction.swift +5 -3
- package/ios/Swipeable/SwipeCell.swift +5 -3
- package/ios/Switch/Switch.swift +53 -28
- package/ios/TabView/TabView.swift +18 -14
- package/ios/Template/TrustBanner/TrustBanner.swift +6 -7
- package/ios/Tooltip/Tooltip.swift +3 -0
- package/ios/Uploader/Uploader.swift +0 -9
- package/ios/native-kits.podspec +1 -1
- package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
- package/package.json +1 -1
|
@@ -196,6 +196,86 @@ public struct BadgeRibbon: View {
|
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
+
/// Ported from Compose's `RoundedBadgeRibbon` (BadgeRibbon.kt:164-326): a canvas-drawn
|
|
200
|
+
/// ribbon shape sized to its text, mirrored per corner via scaleX/scaleY (never an actual
|
|
201
|
+
/// rotation) — the label gets the same scale applied a second time so it stays right-reading
|
|
202
|
+
/// while only the ribbon shape flips.
|
|
203
|
+
public struct RoundedBadgeRibbon: View {
|
|
204
|
+
let text: String
|
|
205
|
+
let position: RibbonPosition
|
|
206
|
+
|
|
207
|
+
public init(text: String = "Label", position: RibbonPosition = .topLeft) {
|
|
208
|
+
self.text = text
|
|
209
|
+
self.position = position
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private var scale: (x: CGFloat, y: CGFloat) {
|
|
213
|
+
switch position {
|
|
214
|
+
case .topLeft: return (-1, 1)
|
|
215
|
+
case .topRight: return (1, 1)
|
|
216
|
+
case .bottomRight: return (1, -1)
|
|
217
|
+
case .bottomLeft: return (-1, -1)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private var measuredTextSize: CGSize {
|
|
222
|
+
let fontSize = scaleSize(TypographyStyle.labelXsMedium.fontSize)
|
|
223
|
+
let font = UIFont.systemFont(ofSize: fontSize, weight: .medium)
|
|
224
|
+
return text.size(withAttributes: [.font: font])
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
public var body: some View {
|
|
228
|
+
let textSize = measuredTextSize
|
|
229
|
+
let minRibbonHeight = max(textSize.height * 1.2, 16)
|
|
230
|
+
let padding = minRibbonHeight / 4
|
|
231
|
+
let badgeHeight = minRibbonHeight + padding
|
|
232
|
+
let badgeWidth = max(textSize.width + padding * 2, 28)
|
|
233
|
+
|
|
234
|
+
ZStack {
|
|
235
|
+
Canvas { context, size in
|
|
236
|
+
let ribbonHeight = size.height - padding
|
|
237
|
+
let cornerRadius = (badgeHeight - padding) / 2
|
|
238
|
+
let mainColor = Colors.orange03
|
|
239
|
+
let tailColor = Colors.red01
|
|
240
|
+
|
|
241
|
+
let headWidth: CGFloat = 4
|
|
242
|
+
// Kotlin computes `leftSectionWidth` + `middleSectionWidth` here, but those
|
|
243
|
+
// cancel algebraically to `width - headWidth` — kept as the direct result.
|
|
244
|
+
let rightX = size.width - headWidth
|
|
245
|
+
|
|
246
|
+
let leftMiddlePath = RoundedCorners(topLeft: cornerRadius, topRight: 0, bottomLeft: cornerRadius, bottomRight: 0)
|
|
247
|
+
.path(in: CGRect(x: 0, y: 0, width: rightX + 1, height: ribbonHeight))
|
|
248
|
+
context.fill(leftMiddlePath, with: .color(mainColor))
|
|
249
|
+
|
|
250
|
+
let headPath = RoundedCorners(topLeft: 0, topRight: cornerRadius, bottomLeft: 0, bottomRight: 0)
|
|
251
|
+
.path(in: CGRect(x: rightX, y: 0, width: headWidth, height: ribbonHeight))
|
|
252
|
+
context.fill(headPath, with: .color(mainColor))
|
|
253
|
+
|
|
254
|
+
let halfHeadWidth = headWidth / 2
|
|
255
|
+
let tailStartX = rightX + headWidth - halfHeadWidth
|
|
256
|
+
let tailY = ribbonHeight
|
|
257
|
+
let tailHeight = padding
|
|
258
|
+
|
|
259
|
+
context.fill(
|
|
260
|
+
Path(CGRect(x: tailStartX, y: tailY, width: halfHeadWidth, height: halfHeadWidth)),
|
|
261
|
+
with: .color(mainColor)
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
let tailRightPath = RoundedCorners(topLeft: 0, topRight: tailHeight, bottomLeft: 0, bottomRight: tailHeight)
|
|
265
|
+
.path(in: CGRect(x: tailStartX, y: tailY, width: halfHeadWidth, height: tailHeight))
|
|
266
|
+
context.fill(tailRightPath, with: .color(tailColor))
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
MomoText(text, typography: .labelXsMedium, color: Colors.black01, maxLines: 1)
|
|
270
|
+
.padding(.bottom, padding)
|
|
271
|
+
.padding(.horizontal, padding / 2)
|
|
272
|
+
.scaleEffect(x: scale.x, y: scale.y)
|
|
273
|
+
}
|
|
274
|
+
.frame(width: badgeWidth, height: badgeHeight)
|
|
275
|
+
.scaleEffect(x: scale.x, y: scale.y)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
199
279
|
// MARK: - Constants
|
|
200
280
|
private struct RibbonConstants {
|
|
201
281
|
static let ribbonHeight: CGFloat = 20
|
package/ios/Button/Button.swift
CHANGED
|
@@ -123,7 +123,6 @@ extension ButtonSize {
|
|
|
123
123
|
case .large: return .actionDefaultBold
|
|
124
124
|
case .medium: return .actionSBold
|
|
125
125
|
case .small: return .actionXsBold
|
|
126
|
-
default : return .actionDefaultBold
|
|
127
126
|
}
|
|
128
127
|
}
|
|
129
128
|
}
|
|
@@ -162,6 +161,35 @@ public struct Button: View {
|
|
|
162
161
|
self.useTintColor = useTintColor
|
|
163
162
|
}
|
|
164
163
|
|
|
164
|
+
/// Mirrors Kotlin's `Button(iconLeft: String, iconRight: String, ...)`, which resolves
|
|
165
|
+
/// icons by design-system source name through the shared `Icon` component rather than
|
|
166
|
+
/// a caller-supplied view. `iconLeft` is required here (unlike Kotlin's `= ""` default)
|
|
167
|
+
/// solely to keep this initializer unambiguous alongside the `AnyView`-based one above —
|
|
168
|
+
/// pass `""` explicitly for an icon-right-only button.
|
|
169
|
+
public init(
|
|
170
|
+
title: String = "Button",
|
|
171
|
+
action: @escaping () -> Void,
|
|
172
|
+
type: ButtonType = .primary,
|
|
173
|
+
size: ButtonSize = .large,
|
|
174
|
+
iconLeft: String,
|
|
175
|
+
iconRight: String = "",
|
|
176
|
+
isFull: Bool = true,
|
|
177
|
+
loading: Bool = false,
|
|
178
|
+
useTintColor: Bool = true
|
|
179
|
+
) {
|
|
180
|
+
self.init(
|
|
181
|
+
title: title,
|
|
182
|
+
action: action,
|
|
183
|
+
type: type,
|
|
184
|
+
size: size,
|
|
185
|
+
iconLeft: iconLeft.isEmpty ? nil : AnyView(Icon(source: iconLeft)),
|
|
186
|
+
iconRight: iconRight.isEmpty ? nil : AnyView(Icon(source: iconRight)),
|
|
187
|
+
isFull: isFull,
|
|
188
|
+
loading: loading,
|
|
189
|
+
useTintColor: useTintColor
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
|
|
165
193
|
private func shouldLoadingOnLeft(_ left: AnyView?, _ right: AnyView?) -> Bool {
|
|
166
194
|
let hasLeft = left != nil
|
|
167
195
|
let hasRight = right != nil
|
|
@@ -214,14 +242,15 @@ public struct Button: View {
|
|
|
214
242
|
.frame(maxWidth: isFull ? .infinity : nil)
|
|
215
243
|
.padding(.horizontal, size.padding)
|
|
216
244
|
.frame(height: size.height)
|
|
217
|
-
.background(bg)
|
|
218
|
-
.overlay(
|
|
219
|
-
RoundedRectangle(cornerRadius: size.radius)
|
|
220
|
-
.strokeBorder(border ?? .clear, lineWidth: border != nil ? borderWidth : 0)
|
|
221
|
-
)
|
|
222
|
-
.clipShape(RoundedRectangle(cornerRadius: size.radius))
|
|
223
245
|
}
|
|
224
|
-
.buttonStyle(ButtonPressFeedbackStyle(
|
|
246
|
+
.buttonStyle(ButtonPressFeedbackStyle(
|
|
247
|
+
isEnabled: isEnabled,
|
|
248
|
+
background: bg,
|
|
249
|
+
border: border,
|
|
250
|
+
borderWidth: borderWidth,
|
|
251
|
+
radius: size.radius,
|
|
252
|
+
height: size.height
|
|
253
|
+
))
|
|
225
254
|
.disabled(!isEnabled)
|
|
226
255
|
}
|
|
227
256
|
|
|
@@ -238,12 +267,39 @@ public struct Button: View {
|
|
|
238
267
|
}
|
|
239
268
|
}
|
|
240
269
|
|
|
270
|
+
/// Mirrors Compose's press feedback: the background shrinks via an animated
|
|
271
|
+
/// horizontal inset while the content fades, rather than a plain opacity dim on
|
|
272
|
+
/// the whole button (matching `animatedPadding`/`alpha` in Button.kt).
|
|
241
273
|
private struct ButtonPressFeedbackStyle: ButtonStyle {
|
|
242
274
|
let isEnabled: Bool
|
|
275
|
+
let background: Color
|
|
276
|
+
let border: Color?
|
|
277
|
+
let borderWidth: CGFloat
|
|
278
|
+
let radius: CGFloat
|
|
279
|
+
let height: CGFloat
|
|
243
280
|
|
|
244
281
|
func makeBody(configuration: Configuration) -> some View {
|
|
245
|
-
configuration.
|
|
246
|
-
|
|
247
|
-
|
|
282
|
+
let pressed = configuration.isPressed && isEnabled
|
|
283
|
+
|
|
284
|
+
ZStack {
|
|
285
|
+
RoundedRectangle(cornerRadius: radius)
|
|
286
|
+
.fill(background)
|
|
287
|
+
.overlay(
|
|
288
|
+
RoundedRectangle(cornerRadius: radius)
|
|
289
|
+
.strokeBorder(border ?? .clear, lineWidth: border != nil ? borderWidth : 0)
|
|
290
|
+
)
|
|
291
|
+
.padding(.horizontal, pressed ? 2 : 0)
|
|
292
|
+
|
|
293
|
+
configuration.label
|
|
294
|
+
.opacity(pressed ? 0.5 : 1)
|
|
295
|
+
}
|
|
296
|
+
// Pin the whole button to its intended height explicitly: the bare
|
|
297
|
+
// RoundedRectangle background has no intrinsic size of its own, so left
|
|
298
|
+
// unconstrained in a ZStack inside a non-height-constraining container
|
|
299
|
+
// (e.g. a footer VStack) it can report an oversized ideal height and
|
|
300
|
+
// stretch the button — and its container — far taller than intended.
|
|
301
|
+
.frame(height: height)
|
|
302
|
+
.clipShape(RoundedRectangle(cornerRadius: radius))
|
|
303
|
+
.animation(.easeInOut(duration: 0.1), value: pressed)
|
|
248
304
|
}
|
|
249
305
|
}
|
|
@@ -94,12 +94,27 @@ public struct Carousel<Content: View>: View {
|
|
|
94
94
|
// Offset so the current page is centered within the viewport padding.
|
|
95
95
|
let baseOffset = -CGFloat(currentPage) * step + contentPadding
|
|
96
96
|
|
|
97
|
+
// Windows the rendered range around `currentPage` instead of instantiating all
|
|
98
|
+
// `virtualPageCount` slides up front — with `loop=true`, that's `itemCount * 1000`
|
|
99
|
+
// eager views, a real memory/perf regression versus Compose's lazy `HorizontalPager`.
|
|
100
|
+
// Clear spacers of the skipped width keep the HStack's total layout size (and thus
|
|
101
|
+
// the existing `baseOffset` math) unchanged.
|
|
102
|
+
let windowRadius = 3
|
|
103
|
+
let windowStart = max(0, currentPage - windowRadius)
|
|
104
|
+
let windowEnd = min(virtualPageCount - 1, currentPage + windowRadius)
|
|
105
|
+
|
|
97
106
|
ZStack {
|
|
98
107
|
HStack(spacing: 0) {
|
|
99
|
-
|
|
108
|
+
if windowStart > 0 {
|
|
109
|
+
Color.clear.frame(width: CGFloat(windowStart) * step)
|
|
110
|
+
}
|
|
111
|
+
ForEach(windowStart...windowEnd, id: \.self) { page in
|
|
100
112
|
slide(page: page, step: step, viewportWidth: viewportWidth)
|
|
101
113
|
.frame(width: pageWidth)
|
|
102
114
|
}
|
|
115
|
+
if windowEnd < virtualPageCount - 1 {
|
|
116
|
+
Color.clear.frame(width: CGFloat(virtualPageCount - 1 - windowEnd) * step)
|
|
117
|
+
}
|
|
103
118
|
}
|
|
104
119
|
.offset(x: baseOffset + dragOffset)
|
|
105
120
|
.animation(isDragging ? nil : .easeInOut(duration: 0.25), value: currentPage)
|
|
@@ -28,29 +28,23 @@ public struct Checkbox: View{
|
|
|
28
28
|
public var body: some View {
|
|
29
29
|
HStack(spacing: Spacing.S) {
|
|
30
30
|
// Checkbox
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
SwiftUI.Button(action: onCheck) {
|
|
32
|
+
ZStack {
|
|
33
|
+
RoundedRectangle(cornerRadius: Radius.XS)
|
|
34
|
+
.fill(backgroundColor)
|
|
35
|
+
.frame(width: 20, height: 20)
|
|
36
|
+
.overlay(
|
|
37
|
+
RoundedRectangle(cornerRadius: Radius.XS)
|
|
38
|
+
.stroke(borderColor, lineWidth: Spacing.XXS)
|
|
39
|
+
)
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
.scaledToFit()
|
|
44
|
-
.foregroundColor(Colors.black01)
|
|
45
|
-
.frame(width: 12, height: 12)
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
.onTapGesture {
|
|
50
|
-
if !disabled {
|
|
51
|
-
onCheck()
|
|
41
|
+
if checked {
|
|
42
|
+
Icon(source: indeterminate ? "navigation_minus" : "ic_checked", size: 20, color: Colors.black01)
|
|
43
|
+
}
|
|
52
44
|
}
|
|
53
45
|
}
|
|
46
|
+
.buttonStyle(ActiveOpacityButtonStyle(activeOpacity: 0.8))
|
|
47
|
+
.disabled(disabled)
|
|
54
48
|
|
|
55
49
|
// Title
|
|
56
50
|
if !title.isEmpty {
|
package/ios/Chip/Chip.swift
CHANGED
|
@@ -52,6 +52,13 @@ public struct Chip: View {
|
|
|
52
52
|
|
|
53
53
|
public var body: some View {
|
|
54
54
|
let dims = (size == .small) ? Self.Small : Self.Large
|
|
55
|
+
// Mirrors Compose's `scaleSize(...)` on every chip dimension so it responds
|
|
56
|
+
// to Dynamic Type scaling the same way the rest of the kit does.
|
|
57
|
+
let height = scaleSize(dims.height)
|
|
58
|
+
let horizontal = scaleSize(dims.horizontal)
|
|
59
|
+
let iconSize = scaleSize(dims.iconSize)
|
|
60
|
+
let iconSpacing = scaleSize(dims.iconSpacing)
|
|
61
|
+
let radius = scaleSize(Radius.L)
|
|
55
62
|
|
|
56
63
|
let bg: Color = {
|
|
57
64
|
if selected { return theme.colors.background.selected }
|
|
@@ -63,36 +70,38 @@ public struct Chip: View {
|
|
|
63
70
|
let leftTint: Color = selected ? theme.colors.primary : iconLeftTint ?? theme.colors.text.default
|
|
64
71
|
let rightTint: Color = selected ? theme.colors.primary : iconRightTint ?? theme.colors.text.default
|
|
65
72
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
73
|
+
SwiftUI.Button(action: onClick) {
|
|
74
|
+
HStack(spacing: iconSpacing) {
|
|
75
|
+
if let iconLeft {
|
|
76
|
+
Icon(source: iconLeft, size: iconSize, color: leftTint)
|
|
77
|
+
}
|
|
70
78
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
79
|
+
if let text = label, !text.isEmpty {
|
|
80
|
+
MomoText(text, typography: (size == .large) ? .labelDefaultMedium : .labelSMedium, color: textColor)
|
|
81
|
+
.lineLimit(1)
|
|
82
|
+
}
|
|
75
83
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
.padding(.horizontal, dims.horizontal)
|
|
81
|
-
.frame(height: dims.height)
|
|
82
|
-
.background(
|
|
83
|
-
RoundedRectangle(cornerRadius: Radius.L, style: .continuous)
|
|
84
|
-
.fill(bg)
|
|
85
|
-
)
|
|
86
|
-
.overlay(
|
|
87
|
-
Group {
|
|
88
|
-
if selected {
|
|
89
|
-
RoundedRectangle(cornerRadius: Radius.L, style: .continuous)
|
|
90
|
-
.stroke(theme.colors.secondary, lineWidth: 2)
|
|
84
|
+
if let iconRight {
|
|
85
|
+
Icon(source: iconRight, size: iconSize, color: rightTint)
|
|
91
86
|
}
|
|
92
87
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
88
|
+
.padding(.horizontal, horizontal)
|
|
89
|
+
.frame(height: height)
|
|
90
|
+
.background(
|
|
91
|
+
RoundedRectangle(cornerRadius: radius, style: .continuous)
|
|
92
|
+
.fill(bg)
|
|
93
|
+
)
|
|
94
|
+
.overlay(
|
|
95
|
+
Group {
|
|
96
|
+
if selected {
|
|
97
|
+
RoundedRectangle(cornerRadius: radius, style: .continuous)
|
|
98
|
+
.stroke(theme.colors.secondary, lineWidth: 2)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
.contentShape(RoundedRectangle(cornerRadius: radius, style: .continuous))
|
|
103
|
+
}
|
|
104
|
+
.buttonStyle(ActiveOpacityButtonStyle())
|
|
96
105
|
.modifier(ChipAccessibilityModifier(accessibilityLabel: accessibilityLabel))
|
|
97
106
|
}
|
|
98
107
|
}
|
|
@@ -30,6 +30,7 @@ public struct Collapse<Content: View>: View {
|
|
|
30
30
|
private let useBackgroundCollapseButton: Bool
|
|
31
31
|
private let backgroundColor: Color?
|
|
32
32
|
private let titleColor: Color?
|
|
33
|
+
private let accessibilityId: String?
|
|
33
34
|
private let onPress: ((Bool) -> Void)?
|
|
34
35
|
private let content: (() -> Content)?
|
|
35
36
|
|
|
@@ -49,6 +50,7 @@ public struct Collapse<Content: View>: View {
|
|
|
49
50
|
useBackgroundCollapseButton: Bool = false,
|
|
50
51
|
backgroundColor: Color? = nil,
|
|
51
52
|
titleColor: Color? = nil,
|
|
53
|
+
accessibilityId: String? = nil,
|
|
52
54
|
onPress: ((Bool) -> Void)? = nil,
|
|
53
55
|
@ViewBuilder content: @escaping () -> Content
|
|
54
56
|
) {
|
|
@@ -65,11 +67,17 @@ public struct Collapse<Content: View>: View {
|
|
|
65
67
|
self.useBackgroundCollapseButton = useBackgroundCollapseButton
|
|
66
68
|
self.backgroundColor = backgroundColor
|
|
67
69
|
self.titleColor = titleColor
|
|
70
|
+
self.accessibilityId = accessibilityId
|
|
68
71
|
self.onPress = onPress
|
|
69
72
|
self.content = content
|
|
70
73
|
_expanded = State(initialValue: expandDefault)
|
|
71
74
|
}
|
|
72
75
|
|
|
76
|
+
// Mirrors Kotlin's `accessibilityId ?: "Collapse/$title"`.
|
|
77
|
+
private var automationId: String {
|
|
78
|
+
accessibilityId ?? "Collapse/\(title)"
|
|
79
|
+
}
|
|
80
|
+
|
|
73
81
|
private var resolvedBackgroundColor: Color {
|
|
74
82
|
backgroundColor ?? theme.colors.background.surface
|
|
75
83
|
}
|
|
@@ -97,6 +105,7 @@ public struct Collapse<Content: View>: View {
|
|
|
97
105
|
.frame(maxWidth: .infinity)
|
|
98
106
|
.background(resolvedBackgroundColor)
|
|
99
107
|
.clipShape(RoundedRectangle(cornerRadius: Radius.S, style: .continuous))
|
|
108
|
+
.accessibilityIdentifier(automationId)
|
|
100
109
|
.overlay(
|
|
101
110
|
showBorder
|
|
102
111
|
? RoundedRectangle(cornerRadius: Radius.S, style: .continuous)
|
|
@@ -159,7 +168,8 @@ public struct Collapse<Content: View>: View {
|
|
|
159
168
|
.frame(maxWidth: .infinity)
|
|
160
169
|
.contentShape(Rectangle())
|
|
161
170
|
}
|
|
162
|
-
.buttonStyle(
|
|
171
|
+
.buttonStyle(ActiveOpacityButtonStyle())
|
|
172
|
+
.accessibilityIdentifier("\(automationId)|touch")
|
|
163
173
|
}
|
|
164
174
|
|
|
165
175
|
private var chevron: some View {
|
|
@@ -14,6 +14,8 @@ private let datePickerWithLabelsHeight: CGFloat = 238
|
|
|
14
14
|
/// - `"HH:mm"` shows hour + minute; a lone `"HH"` switches the hour wheel to a
|
|
15
15
|
/// 12-hour clock with an AM/PM column.
|
|
16
16
|
public struct DateTimePicker: View {
|
|
17
|
+
@Environment(\.appTheme) private var theme
|
|
18
|
+
|
|
17
19
|
// MARK: Lifecycle
|
|
18
20
|
|
|
19
21
|
public init(
|
|
@@ -84,7 +86,7 @@ public struct DateTimePicker: View {
|
|
|
84
86
|
|
|
85
87
|
VStack(spacing: 0) {
|
|
86
88
|
if hasLabel {
|
|
87
|
-
MomoText(arrayLabelTime[index], typography: .actionSBold, color:
|
|
89
|
+
MomoText(arrayLabelTime[index], typography: .actionSBold, color: theme.colors.text.default)
|
|
88
90
|
.padding(.bottom, Spacing.S)
|
|
89
91
|
}
|
|
90
92
|
|
|
@@ -100,7 +102,7 @@ public struct DateTimePicker: View {
|
|
|
100
102
|
}
|
|
101
103
|
.frame(maxWidth: .infinity)
|
|
102
104
|
.frame(height: pickerHeight)
|
|
103
|
-
.background(
|
|
105
|
+
.background(theme.colors.background.surface)
|
|
104
106
|
.padding(.horizontal, Spacing.M)
|
|
105
107
|
.onAppear { emitIfValid() }
|
|
106
108
|
}
|
|
@@ -14,6 +14,8 @@ private let wheelPickerBorderWidth: CGFloat = 1
|
|
|
14
14
|
/// real value can be centered. The currently centered row is reported back via
|
|
15
15
|
/// `onChange(name, value)`.
|
|
16
16
|
public struct WheelPicker: View {
|
|
17
|
+
@Environment(\.appTheme) private var theme
|
|
18
|
+
|
|
17
19
|
// MARK: Lifecycle
|
|
18
20
|
|
|
19
21
|
public init(
|
|
@@ -76,9 +78,9 @@ public struct WheelPicker: View {
|
|
|
76
78
|
.frame(height: wheelItemHeight * CGFloat(wheelVisibleCount))
|
|
77
79
|
.overlay(
|
|
78
80
|
RoundedRectangle(cornerRadius: Radius.S)
|
|
79
|
-
.stroke(
|
|
81
|
+
.stroke(theme.colors.border.default, lineWidth: wheelPickerBorderWidth)
|
|
80
82
|
)
|
|
81
|
-
.background(
|
|
83
|
+
.background(theme.colors.background.surface.clipShape(RoundedRectangle(cornerRadius: Radius.S)))
|
|
82
84
|
.overlay(
|
|
83
85
|
// Centered translucent selection band.
|
|
84
86
|
Rectangle()
|
|
@@ -121,6 +123,8 @@ public struct WheelPicker: View {
|
|
|
121
123
|
// MARK: - WheelPickerItem
|
|
122
124
|
|
|
123
125
|
private struct WheelPickerItem: View {
|
|
126
|
+
@Environment(\.appTheme) private var theme
|
|
127
|
+
|
|
124
128
|
let text: String
|
|
125
129
|
let isSelected: Bool
|
|
126
130
|
let index: Int
|
|
@@ -142,7 +146,7 @@ private struct WheelPickerItem: View {
|
|
|
142
146
|
let scale: CGFloat = isSelected ? 1.0 : 0.87
|
|
143
147
|
|
|
144
148
|
return ZStack {
|
|
145
|
-
MomoText(text, typography: .actionSBold, color:
|
|
149
|
+
MomoText(text, typography: .actionSBold, color: theme.colors.text.default)
|
|
146
150
|
.scaleEffect(scale)
|
|
147
151
|
}
|
|
148
152
|
.frame(maxWidth: .infinity)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
/// Mirrors Compose's `Modifier.activeOpacityClickable(activeOpacity:)` (modifier/Clickable.kt):
|
|
4
|
+
/// fades the whole tappable subtree to `activeOpacity` while pressed, animated, instead of
|
|
5
|
+
/// relying on a platform-default press indication.
|
|
6
|
+
struct ActiveOpacityButtonStyle: ButtonStyle {
|
|
7
|
+
var activeOpacity: Double = 0.2
|
|
8
|
+
|
|
9
|
+
func makeBody(configuration: Configuration) -> some View {
|
|
10
|
+
configuration.label
|
|
11
|
+
.opacity(configuration.isPressed ? activeOpacity : 1)
|
|
12
|
+
.animation(.default, value: configuration.isPressed)
|
|
13
|
+
}
|
|
14
|
+
}
|
package/ios/Icon/Icon.swift
CHANGED
|
@@ -49,7 +49,11 @@ public struct Icon: View {
|
|
|
49
49
|
if let uri = iconSources[source]?.uri {
|
|
50
50
|
// Mirror Compose: certain icons (e.g. branded glyphs) are never tinted
|
|
51
51
|
let resolvedColor = noThemeIcons.contains(source) ? nil : color
|
|
52
|
-
ImageView(uri, color: resolvedColor)
|
|
52
|
+
ImageView(uri, color: resolvedColor)
|
|
53
|
+
.frame(width: size, height: size)
|
|
54
|
+
// Mirrors Compose's auto-generated `contentDescription = "img|$iconUrl"`
|
|
55
|
+
// when no explicit label is provided.
|
|
56
|
+
.accessibilityLabel(accessibilityLabel ?? "img|\(uri)")
|
|
53
57
|
}
|
|
54
58
|
}
|
|
55
59
|
}
|
|
@@ -5,13 +5,15 @@ import SwiftUI
|
|
|
5
5
|
|
|
6
6
|
public enum IconSize {
|
|
7
7
|
case large
|
|
8
|
+
case medium
|
|
8
9
|
case small
|
|
9
10
|
|
|
10
11
|
/// Container width/height of the button.
|
|
11
12
|
var containerSize: CGFloat {
|
|
12
13
|
switch self {
|
|
13
14
|
case .large: return 48
|
|
14
|
-
case .
|
|
15
|
+
case .medium: return 36
|
|
16
|
+
case .small: return 28
|
|
15
17
|
}
|
|
16
18
|
}
|
|
17
19
|
|
|
@@ -19,13 +21,16 @@ public enum IconSize {
|
|
|
19
21
|
var iconSize: CGFloat {
|
|
20
22
|
switch self {
|
|
21
23
|
case .large: return 24
|
|
22
|
-
case .small: return 16
|
|
24
|
+
case .medium, .small: return 16
|
|
23
25
|
}
|
|
24
26
|
}
|
|
27
|
+
}
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
// MARK: - IconShape
|
|
30
|
+
|
|
31
|
+
public enum IconShape {
|
|
32
|
+
case circle
|
|
33
|
+
case square
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
// MARK: - IconButton
|
|
@@ -37,6 +42,7 @@ public struct IconButton: View {
|
|
|
37
42
|
var onClick: () -> Void
|
|
38
43
|
var type: ButtonType
|
|
39
44
|
var size: IconSize
|
|
45
|
+
var shape: IconShape
|
|
40
46
|
var color: Color?
|
|
41
47
|
|
|
42
48
|
public init(
|
|
@@ -44,12 +50,14 @@ public struct IconButton: View {
|
|
|
44
50
|
onClick: @escaping () -> Void,
|
|
45
51
|
type: ButtonType = .primary,
|
|
46
52
|
size: IconSize = .small,
|
|
53
|
+
shape: IconShape = .circle,
|
|
47
54
|
color: Color? = nil
|
|
48
55
|
) {
|
|
49
56
|
self.icon = icon
|
|
50
57
|
self.onClick = onClick
|
|
51
58
|
self.type = type
|
|
52
59
|
self.size = size
|
|
60
|
+
self.shape = shape
|
|
53
61
|
self.color = color
|
|
54
62
|
}
|
|
55
63
|
|
|
@@ -72,9 +80,9 @@ public struct IconButton: View {
|
|
|
72
80
|
Icon(source: icon, size: size.iconSize, color: fg)
|
|
73
81
|
.frame(width: size.containerSize, height: size.containerSize)
|
|
74
82
|
.background(bg)
|
|
75
|
-
.clipShape(
|
|
83
|
+
.clipShape(IconButtonShape(shape))
|
|
76
84
|
.overlay(
|
|
77
|
-
|
|
85
|
+
IconButtonShape(shape)
|
|
78
86
|
.stroke(border ?? .clear, lineWidth: border != nil ? borderWidth : 0)
|
|
79
87
|
)
|
|
80
88
|
}
|
|
@@ -110,3 +118,23 @@ public struct IconButton: View {
|
|
|
110
118
|
}
|
|
111
119
|
}
|
|
112
120
|
}
|
|
121
|
+
|
|
122
|
+
/// Type-erased shape switch (`Circle` vs a fixed `Radius.S` rounded rect) — mirrors
|
|
123
|
+
/// Compose's `IconShape.toShape()`. `AnyShape` needs iOS 17+, so this hand-rolls the same
|
|
124
|
+
/// erasure at the deployment target's iOS 15 floor.
|
|
125
|
+
private struct IconButtonShape: Shape {
|
|
126
|
+
let shape: IconShape
|
|
127
|
+
|
|
128
|
+
init(_ shape: IconShape) {
|
|
129
|
+
self.shape = shape
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
func path(in rect: CGRect) -> Path {
|
|
133
|
+
switch shape {
|
|
134
|
+
case .circle:
|
|
135
|
+
return Circle().path(in: rect)
|
|
136
|
+
case .square:
|
|
137
|
+
return RoundedRectangle(cornerRadius: Radius.S).path(in: rect)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
package/ios/Image/Image.swift
CHANGED
|
@@ -5,15 +5,23 @@ import SDWebImageSwiftUI
|
|
|
5
5
|
import SkeletonUI
|
|
6
6
|
import SwiftUI
|
|
7
7
|
|
|
8
|
+
public enum ImageState {
|
|
9
|
+
case loading, success, error
|
|
10
|
+
}
|
|
11
|
+
|
|
8
12
|
public struct ImageView: View {
|
|
13
|
+
@Environment(\.appTheme) private var theme
|
|
14
|
+
|
|
9
15
|
// MARK: Lifecycle
|
|
10
16
|
|
|
11
|
-
public init(_ url: String, aspectRatio: CGFloat? = nil, contentMode: ContentMode = ContentMode.fit, placeholder: AnyView? = nil, color: Color? = nil) {
|
|
17
|
+
public init(_ url: String, aspectRatio: CGFloat? = nil, contentMode: ContentMode = ContentMode.fit, placeholder: AnyView? = nil, color: Color? = nil, loading: Bool = true, onStateChanged: ((ImageState) -> Void)? = nil) {
|
|
12
18
|
self.url = url
|
|
13
19
|
self.aspectRatio = aspectRatio
|
|
14
20
|
self.contentMode = contentMode
|
|
15
21
|
self.placeholder = placeholder
|
|
16
22
|
self.color = color
|
|
23
|
+
self.loading = loading
|
|
24
|
+
self.onStateChanged = onStateChanged
|
|
17
25
|
}
|
|
18
26
|
|
|
19
27
|
// MARK: Public
|
|
@@ -22,9 +30,11 @@ public struct ImageView: View {
|
|
|
22
30
|
return WebImage(url: URL(string: url), isAnimating: .constant(true))
|
|
23
31
|
.onFailure { _ in
|
|
24
32
|
error = true
|
|
33
|
+
onStateChanged?(.error)
|
|
25
34
|
}
|
|
26
35
|
.onSuccess { _, _, _ in
|
|
27
36
|
error = false
|
|
37
|
+
onStateChanged?(.success)
|
|
28
38
|
}
|
|
29
39
|
.resizable()
|
|
30
40
|
.renderingMode(color != nil ? .template : .original)
|
|
@@ -34,20 +44,21 @@ public struct ImageView: View {
|
|
|
34
44
|
Image("media_fail")
|
|
35
45
|
.resizable()
|
|
36
46
|
.renderingMode(.template)
|
|
37
|
-
.foregroundColor(
|
|
47
|
+
.foregroundColor(theme.colors.text.disable)
|
|
38
48
|
.frame(width: 24, height: 24)
|
|
39
49
|
} else if placeholder != nil {
|
|
40
50
|
placeholder
|
|
41
51
|
}
|
|
42
52
|
})
|
|
43
53
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
44
|
-
.skeleton(with: !error && placeholder == nil)
|
|
54
|
+
.skeleton(with: loading && !error && placeholder == nil)
|
|
45
55
|
.shape(type: .rectangle)
|
|
46
56
|
}
|
|
47
57
|
.scaledToFit()
|
|
48
58
|
.transition(.fade)
|
|
49
59
|
.aspectRatio(aspectRatio, contentMode: contentMode)
|
|
50
60
|
.foregroundColor(color)
|
|
61
|
+
.onAppear { onStateChanged?(.loading) }
|
|
51
62
|
}
|
|
52
63
|
|
|
53
64
|
// MARK: Internal
|
|
@@ -56,6 +67,8 @@ public struct ImageView: View {
|
|
|
56
67
|
var contentMode: ContentMode
|
|
57
68
|
var placeholder: AnyView?
|
|
58
69
|
var color: Color?
|
|
70
|
+
var loading: Bool
|
|
71
|
+
var onStateChanged: ((ImageState) -> Void)?
|
|
59
72
|
|
|
60
73
|
@State var error: Bool = false
|
|
61
74
|
var url: String
|