@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.
Files changed (90) hide show
  1. package/CLAUDE.md +19 -0
  2. package/ios/Application/ApplicationEnvironment.swift +14 -6
  3. package/ios/Application/LanguageSource.swift +93 -0
  4. package/ios/Application/Localize.swift +73 -4
  5. package/ios/Application/LocalizeModifier.swift +30 -0
  6. package/ios/Application/Navigation/NavigationContainer.swift +28 -18
  7. package/ios/Application/Navigation/Navigator.swift +121 -53
  8. package/ios/Application/Navigation/NavigatorManager.swift +49 -0
  9. package/ios/Application/Navigation/component/NavigationFooter.swift +90 -0
  10. package/ios/Application/StackScreen.swift +19 -28
  11. package/ios/Badge/Badge.swift +2 -1
  12. package/ios/Badge/BadgeRibbon.swift +6 -2
  13. package/ios/Button/Button.swift +41 -61
  14. package/ios/Chip/Chip.swift +6 -5
  15. package/ios/Icon/Icon.swift +13 -4
  16. package/ios/Input/ErrorView.swift +2 -1
  17. package/ios/Input/Input.swift +9 -2
  18. package/ios/Input/InputPhoneNumber.swift +2 -1
  19. package/ios/Input/InputSearch.swift +3 -2
  20. package/ios/Input/InputTextArea.swift +6 -1
  21. package/ios/InputMoney/InputMoney.swift +5 -2
  22. package/ios/InputOTP/InputOTP.swift +2 -1
  23. package/ios/Layout/GridContext.swift +30 -0
  24. package/ios/Layout/Item.swift +42 -0
  25. package/ios/Layout/Section.swift +134 -0
  26. package/ios/Popup/PopupNotify.swift +2 -1
  27. package/ios/ProgressInfo/ProgressInfo.swift +4 -3
  28. package/ios/Rating/Rating.swift +2 -1
  29. package/ios/Stepper/Stepper.swift +2 -1
  30. package/ios/SuggestAction/SuggestAction.swift +3 -2
  31. package/ios/Tag/Tag.swift +2 -1
  32. package/ios/Template/TrustBanner/TrustBanner.swift +4 -2
  33. package/ios/Title/Title.swift +3 -2
  34. package/ios/Tooltip/Tooltip.swift +2 -1
  35. package/ios/Typography/FontScale.swift +103 -0
  36. package/ios/Typography/FontScaleStore.swift +52 -13
  37. package/ios/Typography/Text.swift +12 -29
  38. package/ios/Typography/Typography.swift +66 -86
  39. package/ios/native-kits.podspec +2 -1
  40. package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
  41. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/AvatarDemo.swift +87 -73
  42. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/BadgeDemo.swift +55 -249
  43. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ButtonDemo.swift +90 -82
  44. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/CarouselDemo.swift +74 -104
  45. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/CheckboxDemo.swift +47 -50
  46. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ChipDemo.swift +63 -7
  47. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/CollapseDemo.swift +90 -81
  48. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DateTimePickerDemo.swift +73 -97
  49. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DemoScreen.swift +420 -0
  50. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DividerDemo.swift +46 -4
  51. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +54 -57
  52. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IconButtonDemo.swift +80 -45
  53. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IconDemo.swift +81 -0
  54. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ImageDemo.swift +49 -62
  55. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InformationDemo.swift +66 -116
  56. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputDemo.swift +104 -98
  57. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputDropDownDemo.swift +76 -118
  58. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputMoneyDemo.swift +73 -86
  59. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputOTPDemo.swift +69 -54
  60. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputPhoneNumberDemo.swift +74 -54
  61. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputSearchDemo.swift +73 -20
  62. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputTextAreaDemo.swift +68 -38
  63. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/LoaderDemo.swift +55 -52
  64. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/LocalizeDemo.swift +51 -4
  65. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +14 -2
  66. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PaginationDemo.swift +47 -51
  67. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PlaygroundData.swift +76 -0
  68. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupNotifyDemo.swift +67 -93
  69. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupPromotionDemo.swift +34 -43
  70. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ProgressInfoDemo.swift +83 -71
  71. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/RadioDemo.swift +55 -23
  72. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/RatingDemo.swift +58 -46
  73. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SectionDemo.swift +83 -0
  74. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SkeletonDemo.swift +41 -12
  75. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SliderDemo.swift +67 -127
  76. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SnackBarDemo.swift +54 -110
  77. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/StepperDemo.swift +69 -79
  78. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/StepsDemo.swift +78 -58
  79. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SuggestActionDemo.swift +76 -65
  80. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SwipeDemo.swift +82 -127
  81. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SwitchDemo.swift +44 -19
  82. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TabViewDemo.swift +92 -168
  83. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TagDemo.swift +79 -27
  84. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ThemeDemo.swift +0 -1
  85. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TitleDemo.swift +87 -92
  86. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TooltipDemo.swift +94 -163
  87. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TypographyDemo.swift +70 -70
  88. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/UploaderDemo.swift +68 -114
  89. package/ios-demo/Sources/MoMoUIKitsDemo/SwiftUIDemoBrowserView.swift +3 -1
  90. 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
- ScrollView {
23
- VStack(alignment: .leading, spacing: 24) {
24
- section("Format: DD-MM-YYYY") {
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
- // MARK: - Helpers
91
-
92
- @ViewBuilder
93
- private func selectedLabel(_ date: Date?, format: String) -> some View {
94
- if let date = date {
95
- MomoText(
96
- "Selected: \(DateTimePickerUtils.formatDate(date, format: format))",
97
- typography: .descriptionDefaultRegular,
98
- color: Colors.textSecondary
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
- .padding(.top, 8)
50
+ selectedLabel(selected4, format: "dd-MM-yyyy")
101
51
  }
102
52
  }
103
53
 
104
54
  @ViewBuilder
105
- private func section<Content: View>(
106
- _ title: String,
107
- @ViewBuilder content: () -> Content
108
- ) -> some View {
109
- VStack(alignment: .leading, spacing: 12) {
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
- #if DEBUG
119
- struct DateTimePickerDemo_Previews: PreviewProvider {
120
- static var previews: some View {
121
- DateTimePickerDemo()
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
- import SwiftUI
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
- VStack(alignment: .leading, spacing: 8) {
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
- Text("Item\(index)")
27
+ MomoText("Item \(index)", typography: .bodyDefaultRegular)
9
28
  MomoDivider()
10
29
  }
11
30
  }
12
- .padding()
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
  }