@onekeyfe/react-native-native-list 6.7.0-alpha.375 → 6.7.0-alpha.382
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/android/src/main/java/com/onekey/nativelist/HybridNativeList.kt +14 -1
- package/android/src/main/java/com/onekey/nativelist/NativeListAccessoryStack.kt +240 -11
- package/android/src/main/java/com/onekey/nativelist/NativeListActivityDetailsView.kt +7 -4
- package/android/src/main/java/com/onekey/nativelist/NativeListActivityRenderer.kt +6 -3
- package/android/src/main/java/com/onekey/nativelist/NativeListButtonGroupRenderer.kt +102 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListContainerSlotAdapter.kt +12 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListDataRowRenderer.kt +87 -7
- package/android/src/main/java/com/onekey/nativelist/NativeListIdentityRenderer.kt +177 -1
- package/android/src/main/java/com/onekey/nativelist/NativeListLeadingVisual.kt +43 -14
- package/android/src/main/java/com/onekey/nativelist/NativeListMessageRenderer.kt +25 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListModels.kt +33 -2
- package/android/src/main/java/com/onekey/nativelist/NativeListPrimitives.kt +22 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListRendererRegistry.kt +2 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListRendererRowView.kt +2 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListResolvedText.kt +3 -2
- package/android/src/main/java/com/onekey/nativelist/NativeListSectionHeaderRenderer.kt +69 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListTextView.kt +55 -1
- package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +1912 -107
- package/android/src/test/java/com/margelo/nitro/nativelist/NativeListPressPolicyTest.kt +1 -0
- package/docs/ROW_TEMPLATES.md +60 -9
- package/docs/SPEC.md +450 -5
- package/docs/STYLE_SPEC.md +6 -0
- package/ios/HybridNativeList.swift +21 -2
- package/ios/NativeListAccessoryStack.swift +227 -39
- package/ios/NativeListActivityDetailsView.swift +18 -8
- package/ios/NativeListActivityRenderer.swift +8 -2
- package/ios/NativeListButtonGroupRenderer.swift +89 -0
- package/ios/NativeListDataRowRenderer.swift +155 -27
- package/ios/NativeListDesignAssets.swift +3 -0
- package/ios/NativeListIdentityRenderer.swift +166 -30
- package/ios/NativeListLeadingVisual.swift +49 -14
- package/ios/NativeListMessageRenderer.swift +34 -1
- package/ios/NativeListModels.swift +42 -2
- package/ios/NativeListRendererRegistry.swift +1 -0
- package/ios/NativeListResolvedText.swift +2 -1
- package/ios/NativeListRowHost.swift +14 -0
- package/ios/NativeListSectionHeaderRenderer.swift +88 -0
- package/ios/RNCNativeListView.swift +1013 -55
- package/ios/Resources/NativeListIcons.xcassets/onekey_crypto_coin_outline.imageset/Contents.json +15 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_crypto_coin_outline.imageset/icon.svg +4 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_gas_solid.imageset/Contents.json +15 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_gas_solid.imageset/icon.svg +3 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_refresh_ccw_outline.imageset/Contents.json +15 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_refresh_ccw_outline.imageset/icon.svg +3 -0
- package/ios/tests/NativeListActivityPresentationTests.swift +56 -0
- package/ios/tests/NativeListLifecycleTests.swift +2720 -3
- package/lib/module/NativeList.js +175 -28
- package/lib/module/NativeList.web.js +129 -30
- package/lib/module/index.js +1 -1
- package/lib/module/models.js +1 -0
- package/lib/module/selection.js +1 -1
- package/lib/module/validation.js +421 -8
- package/lib/module/web/NativeListWebEngine.js +507 -19
- package/lib/module/web/templates/ActivityRowRenderer.js +8 -7
- package/lib/module/web/templates/ButtonGroupRowRenderer.js +47 -0
- package/lib/module/web/templates/DataRowRenderer.js +40 -5
- package/lib/module/web/templates/IdentityRowRenderer.js +90 -4
- package/lib/module/web/templates/MessageRowRenderer.js +18 -2
- package/lib/module/web/templates/RowContainerStyle.js +1 -0
- package/lib/module/web/templates/RowRendererRegistry.js +3 -0
- package/lib/module/web/templates/SectionHeaderRowRenderer.js +36 -2
- package/lib/nitrogen/generated/android/c++/JHybridNativeListSpec.cpp +26 -0
- package/lib/nitrogen/generated/android/c++/JHybridNativeListSpec.hpp +4 -0
- package/lib/nitrogen/generated/android/c++/views/JHybridNativeListStateUpdater.cpp +10 -0
- package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/HybridNativeListSpec.kt +20 -0
- package/lib/nitrogen/generated/ios/c++/HybridNativeListSpecSwift.hpp +14 -0
- package/lib/nitrogen/generated/ios/c++/views/HybridNativeListComponent.mm +12 -0
- package/lib/nitrogen/generated/ios/swift/HybridNativeListSpec.swift +2 -0
- package/lib/nitrogen/generated/ios/swift/HybridNativeListSpec_cxx.swift +43 -0
- package/lib/nitrogen/generated/shared/c++/HybridNativeListSpec.cpp +4 -0
- package/lib/nitrogen/generated/shared/c++/HybridNativeListSpec.hpp +4 -0
- package/lib/nitrogen/generated/shared/c++/views/HybridNativeListComponent.cpp +4 -0
- package/lib/nitrogen/generated/shared/c++/views/HybridNativeListComponent.hpp +6 -0
- package/lib/nitrogen/generated/shared/json/NativeListConfig.json +2 -0
- package/lib/typescript/src/NativeList.d.ts +1 -1
- package/lib/typescript/src/NativeList.nitro.d.ts +2 -0
- package/lib/typescript/src/NativeList.types.d.ts +18 -0
- package/lib/typescript/src/index.d.ts +2 -2
- package/lib/typescript/src/models.d.ts +105 -9
- package/lib/typescript/src/validation.d.ts +12 -0
- package/lib/typescript/src/web/NativeListWebEngine.d.ts +32 -0
- package/lib/typescript/src/web/templates/ButtonGroupRowRenderer.d.ts +14 -0
- package/lib/typescript/src/web/templates/IdentityRowRenderer.d.ts +1 -1
- package/lib/typescript/src/web/templates/MessageRowRenderer.d.ts +9 -3
- package/nitrogen/generated/android/c++/JHybridNativeListSpec.cpp +26 -0
- package/nitrogen/generated/android/c++/JHybridNativeListSpec.hpp +4 -0
- package/nitrogen/generated/android/c++/views/JHybridNativeListStateUpdater.cpp +10 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/HybridNativeListSpec.kt +20 -0
- package/nitrogen/generated/ios/c++/HybridNativeListSpecSwift.hpp +14 -0
- package/nitrogen/generated/ios/c++/views/HybridNativeListComponent.mm +12 -0
- package/nitrogen/generated/ios/swift/HybridNativeListSpec.swift +2 -0
- package/nitrogen/generated/ios/swift/HybridNativeListSpec_cxx.swift +43 -0
- package/nitrogen/generated/shared/c++/HybridNativeListSpec.cpp +4 -0
- package/nitrogen/generated/shared/c++/HybridNativeListSpec.hpp +4 -0
- package/nitrogen/generated/shared/c++/views/HybridNativeListComponent.cpp +4 -0
- package/nitrogen/generated/shared/c++/views/HybridNativeListComponent.hpp +6 -0
- package/nitrogen/generated/shared/json/NativeListConfig.json +2 -0
- package/package.json +3 -3
- package/react-native-native-list.podspec +1 -0
- package/src/NativeList.nitro.ts +2 -0
- package/src/NativeList.tsx +313 -44
- package/src/NativeList.types.ts +22 -0
- package/src/NativeList.web.tsx +204 -48
- package/src/index.ts +2 -0
- package/src/models.ts +117 -3
- package/src/selection.ts +1 -0
- package/src/validation.ts +852 -6
- package/src/web/NativeListWebEngine.ts +803 -20
- package/src/web/templates/ActivityRowRenderer.ts +14 -6
- package/src/web/templates/ButtonGroupRowRenderer.ts +58 -0
- package/src/web/templates/DataRowRenderer.ts +93 -20
- package/src/web/templates/IdentityRowRenderer.ts +150 -5
- package/src/web/templates/MessageRowRenderer.ts +23 -7
- package/src/web/templates/RowContainerStyle.ts +2 -0
- package/src/web/templates/RowRendererRegistry.ts +11 -1
- package/src/web/templates/SectionHeaderRowRenderer.ts +47 -4
|
@@ -21,6 +21,7 @@ final class NativeListLeadingVisual: UIView {
|
|
|
21
21
|
private let cornerBackdrop = UIView()
|
|
22
22
|
private var slots: [NativeListImageSlot] = []
|
|
23
23
|
private var overlaySlots: [NativeListImageSlot] = []
|
|
24
|
+
private var overlayFallbackIcons: [UIImageView] = []
|
|
24
25
|
private var overlays: [(UIView, [String: Any])] = []
|
|
25
26
|
private let dashedBorder = CAShapeLayer()
|
|
26
27
|
private var visual: [String: Any] = [:]
|
|
@@ -87,6 +88,7 @@ final class NativeListLeadingVisual: UIView {
|
|
|
87
88
|
}
|
|
88
89
|
let placeholder = theme?.string("strongBackground", default: "#0000000F") ?? "#0000000F"
|
|
89
90
|
let fallbackIcon = visual.dictionary("fallbackIcon")
|
|
91
|
+
let fallbackBackground = fallbackIcon?.string("backgroundColor", default: placeholder) ?? placeholder
|
|
90
92
|
let sourceFallback =
|
|
91
93
|
kind != "icon" && !sources.isEmpty
|
|
92
94
|
&& sources[0].0.string("loadingStrategy", default: "none") != "none"
|
|
@@ -95,7 +97,8 @@ final class NativeListLeadingVisual: UIView {
|
|
|
95
97
|
let restored = fallbackKey.map(NativeListSourceFallbackState.has) ?? false
|
|
96
98
|
let fallbackData = kind == "icon" ? visual : fallbackIcon
|
|
97
99
|
fallback.text = String(visual.string("fallbackText").prefix(2))
|
|
98
|
-
fallback.textColor = UIColor(
|
|
100
|
+
fallback.textColor = UIColor(
|
|
101
|
+
nativeListHex: visual.string("fallbackTextColor", default: "#8D8D8D"), fallback: .gray)
|
|
99
102
|
icon.image = fallbackData.map { nativeListIcon(named: $0.string("name")) } ?? nil
|
|
100
103
|
icon.tintColor = UIColor(
|
|
101
104
|
nativeListHex: fallbackData?.string("tintColor", default: "#646464") ?? "#646464",
|
|
@@ -106,7 +109,9 @@ final class NativeListLeadingVisual: UIView {
|
|
|
106
109
|
nativeListHex: visual.string(
|
|
107
110
|
"backgroundColor", default: sources.isEmpty ? placeholder : "#00000000"), fallback: .clear)
|
|
108
111
|
backgroundColor =
|
|
109
|
-
|
|
112
|
+
sources.isEmpty && fallbackIcon?["backgroundColor"] != nil || restored
|
|
113
|
+
? UIColor(nativeListHex: fallbackBackground, fallback: .clear)
|
|
114
|
+
: sourceFallback ? UIColor(nativeListHex: placeholder, fallback: .clear) : visualBackground
|
|
110
115
|
layer.borderWidth = kind == "icon" ? 1 / UIScreen.main.scale : 0
|
|
111
116
|
layer.borderColor = UIColor(nativeListHex: "#0000001F", fallback: .clear).cgColor
|
|
112
117
|
if bitmapBorderWidth > 0 {
|
|
@@ -121,7 +126,7 @@ final class NativeListLeadingVisual: UIView {
|
|
|
121
126
|
continue
|
|
122
127
|
}
|
|
123
128
|
let source = sources[index]
|
|
124
|
-
slot.view.isHidden = index > 0 ||
|
|
129
|
+
slot.view.isHidden = index > 0 || restored
|
|
125
130
|
slot.bind(
|
|
126
131
|
source.0, key: "\(key):leading:\(index)", variant: source.1,
|
|
127
132
|
fit: index == 0 ? style["contentFit"] as? String : nil, placeholder: placeholder
|
|
@@ -136,7 +141,7 @@ final class NativeListLeadingVisual: UIView {
|
|
|
136
141
|
}
|
|
137
142
|
}
|
|
138
143
|
self.backgroundColor =
|
|
139
|
-
loaded ? visualBackground : UIColor(nativeListHex:
|
|
144
|
+
loaded ? visualBackground : UIColor(nativeListHex: fallbackBackground, fallback: .clear)
|
|
140
145
|
self.fallback.isHidden = loaded || fallbackIcon != nil
|
|
141
146
|
self.icon.isHidden = loaded || fallbackIcon == nil
|
|
142
147
|
}
|
|
@@ -158,8 +163,17 @@ final class NativeListLeadingVisual: UIView {
|
|
|
158
163
|
overlays.removeAll(keepingCapacity: true)
|
|
159
164
|
let descriptors = Array(visual.dictionaries("overlays").prefix(2))
|
|
160
165
|
while overlaySlots.count < descriptors.count { overlaySlots.append(NativeListImageSlot()) }
|
|
166
|
+
while overlayFallbackIcons.count < descriptors.count { overlayFallbackIcons.append(UIImageView()) }
|
|
161
167
|
for (index, data) in descriptors.enumerated() {
|
|
162
168
|
let frame = index < previousOverlays.count ? previousOverlays[index].0 : UIView()
|
|
169
|
+
let fallbackData = data.dictionary("fallbackIcon")
|
|
170
|
+
let fallbackGlyph = overlayFallbackIcons[index]
|
|
171
|
+
fallbackGlyph.image = fallbackData.flatMap { nativeListIcon(named: $0.string("name")) }
|
|
172
|
+
fallbackGlyph.tintColor = UIColor(
|
|
173
|
+
nativeListHex: fallbackData?.string("tintColor", default: "#646464") ?? "#646464",
|
|
174
|
+
fallback: .darkGray)
|
|
175
|
+
fallbackGlyph.contentMode = .scaleAspectFit
|
|
176
|
+
fallbackGlyph.isHidden = true
|
|
163
177
|
frame.clipsToBounds = true
|
|
164
178
|
frame.backgroundColor = UIColor(
|
|
165
179
|
nativeListHex: data.string(
|
|
@@ -172,7 +186,9 @@ final class NativeListLeadingVisual: UIView {
|
|
|
172
186
|
child = slot.view
|
|
173
187
|
child.isHidden = true
|
|
174
188
|
slot.bind(image, key: "\(key):overlay:\(index)", placeholder: placeholder) {
|
|
175
|
-
[weak child] loaded in
|
|
189
|
+
[weak child, weak fallbackGlyph] loaded in
|
|
190
|
+
child?.isHidden = !loaded
|
|
191
|
+
fallbackGlyph?.isHidden = loaded || fallbackData == nil
|
|
176
192
|
}
|
|
177
193
|
} else if !data.string("text").isEmpty {
|
|
178
194
|
overlaySlots[index].recycle()
|
|
@@ -181,7 +197,10 @@ final class NativeListLeadingVisual: UIView {
|
|
|
181
197
|
label.textAlignment = .center
|
|
182
198
|
// A line-height box is the legacy wallet text badge: regular weight, centered metrics.
|
|
183
199
|
label.font = nativeListFont(
|
|
184
|
-
ofSize: overlayTextFontSize,
|
|
200
|
+
ofSize: overlayTextFontSize,
|
|
201
|
+
weight: NativeListTextStyles.marketFontWeight(
|
|
202
|
+
data.string("fontWeight"),
|
|
203
|
+
fallback: overlayTextLineHeight == nil ? .medium : .regular),
|
|
185
204
|
fonts: fonts)
|
|
186
205
|
if let line = overlayTextLineHeight {
|
|
187
206
|
let paragraph = NSMutableParagraphStyle()
|
|
@@ -207,10 +226,9 @@ final class NativeListLeadingVisual: UIView {
|
|
|
207
226
|
nativeListHex: data.string("tintColor", default: "#646464"), fallback: .darkGray)
|
|
208
227
|
child = glyph
|
|
209
228
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
229
|
+
frame.subviews.forEach { $0.removeFromSuperview() }
|
|
230
|
+
frame.addSubview(child)
|
|
231
|
+
if data.dictionary("image") != nil && fallbackData != nil { frame.addSubview(fallbackGlyph) }
|
|
214
232
|
if frame.superview == nil { addSubview(frame) }
|
|
215
233
|
overlays.append((frame, data))
|
|
216
234
|
}
|
|
@@ -253,10 +271,16 @@ final class NativeListLeadingVisual: UIView {
|
|
|
253
271
|
}
|
|
254
272
|
fallback.frame = bounds
|
|
255
273
|
let fallbackIcon = visual.dictionary("fallbackIcon")
|
|
274
|
+
let explicitFallbackSize: CGFloat? = fallbackIcon.flatMap { data in
|
|
275
|
+
guard data["size"] != nil else { return nil }
|
|
276
|
+
let size = data.double("size")
|
|
277
|
+
return size > 0 ? CGFloat(size) : nil
|
|
278
|
+
}
|
|
256
279
|
let iconSize: CGFloat =
|
|
257
|
-
|
|
280
|
+
explicitFallbackSize
|
|
281
|
+
?? (!sources.isEmpty && fallbackIcon != nil
|
|
258
282
|
? min(bounds.width, bounds.height)
|
|
259
|
-
* (fallbackIcon?.string("name") == "GlobusOutline" ? 1.2 : 1) : glyphSize
|
|
283
|
+
* (fallbackIcon?.string("name") == "GlobusOutline" ? 1.2 : 1) : glyphSize)
|
|
260
284
|
icon.frame = CGRect(
|
|
261
285
|
x: (bounds.width - iconSize) / 2, y: (bounds.height - iconSize) / 2, width: iconSize,
|
|
262
286
|
height: iconSize)
|
|
@@ -306,7 +330,8 @@ final class NativeListLeadingVisual: UIView {
|
|
|
306
330
|
cornerBackdrop.frame = networkBackdrop.frame
|
|
307
331
|
corner.frame = cornerBackdrop.frame.insetBy(dx: 1, dy: 1)
|
|
308
332
|
unread.frame = logical(CGRect(x: bounds.width - 8, y: 0, width: 8, height: 8))
|
|
309
|
-
for (
|
|
333
|
+
for (index, overlay) in overlays.enumerated() {
|
|
334
|
+
let (frame, data) = overlay
|
|
310
335
|
let size = CGFloat(data.double("size", default: 20))
|
|
311
336
|
// Legacy wallet text badges size to their text (tabular 12pt + 4) at the line height.
|
|
312
337
|
let textBadgeLine =
|
|
@@ -331,9 +356,19 @@ final class NativeListLeadingVisual: UIView {
|
|
|
331
356
|
height: height))
|
|
332
357
|
frame.layer.cornerRadius = min(width, height) / 2
|
|
333
358
|
let inset = CGFloat(data.double("padding", default: 0))
|
|
334
|
-
|
|
359
|
+
let contentFrame = frame.bounds.insetBy(
|
|
335
360
|
dx: data["text"] != nil ? overlayTextHorizontalInset ?? inset : inset,
|
|
336
361
|
dy: data["text"] != nil && overlayTextHorizontalInset != nil ? 0 : inset)
|
|
362
|
+
frame.subviews.forEach { $0.frame = contentFrame }
|
|
363
|
+
let slot = overlaySlots[index].view
|
|
364
|
+
if data.dictionary("image") != nil {
|
|
365
|
+
let circularImage = inset > 0 && width == height
|
|
366
|
+
slot.layer.cornerRadius = circularImage ? min(contentFrame.width, contentFrame.height) / 2 : 0
|
|
367
|
+
slot.clipsToBounds = circularImage
|
|
368
|
+
} else {
|
|
369
|
+
slot.layer.cornerRadius = 0
|
|
370
|
+
slot.clipsToBounds = false
|
|
371
|
+
}
|
|
337
372
|
}
|
|
338
373
|
dashedBorder.isHidden = visual.string("borderStyle") != "dashed"
|
|
339
374
|
dashedBorder.strokeColor =
|
|
@@ -29,7 +29,9 @@ enum NativeListMessageRenderer {
|
|
|
29
29
|
body = NativeListResolvedText(
|
|
30
30
|
item.data.string("body"), style: style.dictionary("body"), size: 14,
|
|
31
31
|
color: nativeListColor(theme, "secondaryText", "#646464"), lineHeight: 20,
|
|
32
|
-
lines: item.data.int("bodyLines", default: 3),
|
|
32
|
+
lines: item.data.int("bodyLines", default: 3),
|
|
33
|
+
breakMode: item.data.int("bodyLines", default: 3) == 0 ? .byWordWrapping : .byTruncatingTail,
|
|
34
|
+
fonts: fonts)
|
|
33
35
|
time = NativeListResolvedText(
|
|
34
36
|
item.data.string("time"), style: style.dictionary("time"), size: 12,
|
|
35
37
|
color: nativeListColor(theme, "disabledText", "#8D8D8D"), lineHeight: 16, lines: 1,
|
|
@@ -150,6 +152,11 @@ enum NativeListMessageRenderer {
|
|
|
150
152
|
}
|
|
151
153
|
|
|
152
154
|
final class NativeListMessageCell: NativeListRendererCell {
|
|
155
|
+
private let topWarningBorder = CALayer()
|
|
156
|
+
private let bottomWarningBorder = CALayer()
|
|
157
|
+
private var warningBorderWidth: CGFloat = 0
|
|
158
|
+
private var warningBorderColor = UIColor.clear
|
|
159
|
+
private var isInlineWarning = false
|
|
153
160
|
override class func measure(
|
|
154
161
|
_ item: NativeListItem, width: CGFloat, theme: [String: Any]?, layout: String,
|
|
155
162
|
fonts: NativeListFontFamilies
|
|
@@ -167,6 +174,10 @@ final class NativeListMessageCell: NativeListRendererCell {
|
|
|
167
174
|
_ item: NativeListItem, theme: [String: Any]?, layout: String,
|
|
168
175
|
checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
|
|
169
176
|
) {
|
|
177
|
+
let container = item.data.dictionary("style")?.dictionary("container") ?? [:]
|
|
178
|
+
isInlineWarning = item.data.string("presentation") == "inlineWarning"
|
|
179
|
+
warningBorderWidth = isInlineWarning ? CGFloat(container.double("borderWidth")) : 0
|
|
180
|
+
warningBorderColor = UIColor(nativeListHex: container.string("borderColor"), fallback: .clear)
|
|
170
181
|
let resolved = NativeListMessageRenderer.Resolved(
|
|
171
182
|
item, theme: theme, layout: layout, fonts: fonts)
|
|
172
183
|
views.bind(resolved, item: item, theme: theme, fonts: fonts)
|
|
@@ -174,5 +185,27 @@ final class NativeListMessageCell: NativeListRendererCell {
|
|
|
174
185
|
top: resolved.verticalPadding, left: resolved.horizontalPadding,
|
|
175
186
|
bottom: resolved.verticalPadding, right: resolved.horizontalPadding)
|
|
176
187
|
}
|
|
188
|
+
override func applyAppearance() {
|
|
189
|
+
super.applyAppearance()
|
|
190
|
+
if isInlineWarning {
|
|
191
|
+
layer.cornerRadius = 0
|
|
192
|
+
contentView.layer.cornerRadius = 0
|
|
193
|
+
contentView.layer.borderWidth = 0
|
|
194
|
+
topWarningBorder.backgroundColor = warningBorderColor.cgColor
|
|
195
|
+
bottomWarningBorder.backgroundColor = warningBorderColor.cgColor
|
|
196
|
+
if topWarningBorder.superlayer == nil { contentView.layer.addSublayer(topWarningBorder) }
|
|
197
|
+
if bottomWarningBorder.superlayer == nil { contentView.layer.addSublayer(bottomWarningBorder) }
|
|
198
|
+
} else {
|
|
199
|
+
topWarningBorder.removeFromSuperlayer()
|
|
200
|
+
bottomWarningBorder.removeFromSuperlayer()
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
override func layoutSubviews() {
|
|
204
|
+
super.layoutSubviews()
|
|
205
|
+
let height = max(0, warningBorderWidth)
|
|
206
|
+
topWarningBorder.frame = CGRect(x: 0, y: 0, width: contentView.bounds.width, height: height)
|
|
207
|
+
bottomWarningBorder.frame = CGRect(
|
|
208
|
+
x: 0, y: contentView.bounds.height - height, width: contentView.bounds.width, height: height)
|
|
209
|
+
}
|
|
177
210
|
override func recycleContent() { views.recycle() }
|
|
178
211
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import Foundation
|
|
2
|
+
import CoreFoundation
|
|
2
3
|
import UIKit
|
|
3
4
|
|
|
4
5
|
// Template type alone partitions reuse; data keys and styles never select a renderer.
|
|
5
6
|
enum NativeListRendererKey: String, CaseIterable {
|
|
6
|
-
case message, rail, mediaTile, action, system, activity, dataRow, metricCard, market, identity, sectionHeader, walletGroup
|
|
7
|
+
case message, rail, mediaTile, action, buttonGroup, system, activity, dataRow, metricCard, market, identity, sectionHeader, walletGroup
|
|
7
8
|
/// Not a template: legacy accepted any non-empty type and drew an empty row with the
|
|
8
9
|
/// shared chrome. Unknown types keep that placeholder instead of failing the snapshot.
|
|
9
10
|
case unsupported = "__nativeListUnsupported"
|
|
@@ -35,7 +36,7 @@ struct NativeListItem {
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
var isRowPressEnabled: Bool {
|
|
38
|
-
!data.bool("disabled") &&
|
|
39
|
+
type != "buttonGroup" && !data.bool("disabled") &&
|
|
39
40
|
!data.bool("pressDisabled") &&
|
|
40
41
|
(type != "system" || data.string("variant") == "retry")
|
|
41
42
|
}
|
|
@@ -78,7 +79,18 @@ struct NativeListItem {
|
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
struct NativeListConfig {
|
|
82
|
+
struct StructuralTransition {
|
|
83
|
+
let id: String
|
|
84
|
+
let duration: TimeInterval
|
|
85
|
+
let anchorKey: String?
|
|
86
|
+
}
|
|
87
|
+
struct Disclosure {
|
|
88
|
+
let expanded: Bool
|
|
89
|
+
let expandedTrailingInset: CGFloat
|
|
90
|
+
}
|
|
81
91
|
let generation: Int
|
|
92
|
+
let structuralTransition: StructuralTransition?
|
|
93
|
+
let disclosure: Disclosure?
|
|
82
94
|
let layout: String
|
|
83
95
|
let orientation: String
|
|
84
96
|
let gridColumns: Int
|
|
@@ -129,6 +141,32 @@ struct NativeListConfig {
|
|
|
129
141
|
let capabilities = root["capabilities"] as? [String: Any]
|
|
130
142
|
let sectionIndex = capabilities?["sectionIndex"] as? [String: Any]
|
|
131
143
|
let footer = try (root["fixedFooter"] as? [String: Any]).map(NativeListItem.init)
|
|
144
|
+
let transition = root["structuralTransition"] as? [String: Any]
|
|
145
|
+
let structuralTransition = (transition?["id"] as? String).map { id in
|
|
146
|
+
StructuralTransition(
|
|
147
|
+
id: id,
|
|
148
|
+
duration: min(2, max(0, (transition?["durationMs"] as? Double ?? 220) / 1000)),
|
|
149
|
+
anchorKey: transition?["anchorKey"] as? String)
|
|
150
|
+
}
|
|
151
|
+
let disclosure: Disclosure?
|
|
152
|
+
if let value = root["disclosure"] {
|
|
153
|
+
guard let data = value as? [String: Any],
|
|
154
|
+
let expandedNumber = data["expanded"] as? NSNumber,
|
|
155
|
+
CFGetTypeID(expandedNumber) == CFBooleanGetTypeID() else {
|
|
156
|
+
throw NativeListModelError.invalidSnapshot
|
|
157
|
+
}
|
|
158
|
+
let insetNumber = data["expandedTrailingInset"] as? NSNumber
|
|
159
|
+
guard data["expandedTrailingInset"] == nil || insetNumber != nil,
|
|
160
|
+
insetNumber.map({ CFGetTypeID($0) != CFBooleanGetTypeID() }) ?? true else {
|
|
161
|
+
throw NativeListModelError.invalidSnapshot
|
|
162
|
+
}
|
|
163
|
+
let inset = insetNumber?.doubleValue ?? 0
|
|
164
|
+
guard inset.isFinite, inset >= 0 else { throw NativeListModelError.invalidSnapshot }
|
|
165
|
+
disclosure = Disclosure(expanded: expandedNumber.boolValue,
|
|
166
|
+
expandedTrailingInset: CGFloat(inset))
|
|
167
|
+
} else {
|
|
168
|
+
disclosure = nil
|
|
169
|
+
}
|
|
132
170
|
|
|
133
171
|
let contentPadding = max(0, CGFloat(layoutData.double("contentPadding")))
|
|
134
172
|
let contentPaddingHorizontal = layoutData["contentPaddingHorizontal"] == nil
|
|
@@ -143,6 +181,8 @@ struct NativeListConfig {
|
|
|
143
181
|
|
|
144
182
|
return NativeListConfig(
|
|
145
183
|
generation: generation,
|
|
184
|
+
structuralTransition: structuralTransition,
|
|
185
|
+
disclosure: disclosure,
|
|
146
186
|
layout: layout,
|
|
147
187
|
orientation: layoutData["orientation"] as? String ?? "vertical",
|
|
148
188
|
gridColumns: min(7, max(1, layoutData["gridColumns"] as? Int ?? 1)),
|
|
@@ -18,6 +18,7 @@ enum NativeListRendererRegistry {
|
|
|
18
18
|
.activity: (NativeListActivityCell.self, { NativeListActivityCell(frame: .zero) }),
|
|
19
19
|
.system: (NativeListSystemCell.self, { NativeListSystemCell(frame: .zero) }),
|
|
20
20
|
.action: (NativeListActionCell.self, { NativeListActionCell(frame: .zero) }),
|
|
21
|
+
.buttonGroup: (NativeListButtonGroupCell.self, { NativeListButtonGroupCell(frame: .zero) }),
|
|
21
22
|
.unsupported: (
|
|
22
23
|
NativeListUnsupportedCell.self, { NativeListUnsupportedCell(frame: .zero) }
|
|
23
24
|
),
|
|
@@ -26,7 +26,8 @@ struct NativeListResolvedText {
|
|
|
26
26
|
let style = style ?? [:]
|
|
27
27
|
self.tabular = tabular
|
|
28
28
|
self.preservesBreakMode = preservesBreakMode
|
|
29
|
-
|
|
29
|
+
let requestedLines = style.int("lines", default: lines)
|
|
30
|
+
self.lines = requestedLines == 0 ? 0 : min(3, max(1, requestedLines))
|
|
30
31
|
explicitTruncation = style["lines"] != nil || style["truncate"] != nil
|
|
31
32
|
text =
|
|
32
33
|
self.lines == 1 && explicitTruncation
|
|
@@ -42,6 +42,20 @@ class NativeListRowHost: UICollectionViewCell {
|
|
|
42
42
|
var listStyle: [String: Any]?
|
|
43
43
|
var onAction: ((NativeListItem, String, NativeSelectionTarget?, NativeListActionOrigin?) -> Void)?
|
|
44
44
|
var onBindingInvalidated: ((NativeListRowHost, Int) -> Void)?
|
|
45
|
+
var structuralContentHeight: CGFloat?
|
|
46
|
+
|
|
47
|
+
override func layoutSubviews() {
|
|
48
|
+
super.layoutSubviews()
|
|
49
|
+
// UIKit relays out cells again when the collection's offset changes.
|
|
50
|
+
if let structuralContentHeight {
|
|
51
|
+
contentView.frame = CGRect(x: 0, y: 0, width: bounds.width, height: structuralContentHeight)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
override func prepareForReuse() {
|
|
56
|
+
super.prepareForReuse()
|
|
57
|
+
structuralContentHeight = nil
|
|
58
|
+
}
|
|
45
59
|
|
|
46
60
|
func bind(
|
|
47
61
|
item: NativeListItem, theme: [String: Any]?, layout: String, itemIndex: Int? = nil,
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import UIKit
|
|
2
2
|
|
|
3
3
|
final class NativeListDottedUnderlineLabel: NativeListTextLabel {
|
|
4
|
+
var usesDashedUnderline = false {
|
|
5
|
+
didSet {
|
|
6
|
+
dottedUnderlineLayer.lineDashPattern = usesDashedUnderline ? [3, 2] : [0, 4]
|
|
7
|
+
dottedUnderlineLayer.lineCap = usesDashedUnderline ? .butt : .round
|
|
8
|
+
setNeedsLayout()
|
|
9
|
+
}
|
|
10
|
+
}
|
|
4
11
|
var showsDottedUnderline = false {
|
|
5
12
|
didSet { setNeedsLayout() }
|
|
6
13
|
}
|
|
@@ -111,6 +118,7 @@ final class NativeListSectionHeaderCell: NativeListRendererCell {
|
|
|
111
118
|
private let mainStack = UIStackView()
|
|
112
119
|
private let titleRowStack = UIStackView()
|
|
113
120
|
private let titleLabel = NativeListDottedUnderlineLabel()
|
|
121
|
+
private let leadingBadge = NativeListInsetLabel()
|
|
114
122
|
private let titleSpinner = UIActivityIndicatorView(style: .medium)
|
|
115
123
|
private let subtitleLabel = NativeListTextLabel()
|
|
116
124
|
private let trailingStack = UIStackView()
|
|
@@ -148,6 +156,7 @@ final class NativeListSectionHeaderCell: NativeListRendererCell {
|
|
|
148
156
|
titleRowStack.axis = .horizontal
|
|
149
157
|
titleRowStack.alignment = .center
|
|
150
158
|
titleRowStack.spacing = 8
|
|
159
|
+
leadingBadge.heightAnchor.constraint(equalToConstant: 20).isActive = true
|
|
151
160
|
titleRowStack.addArrangedSubview(titleLabel)
|
|
152
161
|
mainStack.addArrangedSubview(titleRowStack)
|
|
153
162
|
mainStack.addArrangedSubview(subtitleLabel)
|
|
@@ -182,6 +191,15 @@ final class NativeListSectionHeaderCell: NativeListRendererCell {
|
|
|
182
191
|
selectorTypographyRestorers = []
|
|
183
192
|
currentItem = item
|
|
184
193
|
currentTheme = theme
|
|
194
|
+
if accessoryButtons[0].superview === titleRowStack {
|
|
195
|
+
titleRowStack.removeArrangedSubview(accessoryButtons[0])
|
|
196
|
+
accessoryButtons[0].removeFromSuperview()
|
|
197
|
+
trailingStack.insertArrangedSubview(accessoryButtons[0], at: 0)
|
|
198
|
+
}
|
|
199
|
+
titleRowStack.removeArrangedSubview(leadingBadge)
|
|
200
|
+
leadingBadge.removeFromSuperview()
|
|
201
|
+
leadingBadge.text = nil
|
|
202
|
+
leadingBadge.accessibilityLabel = nil
|
|
185
203
|
titleSpinner.stopAnimating()
|
|
186
204
|
NSLayoutConstraint.deactivate(selectorConstraints)
|
|
187
205
|
selectorConstraints = []
|
|
@@ -198,6 +216,7 @@ final class NativeListSectionHeaderCell: NativeListRendererCell {
|
|
|
198
216
|
titleLabel.removeGestureRecognizer(selectorTitleTap)
|
|
199
217
|
titleLabel.isUserInteractionEnabled = false
|
|
200
218
|
titleLabel.showsDottedUnderline = false
|
|
219
|
+
titleLabel.usesDashedUnderline = false
|
|
201
220
|
titleLabel.reservesDottedUnderlineSpace = false
|
|
202
221
|
titleLabel.dottedUnderlineVerticalOffset = 0
|
|
203
222
|
for label in [titleLabel, subtitleLabel] {
|
|
@@ -211,6 +230,7 @@ final class NativeListSectionHeaderCell: NativeListRendererCell {
|
|
|
211
230
|
label.rowVerticalAlignment = nil
|
|
212
231
|
}
|
|
213
232
|
titleLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
233
|
+
titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
|
|
214
234
|
subtitleLabel.font = nativeListFont(ofSize: 14, fonts: fonts)
|
|
215
235
|
subtitleLabel.textColor = nativeListColor(theme, "secondaryText", "#646464")
|
|
216
236
|
mainStack.alignment = .fill
|
|
@@ -228,6 +248,10 @@ final class NativeListSectionHeaderCell: NativeListRendererCell {
|
|
|
228
248
|
value.contentHorizontalAlignment = .center
|
|
229
249
|
value.contentVerticalAlignment = .center
|
|
230
250
|
value.rowTextOffsetY = 0
|
|
251
|
+
value.helpBindingGeneration += 1
|
|
252
|
+
value.helpPrimaryLineOnly = false
|
|
253
|
+
value.showsHelpUnderline = false
|
|
254
|
+
value.isUserInteractionEnabled = true
|
|
231
255
|
value.marketLineHeight = nil
|
|
232
256
|
value.selectorSummaryLineHeight = nil
|
|
233
257
|
value.isHidden = true
|
|
@@ -253,6 +277,7 @@ final class NativeListSectionHeaderCell: NativeListRendererCell {
|
|
|
253
277
|
if style["lineGap"] != nil { mainStack.spacing = CGFloat(style.double("lineGap")) }
|
|
254
278
|
if style["trailingGap"] != nil { trailingStack.spacing = CGFloat(style.double("trailingGap")) }
|
|
255
279
|
applyTextStyle(item)
|
|
280
|
+
bindPositionHeaderFields(item, theme: theme)
|
|
256
281
|
titleSpinner.color = titleLabel.textColor
|
|
257
282
|
updateTitleSpinner()
|
|
258
283
|
contentInsets = UIEdgeInsets(
|
|
@@ -273,6 +298,63 @@ final class NativeListSectionHeaderCell: NativeListRendererCell {
|
|
|
273
298
|
selectorConstraints.append(width)
|
|
274
299
|
}
|
|
275
300
|
}
|
|
301
|
+
private func bindPositionHeaderFields(_ item: NativeListItem, theme: [String: Any]?) {
|
|
302
|
+
let style = item.data.dictionary("style") ?? [:]
|
|
303
|
+
if let badge = item.data.dictionary("leadingBadge") {
|
|
304
|
+
leadingBadge.text = badge.string("text")
|
|
305
|
+
leadingBadge.font = nativeListFont(ofSize: 12, weight: .medium, fonts: fonts)
|
|
306
|
+
let tones = ["info": ("info", "#0F69F6"), "success": ("positive", "#008F5A"), "warning": ("caution", "#A35E00"), "danger": ("negative", "#CE2C31")]
|
|
307
|
+
if let tone = tones[badge.string("tone")] {
|
|
308
|
+
let color = nativeListColor(theme, tone.0, tone.1)
|
|
309
|
+
leadingBadge.textColor = color
|
|
310
|
+
leadingBadge.backgroundColor = color.withAlphaComponent(0.12)
|
|
311
|
+
if badge.string("tone") == "success", let background = theme?["successBackground"] as? String {
|
|
312
|
+
leadingBadge.backgroundColor = UIColor(nativeListHex: background, fallback: color.withAlphaComponent(0.12))
|
|
313
|
+
}
|
|
314
|
+
} else {
|
|
315
|
+
leadingBadge.textColor = nativeListColor(theme, "secondaryText", "#646464")
|
|
316
|
+
leadingBadge.backgroundColor = nativeListColor(theme, "strongBackground", "#F0F0F0")
|
|
317
|
+
}
|
|
318
|
+
let paragraph = NSMutableParagraphStyle()
|
|
319
|
+
paragraph.minimumLineHeight = 16
|
|
320
|
+
paragraph.maximumLineHeight = 16
|
|
321
|
+
leadingBadge.attributedText = NSAttributedString(
|
|
322
|
+
string: badge.string("text"), attributes: [
|
|
323
|
+
.font: leadingBadge.font as Any,
|
|
324
|
+
.foregroundColor: leadingBadge.textColor as Any,
|
|
325
|
+
.paragraphStyle: paragraph,
|
|
326
|
+
])
|
|
327
|
+
leadingBadge.horizontalInset = 8
|
|
328
|
+
leadingBadge.topInset = 2
|
|
329
|
+
leadingBadge.bottomInset = 2
|
|
330
|
+
leadingBadge.layer.cornerRadius = 4
|
|
331
|
+
leadingBadge.clipsToBounds = true
|
|
332
|
+
leadingBadge.accessibilityLabel = badge["accessibilityLabel"] as? String
|
|
333
|
+
leadingBadge.setContentHuggingPriority(.required, for: .horizontal)
|
|
334
|
+
leadingBadge.setContentCompressionResistancePriority(.required, for: .horizontal)
|
|
335
|
+
titleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
336
|
+
titleRowStack.insertArrangedSubview(leadingBadge, at: 0)
|
|
337
|
+
}
|
|
338
|
+
if style["titleDecoration"] != nil {
|
|
339
|
+
titleLabel.showsDottedUnderline = style.string("titleDecoration") == "dashed"
|
|
340
|
+
titleLabel.usesDashedUnderline = true
|
|
341
|
+
titleLabel.dottedUnderlineColor = titleLabel.textColor
|
|
342
|
+
}
|
|
343
|
+
let value = accessoryButtons[0]
|
|
344
|
+
if style.string("valuePlacement") == "afterTitle" {
|
|
345
|
+
trailingStack.removeArrangedSubview(value)
|
|
346
|
+
value.removeFromSuperview()
|
|
347
|
+
titleLabel.setContentHuggingPriority(.required, for: .horizontal)
|
|
348
|
+
mainStack.alignment = .leading
|
|
349
|
+
titleRowStack.addArrangedSubview(value)
|
|
350
|
+
}
|
|
351
|
+
if let help = item.data.dictionary("valueHelp") {
|
|
352
|
+
value.showsHelpUnderline = help.string("underline") == "dashed"
|
|
353
|
+
value.isUserInteractionEnabled = help.string("presentation") == "popover"
|
|
354
|
+
accessoryActions = [(help.string("presentation") == "popover" ? help.string("actionKey") : "", nil)]
|
|
355
|
+
value.setNeedsLayout()
|
|
356
|
+
}
|
|
357
|
+
}
|
|
276
358
|
private func applyTextStyle(_ item: NativeListItem) {
|
|
277
359
|
let style = item.data.dictionary("style") ?? [:]
|
|
278
360
|
if let title = style.dictionary("title") {
|
|
@@ -312,6 +394,12 @@ final class NativeListSectionHeaderCell: NativeListRendererCell {
|
|
|
312
394
|
override func recycleContent() {
|
|
313
395
|
checkboxControl.reset()
|
|
314
396
|
currentItem = nil
|
|
397
|
+
accessoryActions = []
|
|
398
|
+
titleLabel.removeGestureRecognizer(selectorTitleTap)
|
|
399
|
+
titleLabel.isUserInteractionEnabled = false
|
|
400
|
+
accessoryButtons[0].helpBindingGeneration += 1
|
|
401
|
+
accessoryButtons[0].isUserInteractionEnabled = false
|
|
402
|
+
accessoryButtons[0].showsHelpUnderline = false
|
|
315
403
|
titleSpinner.stopAnimating()
|
|
316
404
|
}
|
|
317
405
|
override func didMoveToWindow() {
|