@momo-kits/native-kits 0.163.1-sp.3-debug → 0.163.1-sp.4-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/settings.local.json +57 -1
- package/compose/build.gradle.kts +1 -1
- package/compose/compose.podspec +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +5 -5
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +4 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +66 -19
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +15 -3
- package/gradle.properties +1 -1
- package/ios/Application/Components.swift +14 -14
- package/ios/Application/HeaderRight.swift +45 -79
- package/ios/Application/Navigation/component/Header.swift +1 -1
- package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +11 -0
- package/ios/Application/Navigation/component/NavigationFooter.swift +90 -0
- package/ios/Application/Navigation/component/NavigationHeaderRight.swift +196 -0
- package/ios/Application/StackScreen.swift +18 -21
- package/ios/Badge/BadgeRibbon.swift +4 -1
- package/ios/Button/Button.swift +54 -80
- package/ios/Icon/Icon.swift +13 -4
- package/ios/Layout/GridContext.swift +30 -0
- package/ios/Layout/Item.swift +42 -0
- package/ios/Layout/Section.swift +134 -0
- package/ios/native-kits.podspec +2 -1
- package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ButtonDemo.swift +8 -8
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/CheckboxDemo.swift +3 -2
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +5 -12
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IconDemo.swift +104 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SectionDemo.swift +100 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SwiftUIDemoBrowserView.swift +3 -1
- package/package.json +1 -1
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
/// Span-sized cell inside a `SectionView`. Reads the grid published by its container;
|
|
4
|
+
/// without one it just wraps its content. Mirrors `vn.momo.kits.layout.Item`.
|
|
5
|
+
public struct ItemView<Content: View>: View {
|
|
6
|
+
// MARK: Lifecycle
|
|
7
|
+
|
|
8
|
+
public init(
|
|
9
|
+
widthSpan: Int = 12,
|
|
10
|
+
heightSpan: Int = 0,
|
|
11
|
+
@ViewBuilder content: @escaping () -> Content
|
|
12
|
+
) {
|
|
13
|
+
self.widthSpan = widthSpan
|
|
14
|
+
self.heightSpan = heightSpan
|
|
15
|
+
self.content = content
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// MARK: Public
|
|
19
|
+
|
|
20
|
+
public var body: some View {
|
|
21
|
+
// A VStack (not a ZStack) so multiple children flow VERTICALLY instead of stacking
|
|
22
|
+
// on top of each other — matches the React Item's flex-column content.
|
|
23
|
+
VStack(alignment: .leading, spacing: 0) { content() }
|
|
24
|
+
.frame(width: width, height: height, alignment: .topLeading)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// MARK: Private
|
|
28
|
+
|
|
29
|
+
private let widthSpan: Int
|
|
30
|
+
private let heightSpan: Int
|
|
31
|
+
private let content: () -> Content
|
|
32
|
+
|
|
33
|
+
@Environment(\.gridContext) private var grid
|
|
34
|
+
|
|
35
|
+
private var width: CGFloat? {
|
|
36
|
+
grid.map { $0.sizeForSpan(widthSpan) }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private var height: CGFloat? {
|
|
40
|
+
heightSpan > 0 ? grid.map { $0.sizeForSpan(heightSpan) } : nil
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
/// 12-column section container: horizontal margin, optional background image, and a
|
|
4
|
+
/// wrapping row of children that read their span size from `\.gridContext`.
|
|
5
|
+
/// Mirrors `vn.momo.kits.layout.Section`.
|
|
6
|
+
public struct SectionView<Content: View>: View {
|
|
7
|
+
// MARK: Lifecycle
|
|
8
|
+
|
|
9
|
+
public init(
|
|
10
|
+
useMargin: Bool = true,
|
|
11
|
+
backgroundImage: String? = nil,
|
|
12
|
+
@ViewBuilder content: @escaping () -> Content
|
|
13
|
+
) {
|
|
14
|
+
self.useMargin = useMargin
|
|
15
|
+
self.backgroundImage = backgroundImage
|
|
16
|
+
self.content = content
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// MARK: Public
|
|
20
|
+
|
|
21
|
+
public var body: some View {
|
|
22
|
+
flow
|
|
23
|
+
.environment(\.gridContext, grid)
|
|
24
|
+
.frame(maxWidth: .infinity, alignment: .topLeading)
|
|
25
|
+
.background(widthReader)
|
|
26
|
+
.background(background)
|
|
27
|
+
.padding(.horizontal, useMargin ? Spacing.M : 0)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// MARK: Private
|
|
31
|
+
|
|
32
|
+
private static var columns: Int { 12 }
|
|
33
|
+
private static var gutter: CGFloat { Spacing.M }
|
|
34
|
+
|
|
35
|
+
private let useMargin: Bool
|
|
36
|
+
private let backgroundImage: String?
|
|
37
|
+
private let content: () -> Content
|
|
38
|
+
|
|
39
|
+
/// Width available *inside* the horizontal margin — the Compose `BoxWithConstraints` equivalent.
|
|
40
|
+
@State private var availableWidth: CGFloat = 0
|
|
41
|
+
|
|
42
|
+
private var grid: GridContext {
|
|
43
|
+
let columns = CGFloat(Self.columns)
|
|
44
|
+
// The trailing `- 1 / columns` mirrors Compose: it absorbs rounding so 12 spans
|
|
45
|
+
// plus their gutters never overflow the row by a sub-point.
|
|
46
|
+
let sizePerSpan = (availableWidth - Self.gutter * (columns - 1)) / columns - 1 / columns
|
|
47
|
+
return GridContext(numberOfColumns: Self.columns, gutter: Self.gutter, sizePerSpan: max(0, sizePerSpan))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@ViewBuilder private var flow: some View {
|
|
51
|
+
if #available(iOS 16.0, *) {
|
|
52
|
+
FlowRowLayout(gutter: Self.gutter) { content() }
|
|
53
|
+
} else {
|
|
54
|
+
// No `Layout` protocol before iOS 16: children stack instead of wrapping.
|
|
55
|
+
// Full-width (span 12) items — the common case — look identical.
|
|
56
|
+
VStack(alignment: .leading, spacing: Self.gutter) { content() }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@ViewBuilder private var background: some View {
|
|
61
|
+
if let backgroundImage {
|
|
62
|
+
ImageView(backgroundImage, contentMode: .fill).clipped()
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private var widthReader: some View {
|
|
67
|
+
GeometryReader { proxy in
|
|
68
|
+
Color.clear.preference(key: SectionWidthKey.self, value: proxy.size.width)
|
|
69
|
+
}
|
|
70
|
+
.onPreferenceChange(SectionWidthKey.self) { availableWidth = $0 }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private struct SectionWidthKey: PreferenceKey {
|
|
75
|
+
static var defaultValue: CGFloat = 0
|
|
76
|
+
|
|
77
|
+
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
|
|
78
|
+
value = max(value, nextValue())
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// Wrapping row with equal horizontal/vertical spacing — the Compose `FlowRow` equivalent.
|
|
83
|
+
@available(iOS 16.0, *)
|
|
84
|
+
private struct FlowRowLayout: Layout {
|
|
85
|
+
struct Row {
|
|
86
|
+
var items: [(index: Int, size: CGSize)] = []
|
|
87
|
+
var width: CGFloat = 0
|
|
88
|
+
var height: CGFloat = 0
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let gutter: CGFloat
|
|
92
|
+
|
|
93
|
+
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) -> CGSize {
|
|
94
|
+
let maxWidth = proposal.width ?? .infinity
|
|
95
|
+
let rows = rows(for: subviews, maxWidth: maxWidth)
|
|
96
|
+
let height = rows.reduce(0) { $0 + $1.height } + gutter * CGFloat(max(0, rows.count - 1))
|
|
97
|
+
return CGSize(width: proposal.width ?? rows.map(\.width).max() ?? 0, height: height)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) {
|
|
101
|
+
var y = bounds.minY
|
|
102
|
+
for row in rows(for: subviews, maxWidth: bounds.width) {
|
|
103
|
+
var x = bounds.minX
|
|
104
|
+
for item in row.items {
|
|
105
|
+
subviews[item.index].place(
|
|
106
|
+
at: CGPoint(x: x, y: y),
|
|
107
|
+
anchor: .topLeading,
|
|
108
|
+
proposal: ProposedViewSize(item.size)
|
|
109
|
+
)
|
|
110
|
+
x += item.size.width + gutter
|
|
111
|
+
}
|
|
112
|
+
y += row.height + gutter
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private func rows(for subviews: Subviews, maxWidth: CGFloat) -> [Row] {
|
|
117
|
+
var rows: [Row] = []
|
|
118
|
+
var current = Row()
|
|
119
|
+
for index in subviews.indices {
|
|
120
|
+
let size = subviews[index].sizeThatFits(ProposedViewSize(width: maxWidth, height: nil))
|
|
121
|
+
let widthIfAppended = current.items.isEmpty ? size.width : current.width + gutter + size.width
|
|
122
|
+
// Sub-point tolerance: spans are computed to land exactly on `maxWidth`.
|
|
123
|
+
if !current.items.isEmpty, widthIfAppended > maxWidth + 0.5 {
|
|
124
|
+
rows.append(current)
|
|
125
|
+
current = Row()
|
|
126
|
+
}
|
|
127
|
+
current.width = current.items.isEmpty ? size.width : current.width + gutter + size.width
|
|
128
|
+
current.height = max(current.height, size.height)
|
|
129
|
+
current.items.append((index, size))
|
|
130
|
+
}
|
|
131
|
+
if !current.items.isEmpty { rows.append(current) }
|
|
132
|
+
return rows
|
|
133
|
+
}
|
|
134
|
+
}
|
package/ios/native-kits.podspec
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Pod::Spec.new do |s|
|
|
2
2
|
s.name = 'MoMoUIKits'
|
|
3
|
-
s.version = '0.163.1-sp.
|
|
3
|
+
s.version = '0.163.1-sp.4'
|
|
4
4
|
s.summary = 'MoMoUIKits for iOS'
|
|
5
5
|
s.homepage = 'https://momo.vn'
|
|
6
6
|
s.license = { :type => 'MIT' }
|
|
@@ -34,6 +34,7 @@ Pod::Spec.new do |s|
|
|
|
34
34
|
"InputDropDown/**/*.{swift,m,h}",
|
|
35
35
|
"InputMoney/**/*.{swift,m,h}",
|
|
36
36
|
"InputOTP/**/*.{swift,m,h}",
|
|
37
|
+
"Layout/**/*.{swift,m,h}",
|
|
37
38
|
"Loader/**/*.{swift,m,h}",
|
|
38
39
|
"Lottie/**/*.{swift,m,h}",
|
|
39
40
|
"OTPKeyboard/**/*.{swift,m,h}",
|
|
@@ -22,19 +22,19 @@ struct ButtonDemo: View {
|
|
|
22
22
|
|
|
23
23
|
Group {
|
|
24
24
|
Button(title: "Button", action: onPress, type: disabled ? .disabled : .primary, size: .large,
|
|
25
|
-
iconRight:
|
|
25
|
+
iconRight: "notifications_check_circle")
|
|
26
26
|
Button(title: "Button", action: onPress, size: .small)
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
Group {
|
|
30
30
|
Button(title: "Button", action: onPress, type: .danger,
|
|
31
|
-
iconLeft:
|
|
31
|
+
iconLeft: "notifications_check_circle")
|
|
32
32
|
Button(title: "Button", action: onPress, type: .danger, size: .medium)
|
|
33
33
|
Button(title: "Button", action: onPress, type: .danger, size: .small)
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
Group {
|
|
37
|
-
Button(title: "Button", action: onPress, type: .danger, iconLeft:
|
|
37
|
+
Button(title: "Button", action: onPress, type: .danger, iconLeft: "notifications_check_circle", isFull: false)
|
|
38
38
|
Button(title: "Button", action: onPress, type: .danger, size: .medium, isFull: false)
|
|
39
39
|
Button(title: "Button", action: onPress, type: .danger, size: .small, isFull: false)
|
|
40
40
|
}
|
|
@@ -48,9 +48,9 @@ struct ButtonDemo: View {
|
|
|
48
48
|
|
|
49
49
|
Group {
|
|
50
50
|
Button(title: "Tinted icon", action: onPress, type: .primary,
|
|
51
|
-
iconLeft:
|
|
51
|
+
iconLeft: "notifications_check_circle", useTintColor: true)
|
|
52
52
|
Button(title: "Original icon", action: onPress, type: .primary,
|
|
53
|
-
iconLeft:
|
|
53
|
+
iconLeft: "notifications_check_circle", useTintColor: false)
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
Group {
|
|
@@ -78,9 +78,9 @@ struct ButtonDemo: View {
|
|
|
78
78
|
}
|
|
79
79
|
Group {
|
|
80
80
|
Button(title: "Button", action: onPress, type: .primary, size: .medium, loading: true)
|
|
81
|
-
Button(title: "Button", action: onPress, type: .danger, size: .medium, iconLeft:
|
|
82
|
-
Button(title: "Button", action: onPress, type: .disabled, size: .medium, iconRight:
|
|
83
|
-
Button(title: "Button", action: onPress, type: .outline, size: .medium, iconLeft:
|
|
81
|
+
Button(title: "Button", action: onPress, type: .danger, size: .medium, iconLeft: "notifications_check_circle", loading: true)
|
|
82
|
+
Button(title: "Button", action: onPress, type: .disabled, size: .medium, iconRight: "notifications_check_circle", loading: true)
|
|
83
|
+
Button(title: "Button", action: onPress, type: .outline, size: .medium, iconLeft: "notifications_check_circle", iconRight: "notifications_check_circle", loading: true)
|
|
84
84
|
Button(title: "Button", action: onPress, type: .secondary, size: .medium, loading: true)
|
|
85
85
|
Button(title: "Button", action: onPress, type: .text, size: .medium, loading: true)
|
|
86
86
|
Button(title: "Button", action: onPress, type: .tonal, size: .medium, loading: true)
|
|
@@ -12,10 +12,11 @@ struct CheckboxDemo: View {
|
|
|
12
12
|
|
|
13
13
|
var body: some View {
|
|
14
14
|
VStack(alignment: .leading, spacing: 20) {
|
|
15
|
-
// Interactive checkbox
|
|
15
|
+
// Interactive checkbox. Checkbox toggles the binding itself;
|
|
16
|
+
// onChange only reports the new value — don't flip state again here.
|
|
16
17
|
Checkbox(
|
|
17
18
|
$checked,
|
|
18
|
-
onChange: {
|
|
19
|
+
onChange: { value in print("Checkbox changed: \(value)") },
|
|
19
20
|
title: "Check Box"
|
|
20
21
|
)
|
|
21
22
|
|
|
@@ -34,29 +34,22 @@ public func getIcon(icon: String) -> String {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
public let ScreenUsages: [ScreenUsage] = [
|
|
37
|
-
// ScreenUsage(title: "ScreenUsage", destination: AnyView(ContentView()), icon: getIcon(icon: "Layout")),
|
|
38
37
|
ScreenUsage(title: "Badge", destination: AnyView(BadgeDemo()), icon: getIcon(icon: "Badge")),
|
|
39
38
|
ScreenUsage(title: "Button", destination: AnyView(ButtonDemo()), icon: getIcon(icon: "Button")),
|
|
40
39
|
ScreenUsage(title: "CheckBox", destination: AnyView(CheckboxDemo()), icon: getIcon(icon: "Checkbox")),
|
|
41
40
|
ScreenUsage(title: "Chip", destination: AnyView(ChipDemo()), icon: getIcon(icon: "Chip")),
|
|
42
|
-
|
|
43
|
-
// ScreenUsage(title: "Icon", destination: AnyView(ContentView()), icon: getIcon(icon: "Icon")),
|
|
41
|
+
ScreenUsage(title: "Icon", destination: AnyView(IconDemo()), icon: getIcon(icon: "Icon")),
|
|
44
42
|
ScreenUsage(title: "Image", destination: AnyView(ImageDemo()), icon: getIcon(icon: "Layout")),
|
|
45
43
|
ScreenUsage(title: "Baseline View", destination: AnyView(BaselineViewDemo()), icon: getIcon(icon: "Layout")),
|
|
46
|
-
|
|
44
|
+
ScreenUsage(title: "Section", destination: AnyView(SectionDemo()), icon: getIcon(icon: "Layout")),
|
|
47
45
|
ScreenUsage(title: "Input", destination: AnyView(InputDemo()), icon: getIcon(icon: "FormInput")),
|
|
48
46
|
ScreenUsage(title: "Input Phone Number", destination: AnyView(InputPhoneNumberDemo()), icon: getIcon(icon: "FormInput")),
|
|
49
47
|
ScreenUsage(title: "InputTextArea", destination: AnyView(InputTextAreaDemo()), icon: getIcon(icon: "FormInput")),
|
|
50
48
|
ScreenUsage(title: "InputSearch", destination: AnyView(InputSearchDemo()), icon: getIcon(icon: "FormInput")),
|
|
51
|
-
// ScreenUsage(title: "Pagination", destination: AnyView(ContentView()), icon: getIcon(icon: "Badge")),
|
|
52
|
-
// ScreenUsage(title: "InputDropdown", destination: AnyView(ContentView()), icon: getIcon(icon: "FormInput")),
|
|
53
49
|
ScreenUsage(title: "PopupNotify", destination: AnyView(PopupNotifyDemo()), icon: getIcon(icon: "Popup")),
|
|
54
50
|
ScreenUsage(title: "PopupPromotion", destination: AnyView(PopupPromotionDemo()), icon: getIcon(icon: "Popup")),
|
|
55
51
|
ScreenUsage(title: "SnackBar", destination: AnyView(SnackBarDemo()), icon: getIcon(icon: "Popup")),
|
|
56
|
-
// ScreenUsage(title: "Radio", destination: AnyView(ContentView()), icon: getIcon(icon: "Radio")),
|
|
57
|
-
// ScreenUsage(title: "Skeleton", destination: AnyView(ContentView()), icon: getIcon(icon: "Skeleton")),
|
|
58
52
|
ScreenUsage(title: "Switch", destination: AnyView(SwitchDemo()), icon: getIcon(icon: "Toggle")),
|
|
59
|
-
// ScreenUsage(title: "Tag", destination: AnyView(ContentView()), icon: getIcon(icon: "Skeleton")),
|
|
60
53
|
ScreenUsage(title: "Text", destination: AnyView(TypographyDemo()), icon: getIcon(icon: "Typography")),
|
|
61
54
|
// ScreenUsage(title: "TrustBanner", destination: AnyView(TrustBanner(onPress: {_ in })), icon: getIcon(icon: "Typography")),
|
|
62
55
|
ScreenUsage(title: "Information", destination: AnyView(InformationDemo()), icon: getIcon(icon: "Typography")),
|
|
@@ -134,11 +127,11 @@ public struct HomeView: View {
|
|
|
134
127
|
headerRight: {
|
|
135
128
|
HeaderRight(headerRight: HeaderRightData(useShortcut: true))
|
|
136
129
|
},
|
|
137
|
-
// footerComponent = { Button("
|
|
130
|
+
// footerComponent = { Button("Open SwiftUI (iOS only)") }
|
|
138
131
|
footer: {
|
|
139
132
|
AnyView(
|
|
140
133
|
MoMoUIKits.Button(
|
|
141
|
-
title: "
|
|
134
|
+
title: "Open Compose",
|
|
142
135
|
action: onOpenCompose,
|
|
143
136
|
type: .primary,
|
|
144
137
|
size: .large
|
|
@@ -267,7 +260,7 @@ public struct HomeItemView: View {
|
|
|
267
260
|
Divider()
|
|
268
261
|
}
|
|
269
262
|
.padding(.horizontal, 16)
|
|
270
|
-
.padding(.vertical,
|
|
263
|
+
.padding(.vertical, 16)
|
|
271
264
|
}
|
|
272
265
|
}
|
|
273
266
|
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import MoMoUIKits
|
|
2
|
+
import SwiftUI
|
|
3
|
+
|
|
4
|
+
// MARK: - IconDemo
|
|
5
|
+
|
|
6
|
+
struct IconDemo: View {
|
|
7
|
+
var body: some View {
|
|
8
|
+
ScrollView {
|
|
9
|
+
VStack(alignment: .leading, spacing: 24) {
|
|
10
|
+
Group {
|
|
11
|
+
Text("Source")
|
|
12
|
+
.font(.headline)
|
|
13
|
+
HStack(spacing: 12) {
|
|
14
|
+
Icon(source: "basic_home", size: 24)
|
|
15
|
+
Icon(source: "basic_flag", size: 24)
|
|
16
|
+
Icon(source: "basic_filter", size: 24)
|
|
17
|
+
Icon(source: "basic_delete", size: 24)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
Group {
|
|
22
|
+
Text("Size")
|
|
23
|
+
.font(.headline)
|
|
24
|
+
HStack(alignment: .center, spacing: 12) {
|
|
25
|
+
Icon(source: "basic_home", size: 16)
|
|
26
|
+
Icon(source: "basic_flag", size: 20)
|
|
27
|
+
Icon(source: "basic_filter", size: 24)
|
|
28
|
+
Icon(source: "basic_delete", size: 28)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
Group {
|
|
33
|
+
Text("Color")
|
|
34
|
+
.font(.headline)
|
|
35
|
+
HStack(spacing: 12) {
|
|
36
|
+
Icon(source: "basic_home", size: 24, color: Colors.red03)
|
|
37
|
+
Icon(source: "basic_flag", size: 24, color: Colors.green04)
|
|
38
|
+
Icon(source: "basic_filter", size: 24, color: Colors.pink06)
|
|
39
|
+
Icon(source: "basic_delete", size: 24, color: Colors.yellow02)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
Group {
|
|
44
|
+
// Untinted / explicit colour / theme default (no colour argument)
|
|
45
|
+
Text("URL source")
|
|
46
|
+
.font(.headline)
|
|
47
|
+
HStack(spacing: 12) {
|
|
48
|
+
Icon(source: remoteIcon, size: 24, color: nil)
|
|
49
|
+
Icon(source: remoteIcon, size: 24, color: Colors.blue04)
|
|
50
|
+
Icon(source: remoteIcon, size: 24)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
Group {
|
|
55
|
+
Text("No theme icon")
|
|
56
|
+
.font(.headline)
|
|
57
|
+
// `ic_round_momo_tiny` is in noThemeIcons: the tint is dropped, so
|
|
58
|
+
// both render identically.
|
|
59
|
+
HStack(spacing: 12) {
|
|
60
|
+
Icon(source: "ic_round_momo_tiny", size: 24)
|
|
61
|
+
Icon(source: "ic_round_momo_tiny", size: 24, color: Colors.red03)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
Group {
|
|
66
|
+
Text("Gallery")
|
|
67
|
+
.font(.headline)
|
|
68
|
+
LazyVGrid(
|
|
69
|
+
columns: Array(repeating: GridItem(.flexible(), spacing: 12), count: 4),
|
|
70
|
+
spacing: 16
|
|
71
|
+
) {
|
|
72
|
+
ForEach(gallery, id: \.self) { source in
|
|
73
|
+
VStack(spacing: 6) {
|
|
74
|
+
Icon(source: source, size: 24, color: Colors.black17)
|
|
75
|
+
Text(source)
|
|
76
|
+
.font(.caption2)
|
|
77
|
+
.multilineTextAlignment(.center)
|
|
78
|
+
.foregroundColor(Colors.black17)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
.padding(16)
|
|
85
|
+
}
|
|
86
|
+
.padding(.top)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// MARK: Private
|
|
90
|
+
|
|
91
|
+
private let remoteIcon = "https://img.mservice.com.vn/app/img/kits/basic_home.png"
|
|
92
|
+
|
|
93
|
+
private let gallery = [
|
|
94
|
+
"basic_home",
|
|
95
|
+
"basic_flag",
|
|
96
|
+
"basic_filter",
|
|
97
|
+
"basic_delete",
|
|
98
|
+
"arrow_chevron_right",
|
|
99
|
+
"shopping_coupon",
|
|
100
|
+
"shopping_store",
|
|
101
|
+
"travel_plane",
|
|
102
|
+
"time_alarm_check",
|
|
103
|
+
]
|
|
104
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
import MoMoUIKits
|
|
3
|
+
|
|
4
|
+
struct SectionDemo: View {
|
|
5
|
+
var body: some View {
|
|
6
|
+
ScrollView {
|
|
7
|
+
// No horizontal padding here — SectionView brings its own 12pt margin.
|
|
8
|
+
VStack(alignment: .leading, spacing: 24) {
|
|
9
|
+
block("Full width — span 12") {
|
|
10
|
+
SectionView {
|
|
11
|
+
SectionTile("12")
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
block("Even splits") {
|
|
16
|
+
SectionView {
|
|
17
|
+
ItemView(widthSpan: 6) { SectionTile("6") }
|
|
18
|
+
ItemView(widthSpan: 6) { SectionTile("6") }
|
|
19
|
+
ItemView(widthSpan: 4) { SectionTile("4") }
|
|
20
|
+
ItemView(widthSpan: 4) { SectionTile("4") }
|
|
21
|
+
ItemView(widthSpan: 4) { SectionTile("4") }
|
|
22
|
+
ItemView(widthSpan: 3) { SectionTile("3") }
|
|
23
|
+
ItemView(widthSpan: 3) { SectionTile("3") }
|
|
24
|
+
ItemView(widthSpan: 3) { SectionTile("3") }
|
|
25
|
+
ItemView(widthSpan: 3) { SectionTile("3") }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
block("Wrapping — a row that overflows breaks to the next line") {
|
|
30
|
+
SectionView {
|
|
31
|
+
ItemView(widthSpan: 8) { SectionTile("8") }
|
|
32
|
+
ItemView(widthSpan: 5) { SectionTile("5") }
|
|
33
|
+
ItemView(widthSpan: 7) { SectionTile("7") }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
block("heightSpan — square tiles") {
|
|
38
|
+
SectionView {
|
|
39
|
+
ItemView(widthSpan: 3, heightSpan: 3) { SectionTile("3×3") }
|
|
40
|
+
ItemView(widthSpan: 3, heightSpan: 3) { SectionTile("3×3") }
|
|
41
|
+
ItemView(widthSpan: 6, heightSpan: 3) { SectionTile("6×3") }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
block("useMargin: false — edge to edge") {
|
|
46
|
+
SectionView(useMargin: false) {
|
|
47
|
+
ItemView(widthSpan: 6) { SectionTile("6") }
|
|
48
|
+
ItemView(widthSpan: 6) { SectionTile("6") }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
block("backgroundImage") {
|
|
53
|
+
SectionView(backgroundImage: "https://images.unsplash.com/photo-1509395062183-67c5ad6faff9") {
|
|
54
|
+
ItemView(widthSpan: 6, heightSpan: 4) { SectionTile("6×4") }
|
|
55
|
+
ItemView(widthSpan: 6, heightSpan: 4) { SectionTile("6×4") }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
block("Multiple children stack vertically inside one Item") {
|
|
60
|
+
SectionView {
|
|
61
|
+
ItemView(widthSpan: 6) {
|
|
62
|
+
SectionTile("top")
|
|
63
|
+
SectionTile("bottom")
|
|
64
|
+
}
|
|
65
|
+
ItemView(widthSpan: 6) { SectionTile("6") }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
.padding(.vertical)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@ViewBuilder
|
|
74
|
+
private func block<Content: View>(_ title: String, @ViewBuilder content: () -> Content) -> some View {
|
|
75
|
+
VStack(alignment: .leading, spacing: 8) {
|
|
76
|
+
Text(title)
|
|
77
|
+
.font(.headline)
|
|
78
|
+
.padding(.horizontal, Spacing.M)
|
|
79
|
+
content()
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// Filled placeholder so span widths are visible.
|
|
85
|
+
private struct SectionTile: View {
|
|
86
|
+
let label: String
|
|
87
|
+
|
|
88
|
+
init(_ label: String) {
|
|
89
|
+
self.label = label
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
var body: some View {
|
|
93
|
+
Text(label)
|
|
94
|
+
.font(.footnote)
|
|
95
|
+
.foregroundColor(Colors.black17)
|
|
96
|
+
.frame(maxWidth: .infinity, minHeight: 44, maxHeight: .infinity)
|
|
97
|
+
.background(Colors.blue08)
|
|
98
|
+
.clipShape(RoundedRectangle(cornerRadius: Radius.S, style: .continuous))
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -7,7 +7,7 @@ struct SwiftUIDemoBrowserView: View {
|
|
|
7
7
|
@State private var searchText = ""
|
|
8
8
|
|
|
9
9
|
var body: some View {
|
|
10
|
-
|
|
10
|
+
ZStack {
|
|
11
11
|
if #available(iOS 16.0, *) {
|
|
12
12
|
HomeView(
|
|
13
13
|
onOpenCompose: onOpenCompose,
|
|
@@ -16,6 +16,8 @@ struct SwiftUIDemoBrowserView: View {
|
|
|
16
16
|
} else {
|
|
17
17
|
fallbackBrowser
|
|
18
18
|
}
|
|
19
|
+
|
|
20
|
+
BaselineView()
|
|
19
21
|
}
|
|
20
22
|
.environment(\.appTheme, defaultTheme)
|
|
21
23
|
}
|