@momo-kits/native-kits 0.162.30 → 0.162.31
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 +19 -0
- package/ios/Application/ApplicationEnvironment.swift +14 -6
- package/ios/Application/LanguageSource.swift +93 -0
- package/ios/Application/Localize.swift +73 -4
- package/ios/Application/LocalizeModifier.swift +30 -0
- package/ios/Application/Navigation/NavigationContainer.swift +28 -18
- package/ios/Application/Navigation/Navigator.swift +121 -53
- package/ios/Application/Navigation/NavigatorManager.swift +49 -0
- package/ios/Application/Navigation/component/NavigationFooter.swift +90 -0
- package/ios/Application/StackScreen.swift +19 -28
- package/ios/Badge/Badge.swift +2 -1
- package/ios/Badge/BadgeRibbon.swift +6 -2
- package/ios/Button/Button.swift +41 -61
- package/ios/Chip/Chip.swift +6 -5
- package/ios/Icon/Icon.swift +13 -4
- package/ios/Input/ErrorView.swift +2 -1
- package/ios/Input/Input.swift +9 -2
- package/ios/Input/InputPhoneNumber.swift +2 -1
- package/ios/Input/InputSearch.swift +3 -2
- package/ios/Input/InputTextArea.swift +6 -1
- package/ios/InputMoney/InputMoney.swift +5 -2
- package/ios/InputOTP/InputOTP.swift +2 -1
- package/ios/Layout/GridContext.swift +30 -0
- package/ios/Layout/Item.swift +42 -0
- package/ios/Layout/Section.swift +134 -0
- package/ios/Popup/PopupNotify.swift +2 -1
- package/ios/ProgressInfo/ProgressInfo.swift +4 -3
- package/ios/Rating/Rating.swift +2 -1
- package/ios/Stepper/Stepper.swift +2 -1
- package/ios/SuggestAction/SuggestAction.swift +3 -2
- package/ios/Tag/Tag.swift +2 -1
- package/ios/Template/TrustBanner/TrustBanner.swift +4 -2
- package/ios/Title/Title.swift +3 -2
- package/ios/Tooltip/Tooltip.swift +2 -1
- package/ios/Typography/FontScale.swift +103 -0
- package/ios/Typography/FontScaleStore.swift +52 -13
- package/ios/Typography/Text.swift +12 -29
- package/ios/Typography/Typography.swift +66 -86
- package/ios/native-kits.podspec +2 -1
- package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/AvatarDemo.swift +87 -73
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/BadgeDemo.swift +55 -249
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ButtonDemo.swift +90 -82
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/CarouselDemo.swift +74 -104
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/CheckboxDemo.swift +47 -50
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ChipDemo.swift +63 -7
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/CollapseDemo.swift +90 -81
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DateTimePickerDemo.swift +73 -97
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DemoScreen.swift +420 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DividerDemo.swift +46 -4
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +54 -57
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IconButtonDemo.swift +80 -45
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IconDemo.swift +81 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ImageDemo.swift +49 -62
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InformationDemo.swift +66 -116
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputDemo.swift +104 -98
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputDropDownDemo.swift +76 -118
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputMoneyDemo.swift +73 -86
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputOTPDemo.swift +69 -54
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputPhoneNumberDemo.swift +74 -54
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputSearchDemo.swift +73 -20
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputTextAreaDemo.swift +68 -38
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/LoaderDemo.swift +55 -52
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/LocalizeDemo.swift +51 -4
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +14 -2
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PaginationDemo.swift +47 -51
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PlaygroundData.swift +76 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupNotifyDemo.swift +67 -93
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupPromotionDemo.swift +34 -43
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ProgressInfoDemo.swift +83 -71
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/RadioDemo.swift +55 -23
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/RatingDemo.swift +58 -46
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SectionDemo.swift +83 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SkeletonDemo.swift +41 -12
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SliderDemo.swift +67 -127
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SnackBarDemo.swift +54 -110
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/StepperDemo.swift +69 -79
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/StepsDemo.swift +78 -58
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SuggestActionDemo.swift +76 -65
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SwipeDemo.swift +82 -127
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SwitchDemo.swift +44 -19
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TabViewDemo.swift +92 -168
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TagDemo.swift +79 -27
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ThemeDemo.swift +0 -1
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TitleDemo.swift +87 -92
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TooltipDemo.swift +94 -163
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TypographyDemo.swift +70 -70
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/UploaderDemo.swift +68 -114
- package/ios-demo/Sources/MoMoUIKitsDemo/SwiftUIDemoBrowserView.swift +3 -1
- package/package.json +2 -2
|
@@ -2,123 +2,99 @@
|
|
|
2
2
|
// DateTimePickerDemo.swift
|
|
3
3
|
// Example
|
|
4
4
|
//
|
|
5
|
+
// Preview + Playground demo (mirrors the Compose DemoScreen pattern).
|
|
6
|
+
//
|
|
5
7
|
|
|
6
|
-
import Foundation
|
|
7
8
|
import MoMoUIKits
|
|
8
9
|
import SwiftUI
|
|
9
10
|
|
|
10
|
-
// MARK: - DateTimePickerDemo
|
|
11
|
-
|
|
12
11
|
struct DateTimePickerDemo: View {
|
|
12
|
+
var body: some View {
|
|
13
|
+
if #available(iOS 16.0, *) {
|
|
14
|
+
DemoScreen(preview: { DateTimePickerPreview() }, playground: { DateTimePickerPlayground() })
|
|
15
|
+
} else {
|
|
16
|
+
Text("Requires iOS 16+").padding()
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// MARK: - Preview
|
|
22
|
+
|
|
23
|
+
private struct DateTimePickerPreview: View {
|
|
13
24
|
@State private var selected1: Date?
|
|
14
25
|
@State private var selected2: Date?
|
|
15
26
|
@State private var selected3: Date?
|
|
16
27
|
@State private var selected4: Date?
|
|
17
|
-
@State private var selected5: Date?
|
|
18
|
-
@State private var selected6: Date?
|
|
19
|
-
@State private var selected7: Date? = DateTimePickerUtils.getCurrentDateTime()
|
|
20
28
|
|
|
21
29
|
var body: some View {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
DateTimePicker(
|
|
26
|
-
format: "DD-MM-YYYY",
|
|
27
|
-
onChange: { selected1 = $0 }
|
|
28
|
-
)
|
|
29
|
-
selectedLabel(selected1, format: "dd-MM-yyyy")
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
section("Format: DD-MM-YYYY HH:mm") {
|
|
33
|
-
DateTimePicker(
|
|
34
|
-
format: "DD-MM-YYYY HH:mm",
|
|
35
|
-
onChange: { selected2 = $0 }
|
|
36
|
-
)
|
|
37
|
-
selectedLabel(selected2, format: "yyyy-MM-dd HH:mm")
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
section("Format: HH:mm") {
|
|
41
|
-
DateTimePicker(
|
|
42
|
-
format: "HH:mm",
|
|
43
|
-
onChange: { selected3 = $0 }
|
|
44
|
-
)
|
|
45
|
-
selectedLabel(selected3, format: "HH:mm")
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
section("With minute interval (5 minutes)") {
|
|
49
|
-
DateTimePicker(
|
|
50
|
-
format: "DD-MM-YYYY HH:mm",
|
|
51
|
-
minuteInterval: 5,
|
|
52
|
-
onChange: { selected4 = $0 }
|
|
53
|
-
)
|
|
54
|
-
selectedLabel(selected4, format: "yyyy-MM-dd HH:mm")
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
section("With min/max date") {
|
|
58
|
-
DateTimePicker(
|
|
59
|
-
format: "DD-MM-YYYY",
|
|
60
|
-
selectedValue: nil,
|
|
61
|
-
minDate: DateTimePickerUtils.createRelativeDate(years: -1),
|
|
62
|
-
maxDate: DateTimePickerUtils.createRelativeDate(years: 1),
|
|
63
|
-
onChange: { selected5 = $0 }
|
|
64
|
-
)
|
|
65
|
-
selectedLabel(selected5, format: "dd-MM-yyyy")
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
section("With labels") {
|
|
69
|
-
DateTimePicker(
|
|
70
|
-
format: "DD-MM-YYYY HH:mm",
|
|
71
|
-
arrayLabelTime: ["Day", "Month", "Year", "Hour", "Min"],
|
|
72
|
-
onChange: { selected6 = $0 }
|
|
73
|
-
)
|
|
74
|
-
selectedLabel(selected6, format: "yyyy-MM-dd HH:mm")
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
section("With selected value") {
|
|
78
|
-
DateTimePicker(
|
|
79
|
-
format: "DD-MM-YYYY HH:mm",
|
|
80
|
-
selectedValue: selected7,
|
|
81
|
-
onChange: { selected7 = $0 }
|
|
82
|
-
)
|
|
83
|
-
selectedLabel(selected7, format: "yyyy-MM-dd HH:mm")
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
.padding(.vertical, 16)
|
|
30
|
+
BasicColumnBox("Format: DD-MM-YYYY", alignment: .leading) {
|
|
31
|
+
DateTimePicker(format: "DD-MM-YYYY", onChange: { selected1 = $0 })
|
|
32
|
+
selectedLabel(selected1, format: "dd-MM-yyyy")
|
|
87
33
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
34
|
+
BasicColumnBox("Format: DD-MM-YYYY HH:mm", alignment: .leading) {
|
|
35
|
+
DateTimePicker(format: "DD-MM-YYYY HH:mm", onChange: { selected2 = $0 })
|
|
36
|
+
selectedLabel(selected2, format: "yyyy-MM-dd HH:mm")
|
|
37
|
+
}
|
|
38
|
+
BasicColumnBox("Format: HH:mm", alignment: .leading) {
|
|
39
|
+
DateTimePicker(format: "HH:mm", onChange: { selected3 = $0 })
|
|
40
|
+
selectedLabel(selected3, format: "HH:mm")
|
|
41
|
+
}
|
|
42
|
+
BasicColumnBox("With min/max date (±1 year)", alignment: .leading) {
|
|
43
|
+
DateTimePicker(
|
|
44
|
+
format: "DD-MM-YYYY",
|
|
45
|
+
selectedValue: nil,
|
|
46
|
+
minDate: DateTimePickerUtils.createRelativeDate(years: -1),
|
|
47
|
+
maxDate: DateTimePickerUtils.createRelativeDate(years: 1),
|
|
48
|
+
onChange: { selected4 = $0 }
|
|
99
49
|
)
|
|
100
|
-
|
|
50
|
+
selectedLabel(selected4, format: "dd-MM-yyyy")
|
|
101
51
|
}
|
|
102
52
|
}
|
|
103
53
|
|
|
104
54
|
@ViewBuilder
|
|
105
|
-
private func
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
Text(title)
|
|
111
|
-
.font(.headline)
|
|
112
|
-
content()
|
|
55
|
+
private func selectedLabel(_ date: Date?, format: String) -> some View {
|
|
56
|
+
if let date = date {
|
|
57
|
+
MomoText("Selected: \(DateTimePickerUtils.formatDate(date, format: format))",
|
|
58
|
+
typography: .descriptionDefaultRegular, color: Colors.textSecondary)
|
|
59
|
+
.padding(.top, Spacing.S)
|
|
113
60
|
}
|
|
114
|
-
.padding(.horizontal, 16)
|
|
115
61
|
}
|
|
116
62
|
}
|
|
117
63
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
64
|
+
// MARK: - Playground
|
|
65
|
+
|
|
66
|
+
private let dateFormatList: [PlaygroundOption] = [
|
|
67
|
+
.init("DD-MM-YYYY", "DD-MM-YYYY"),
|
|
68
|
+
.init("DD-MM-YYYY HH:mm", "DD-MM-YYYY HH:mm"),
|
|
69
|
+
.init("HH:mm", "HH:mm"),
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
@available(iOS 16.0, *)
|
|
73
|
+
private struct DateTimePickerPlayground: View {
|
|
74
|
+
@State private var format: PlaygroundOption? = dateFormatList.first { $0.key == "DD-MM-YYYY HH:mm" }
|
|
75
|
+
@State private var minuteInterval = 1.0
|
|
76
|
+
@State private var selected: Date?
|
|
77
|
+
|
|
78
|
+
var body: some View {
|
|
79
|
+
PlaygroundTab(
|
|
80
|
+
component: {
|
|
81
|
+
VStack(alignment: .leading, spacing: Spacing.S) {
|
|
82
|
+
DateTimePicker(
|
|
83
|
+
format: (format?.value as? String) ?? "DD-MM-YYYY HH:mm",
|
|
84
|
+
minuteInterval: Int(minuteInterval),
|
|
85
|
+
onChange: { selected = $0 }
|
|
86
|
+
)
|
|
87
|
+
.id("\(format?.key ?? "")-\(Int(minuteInterval))")
|
|
88
|
+
if let selected {
|
|
89
|
+
MomoText("Selected: \(DateTimePickerUtils.formatDate(selected, format: "yyyy-MM-dd HH:mm"))",
|
|
90
|
+
typography: .descriptionDefaultRegular, color: Colors.textSecondary)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
params: [
|
|
95
|
+
.radio("Format", dateFormatList, $format),
|
|
96
|
+
.slider("MinuteInterval", $minuteInterval, min: 1, max: 30, step: 1),
|
|
97
|
+
]
|
|
98
|
+
)
|
|
122
99
|
}
|
|
123
100
|
}
|
|
124
|
-
#endif
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
//
|
|
2
|
+
// DemoScreen.swift
|
|
3
|
+
// Example
|
|
4
|
+
//
|
|
5
|
+
// SwiftUI port of Compose's `sample/shared/.../screens/DemoScreen.kt`: the demo
|
|
6
|
+
// shell every component uses. `DemoScreen(preview:playground:)` renders a two-tab
|
|
7
|
+
// `BottomTab` — a static "Preview" showcase and an interactive, data-driven
|
|
8
|
+
// "Playground" props editor (BasicColumnBox / PlaygroundParams / PlaygroundView /
|
|
9
|
+
// PlaygroundTab). Must be pushed inside the `NavigationContainer` (BottomTab reads
|
|
10
|
+
// the Navigator) with `headerType: .none, scrollable: false`.
|
|
11
|
+
//
|
|
12
|
+
|
|
13
|
+
import MoMoUIKits
|
|
14
|
+
import SwiftUI
|
|
15
|
+
|
|
16
|
+
// MARK: - DemoScreen shell
|
|
17
|
+
|
|
18
|
+
@available(iOS 16.0, *)
|
|
19
|
+
struct DemoScreen<Preview: View, Playground: View>: View {
|
|
20
|
+
@ViewBuilder let preview: () -> Preview
|
|
21
|
+
@ViewBuilder let playground: () -> Playground
|
|
22
|
+
|
|
23
|
+
var body: some View {
|
|
24
|
+
BottomTab(items: [
|
|
25
|
+
BottomTabItem(
|
|
26
|
+
name: "Preview",
|
|
27
|
+
label: "Preview",
|
|
28
|
+
icon: "security_eye_open",
|
|
29
|
+
options: NavigationOptions(headerTitle: .default("Preview"), scrollable: true),
|
|
30
|
+
// StackScreen provides the scroll when scrollable == true.
|
|
31
|
+
screen: {
|
|
32
|
+
VStack(alignment: .leading, spacing: 0) { preview() }
|
|
33
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
34
|
+
}
|
|
35
|
+
),
|
|
36
|
+
BottomTabItem(
|
|
37
|
+
name: "Playground",
|
|
38
|
+
label: "Playground",
|
|
39
|
+
icon: "24_basic_chart",
|
|
40
|
+
// Playground owns its own scroll (Props list), so disable the outer one.
|
|
41
|
+
options: NavigationOptions(headerTitle: .default("Playground"), scrollable: false),
|
|
42
|
+
screen: { playground() }
|
|
43
|
+
),
|
|
44
|
+
])
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// MARK: - BasicColumnBox (titled surface card)
|
|
49
|
+
|
|
50
|
+
struct BasicColumnBox<Content: View>: View {
|
|
51
|
+
let title: String
|
|
52
|
+
let alignment: HorizontalAlignment
|
|
53
|
+
let background: Color?
|
|
54
|
+
@ViewBuilder let content: () -> Content
|
|
55
|
+
|
|
56
|
+
init(
|
|
57
|
+
_ title: String = "",
|
|
58
|
+
alignment: HorizontalAlignment = .center,
|
|
59
|
+
background: Color? = nil,
|
|
60
|
+
@ViewBuilder content: @escaping () -> Content
|
|
61
|
+
) {
|
|
62
|
+
self.title = title
|
|
63
|
+
self.alignment = alignment
|
|
64
|
+
self.background = background
|
|
65
|
+
self.content = content
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
var body: some View {
|
|
69
|
+
VStack(alignment: .leading, spacing: Spacing.S) {
|
|
70
|
+
if !title.isEmpty {
|
|
71
|
+
MomoText(title, typography: .headerMBold)
|
|
72
|
+
.padding(.top, Spacing.S)
|
|
73
|
+
}
|
|
74
|
+
VStack(alignment: alignment, spacing: Spacing.S) {
|
|
75
|
+
content()
|
|
76
|
+
}
|
|
77
|
+
.frame(maxWidth: .infinity, alignment: alignmentFrame)
|
|
78
|
+
.padding(Spacing.M)
|
|
79
|
+
.background(
|
|
80
|
+
RoundedRectangle(cornerRadius: Radius.M)
|
|
81
|
+
.fill(background ?? defaultTheme.colors.background.surface)
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
.padding(Spacing.M)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private var alignmentFrame: Alignment {
|
|
88
|
+
switch alignment {
|
|
89
|
+
case .leading: return .leading
|
|
90
|
+
case .trailing: return .trailing
|
|
91
|
+
default: return .center
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// MARK: - Playground model (mirrors PlaygroundParams / PlaygroundDataType)
|
|
97
|
+
|
|
98
|
+
enum PlaygroundDataType {
|
|
99
|
+
case string, number, option, color, radio, checkbox, slider
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/// One selectable entry for OPTION / COLOR / RADIO params. Replaces Compose's
|
|
103
|
+
/// `Map<String, Any>` + `Map.Entry`. Equality is by `key`.
|
|
104
|
+
struct PlaygroundOption: Identifiable, Hashable {
|
|
105
|
+
var id: String { key }
|
|
106
|
+
let key: String
|
|
107
|
+
let value: AnyHashable
|
|
108
|
+
|
|
109
|
+
init(_ key: String, _ value: AnyHashable) {
|
|
110
|
+
self.key = key
|
|
111
|
+
self.value = value
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
static func == (lhs: PlaygroundOption, rhs: PlaygroundOption) -> Bool { lhs.key == rhs.key }
|
|
115
|
+
func hash(into hasher: inout Hasher) { hasher.combine(key) }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// A single editable prop row. Build with the per-type factory methods so the
|
|
119
|
+
/// call site stays type-safe (`.radio("Type", buttonTypeList, $type)`).
|
|
120
|
+
struct PlaygroundParams: Identifiable {
|
|
121
|
+
// Identity is the title, never a fresh UUID: the array is rebuilt on every body
|
|
122
|
+
// pass, so a per-instance id would re-key the ForEach on each keystroke — tearing
|
|
123
|
+
// down the row (an editing TextField loses first responder) and wiping its @State.
|
|
124
|
+
var id: String { title }
|
|
125
|
+
let title: String
|
|
126
|
+
let type: PlaygroundDataType
|
|
127
|
+
let list: [PlaygroundOption]
|
|
128
|
+
let enable: Binding<Bool>
|
|
129
|
+
let required: Bool
|
|
130
|
+
let sliderMin: Double
|
|
131
|
+
let sliderMax: Double
|
|
132
|
+
let sliderStep: Double
|
|
133
|
+
|
|
134
|
+
// Only the binding matching `type` is non-nil.
|
|
135
|
+
let text: Binding<String>?
|
|
136
|
+
let selection: Binding<PlaygroundOption?>?
|
|
137
|
+
let flag: Binding<Bool>?
|
|
138
|
+
let number: Binding<Double>?
|
|
139
|
+
|
|
140
|
+
private init(
|
|
141
|
+
title: String,
|
|
142
|
+
type: PlaygroundDataType,
|
|
143
|
+
list: [PlaygroundOption] = [],
|
|
144
|
+
enable: Binding<Bool> = .constant(true),
|
|
145
|
+
required: Bool = false,
|
|
146
|
+
sliderMin: Double = 0,
|
|
147
|
+
sliderMax: Double = 100,
|
|
148
|
+
sliderStep: Double = 1,
|
|
149
|
+
text: Binding<String>? = nil,
|
|
150
|
+
selection: Binding<PlaygroundOption?>? = nil,
|
|
151
|
+
flag: Binding<Bool>? = nil,
|
|
152
|
+
number: Binding<Double>? = nil
|
|
153
|
+
) {
|
|
154
|
+
self.title = title
|
|
155
|
+
self.type = type
|
|
156
|
+
self.list = list
|
|
157
|
+
self.enable = enable
|
|
158
|
+
self.required = required
|
|
159
|
+
self.sliderMin = sliderMin
|
|
160
|
+
self.sliderMax = sliderMax
|
|
161
|
+
self.sliderStep = sliderStep
|
|
162
|
+
self.text = text
|
|
163
|
+
self.selection = selection
|
|
164
|
+
self.flag = flag
|
|
165
|
+
self.number = number
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
static func string(_ title: String, _ text: Binding<String>, required: Bool = false, enable: Binding<Bool> = .constant(true)) -> PlaygroundParams {
|
|
169
|
+
PlaygroundParams(title: title, type: .string, enable: enable, required: required, text: text)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
static func number(_ title: String, _ text: Binding<String>, required: Bool = false, enable: Binding<Bool> = .constant(true)) -> PlaygroundParams {
|
|
173
|
+
PlaygroundParams(title: title, type: .number, enable: enable, required: required, text: text)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
static func option(_ title: String, _ list: [PlaygroundOption], _ selection: Binding<PlaygroundOption?>, required: Bool = false, enable: Binding<Bool> = .constant(true)) -> PlaygroundParams {
|
|
177
|
+
PlaygroundParams(title: title, type: .option, list: list, enable: enable, required: required, selection: selection)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
static func color(_ title: String, _ list: [PlaygroundOption], _ selection: Binding<PlaygroundOption?>, required: Bool = false, enable: Binding<Bool> = .constant(true)) -> PlaygroundParams {
|
|
181
|
+
PlaygroundParams(title: title, type: .color, list: list, enable: enable, required: required, selection: selection)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
static func radio(_ title: String, _ list: [PlaygroundOption], _ selection: Binding<PlaygroundOption?>, required: Bool = false, enable: Binding<Bool> = .constant(true)) -> PlaygroundParams {
|
|
185
|
+
PlaygroundParams(title: title, type: .radio, list: list, enable: enable, required: required, selection: selection)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
static func checkbox(_ title: String, _ flag: Binding<Bool>, enable: Binding<Bool> = .constant(true)) -> PlaygroundParams {
|
|
189
|
+
PlaygroundParams(title: title, type: .checkbox, enable: enable, flag: flag)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
static func slider(_ title: String, _ number: Binding<Double>, min: Double = 0, max: Double = 100, step: Double = 1, enable: Binding<Bool> = .constant(true)) -> PlaygroundParams {
|
|
193
|
+
PlaygroundParams(title: title, type: .slider, enable: enable, sliderMin: min, sliderMax: max, sliderStep: step, number: number)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// MARK: - PlaygroundTab (live component + scrollable props list)
|
|
198
|
+
|
|
199
|
+
@available(iOS 16.0, *)
|
|
200
|
+
struct PlaygroundTab<Component: View>: View {
|
|
201
|
+
@ViewBuilder let component: () -> Component
|
|
202
|
+
let params: [PlaygroundParams]
|
|
203
|
+
var isHideComponent: Bool = false
|
|
204
|
+
|
|
205
|
+
var body: some View {
|
|
206
|
+
VStack(spacing: 0) {
|
|
207
|
+
if !isHideComponent {
|
|
208
|
+
BasicColumnBox("Component", alignment: .leading) { component() }
|
|
209
|
+
}
|
|
210
|
+
ScrollView {
|
|
211
|
+
BasicColumnBox("Props", alignment: .leading) {
|
|
212
|
+
ForEach(params) { param in
|
|
213
|
+
MomoText(param.title, typography: .headerDefaultBold)
|
|
214
|
+
.opacity(param.enable.wrappedValue ? 1 : 0.4)
|
|
215
|
+
PlaygroundView(param: param)
|
|
216
|
+
Spacer().frame(height: Spacing.L)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// MARK: - PlaygroundView (per-type editor, mirrors PlayGroundView.kt)
|
|
225
|
+
|
|
226
|
+
@available(iOS 16.0, *)
|
|
227
|
+
struct PlaygroundView: View {
|
|
228
|
+
let param: PlaygroundParams
|
|
229
|
+
@EnvironmentObject private var navigator: Navigator
|
|
230
|
+
@State private var isUndefined = false
|
|
231
|
+
@State private var initialSelection: PlaygroundOption?
|
|
232
|
+
@State private var didCapture = false
|
|
233
|
+
|
|
234
|
+
var body: some View {
|
|
235
|
+
editor
|
|
236
|
+
.opacity(param.enable.wrappedValue ? 1 : 0.4)
|
|
237
|
+
.disabled(!param.enable.wrappedValue)
|
|
238
|
+
.allowsHitTesting(param.enable.wrappedValue)
|
|
239
|
+
.onAppear {
|
|
240
|
+
if !didCapture {
|
|
241
|
+
initialSelection = param.selection?.wrappedValue
|
|
242
|
+
didCapture = true
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
@ViewBuilder
|
|
248
|
+
private var editor: some View {
|
|
249
|
+
switch param.type {
|
|
250
|
+
case .string, .number: stringEditor
|
|
251
|
+
case .option, .color: optionEditor
|
|
252
|
+
case .radio: radioEditor
|
|
253
|
+
case .checkbox: checkboxEditor
|
|
254
|
+
case .slider: sliderEditor
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// STRING / NUMBER
|
|
259
|
+
@ViewBuilder
|
|
260
|
+
private var stringEditor: some View {
|
|
261
|
+
if let text = param.text {
|
|
262
|
+
Input(
|
|
263
|
+
text: text,
|
|
264
|
+
keyboardType: param.type == .number ? .numberPad : .default,
|
|
265
|
+
onChangeText: { _ in isUndefined = false }
|
|
266
|
+
)
|
|
267
|
+
if !param.required {
|
|
268
|
+
undefinedCheckbox { text.wrappedValue = "" }
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// OPTION / COLOR
|
|
274
|
+
@ViewBuilder
|
|
275
|
+
private var optionEditor: some View {
|
|
276
|
+
if let selection = param.selection {
|
|
277
|
+
HStack {
|
|
278
|
+
Spacer().frame(width: 30)
|
|
279
|
+
MomoText(selection.wrappedValue?.key ?? "", typography: .bodyDefaultRegular)
|
|
280
|
+
Spacer()
|
|
281
|
+
Icon(source: "arrow_chevron_down", size: 20)
|
|
282
|
+
.padding(.trailing, Spacing.XS)
|
|
283
|
+
}
|
|
284
|
+
.frame(height: 40)
|
|
285
|
+
.overlay(
|
|
286
|
+
RoundedRectangle(cornerRadius: Radius.XS)
|
|
287
|
+
.stroke(defaultTheme.colors.border.default, lineWidth: 1)
|
|
288
|
+
)
|
|
289
|
+
.contentShape(Rectangle())
|
|
290
|
+
.onTapGesture { openPicker(selection: selection) }
|
|
291
|
+
|
|
292
|
+
if !param.required {
|
|
293
|
+
// An OPTION/COLOR prop is genuinely nullable, so "undefined" clears it
|
|
294
|
+
// outright rather than restoring the seeded default.
|
|
295
|
+
undefinedCheckbox { selection.wrappedValue = nil }
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// RADIO
|
|
301
|
+
@ViewBuilder
|
|
302
|
+
private var radioEditor: some View {
|
|
303
|
+
if let selection = param.selection {
|
|
304
|
+
ForEach(param.list) { option in
|
|
305
|
+
Radio(
|
|
306
|
+
value: option.key,
|
|
307
|
+
groupValue: Binding(
|
|
308
|
+
get: { AnyHashable(selection.wrappedValue?.key ?? "") },
|
|
309
|
+
set: { _ in }
|
|
310
|
+
),
|
|
311
|
+
onChange: { isUndefined = false; selection.wrappedValue = option },
|
|
312
|
+
label: option.key
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
if !param.required {
|
|
316
|
+
undefinedCheckbox { selection.wrappedValue = initialSelection }
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// CHECKBOX
|
|
322
|
+
@ViewBuilder
|
|
323
|
+
private var checkboxEditor: some View {
|
|
324
|
+
if let flag = param.flag {
|
|
325
|
+
Checkbox(flag, title: flag.wrappedValue.description)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// SLIDER
|
|
330
|
+
@ViewBuilder
|
|
331
|
+
private var sliderEditor: some View {
|
|
332
|
+
if let number = param.number {
|
|
333
|
+
MomoText(formatSlider(number.wrappedValue), typography: .headerDefaultBold)
|
|
334
|
+
MomoSlider(
|
|
335
|
+
min: param.sliderMin,
|
|
336
|
+
max: param.sliderMax,
|
|
337
|
+
step: param.sliderStep,
|
|
338
|
+
low: number.wrappedValue,
|
|
339
|
+
disableRange: true,
|
|
340
|
+
onValueChanged: { low, _, _ in number.wrappedValue = low }
|
|
341
|
+
)
|
|
342
|
+
.frame(maxWidth: .infinity)
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// "is undefined" toggle for optional params. RADIO restores the value captured on
|
|
347
|
+
// first render (Compose's `remember { data.value }`) since its prop is never null.
|
|
348
|
+
@ViewBuilder
|
|
349
|
+
private func undefinedCheckbox(_ onUndefined: @escaping () -> Void) -> some View {
|
|
350
|
+
Checkbox($isUndefined, onChange: { if $0 { onUndefined() } }, title: "is undefined")
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private func openPicker(selection: Binding<PlaygroundOption?>) {
|
|
354
|
+
let isColor = param.type == .color
|
|
355
|
+
navigator.showBottomSheet({
|
|
356
|
+
PlaygroundOptionPicker(list: param.list, isColor: isColor) { picked in
|
|
357
|
+
isUndefined = false
|
|
358
|
+
selection.wrappedValue = picked
|
|
359
|
+
navigator.hideOverplay()
|
|
360
|
+
}
|
|
361
|
+
}, header: .title("Choose option"))
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private func formatSlider(_ v: Double) -> String {
|
|
365
|
+
v == v.rounded() ? String(Int(v)) : String(format: "%.1f", v)
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// MARK: - Option picker (searchable bottom-sheet list)
|
|
370
|
+
|
|
371
|
+
@available(iOS 16.0, *)
|
|
372
|
+
private struct PlaygroundOptionPicker: View {
|
|
373
|
+
let list: [PlaygroundOption]
|
|
374
|
+
let isColor: Bool
|
|
375
|
+
let onSelect: (PlaygroundOption) -> Void
|
|
376
|
+
|
|
377
|
+
@State private var query = ""
|
|
378
|
+
|
|
379
|
+
private var filtered: [PlaygroundOption] {
|
|
380
|
+
query.isEmpty ? list : list.filter { $0.key.localizedCaseInsensitiveContains(query) }
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
var body: some View {
|
|
384
|
+
VStack(spacing: 0) {
|
|
385
|
+
InputSearch(props: InputSearchProps(
|
|
386
|
+
text: $query,
|
|
387
|
+
placeholder: "Search input",
|
|
388
|
+
onChangeText: { query = $0.replacingOccurrences(of: "\n", with: " ") }
|
|
389
|
+
))
|
|
390
|
+
.padding(.horizontal, Spacing.L)
|
|
391
|
+
|
|
392
|
+
ScrollView {
|
|
393
|
+
LazyVStack(alignment: .leading, spacing: 0) {
|
|
394
|
+
ForEach(filtered) { item in
|
|
395
|
+
HStack(spacing: Spacing.S) {
|
|
396
|
+
if isColor, let color = item.value.base as? Color {
|
|
397
|
+
RoundedRectangle(cornerRadius: Radius.S)
|
|
398
|
+
.fill(color)
|
|
399
|
+
.frame(width: 36, height: 36)
|
|
400
|
+
.overlay(
|
|
401
|
+
RoundedRectangle(cornerRadius: Radius.S)
|
|
402
|
+
.stroke(defaultTheme.colors.border.default, lineWidth: 1)
|
|
403
|
+
)
|
|
404
|
+
} else if let iconName = item.value.base as? String {
|
|
405
|
+
Icon(source: iconName, size: 24)
|
|
406
|
+
}
|
|
407
|
+
MomoText(item.key, typography: .bodyDefaultRegular)
|
|
408
|
+
}
|
|
409
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
410
|
+
.padding(.vertical, Spacing.XS)
|
|
411
|
+
.contentShape(Rectangle())
|
|
412
|
+
.onTapGesture { onSelect(item) }
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
.padding(Spacing.L)
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
.frame(height: 600)
|
|
419
|
+
}
|
|
420
|
+
}
|
|
@@ -1,14 +1,56 @@
|
|
|
1
|
-
|
|
1
|
+
//
|
|
2
|
+
// DividerDemo.swift
|
|
3
|
+
// Example
|
|
4
|
+
//
|
|
5
|
+
// Preview + Playground demo for MomoDivider.
|
|
6
|
+
//
|
|
7
|
+
|
|
2
8
|
import MoMoUIKits
|
|
9
|
+
import SwiftUI
|
|
3
10
|
|
|
4
11
|
struct DividerDemo: View {
|
|
5
12
|
var body: some View {
|
|
6
|
-
|
|
13
|
+
if #available(iOS 16.0, *) {
|
|
14
|
+
DemoScreen(preview: { DividerPreview() }, playground: { DividerPlayground() })
|
|
15
|
+
} else {
|
|
16
|
+
Text("Requires iOS 16+").padding()
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// MARK: - Preview
|
|
22
|
+
|
|
23
|
+
private struct DividerPreview: View {
|
|
24
|
+
var body: some View {
|
|
25
|
+
BasicColumnBox("Between items", alignment: .leading) {
|
|
7
26
|
ForEach(0..<5, id: \.self) { index in
|
|
8
|
-
|
|
27
|
+
MomoText("Item \(index)", typography: .bodyDefaultRegular)
|
|
9
28
|
MomoDivider()
|
|
10
29
|
}
|
|
11
30
|
}
|
|
12
|
-
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// MARK: - Playground
|
|
35
|
+
|
|
36
|
+
// MomoDivider takes no props; the playground varies how many are rendered.
|
|
37
|
+
@available(iOS 16.0, *)
|
|
38
|
+
private struct DividerPlayground: View {
|
|
39
|
+
@State private var count: Double = 3
|
|
40
|
+
|
|
41
|
+
var body: some View {
|
|
42
|
+
PlaygroundTab(
|
|
43
|
+
component: {
|
|
44
|
+
VStack(alignment: .leading, spacing: Spacing.S) {
|
|
45
|
+
ForEach(0..<Int(count), id: \.self) { index in
|
|
46
|
+
MomoText("Item \(index)", typography: .bodyDefaultRegular)
|
|
47
|
+
MomoDivider()
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
params: [
|
|
52
|
+
.slider("Count", $count, min: 1, max: 6, step: 1),
|
|
53
|
+
]
|
|
54
|
+
)
|
|
13
55
|
}
|
|
14
56
|
}
|