@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.
Files changed (116) hide show
  1. package/android/src/main/java/com/onekey/nativelist/HybridNativeList.kt +14 -1
  2. package/android/src/main/java/com/onekey/nativelist/NativeListAccessoryStack.kt +240 -11
  3. package/android/src/main/java/com/onekey/nativelist/NativeListActivityDetailsView.kt +7 -4
  4. package/android/src/main/java/com/onekey/nativelist/NativeListActivityRenderer.kt +6 -3
  5. package/android/src/main/java/com/onekey/nativelist/NativeListButtonGroupRenderer.kt +102 -0
  6. package/android/src/main/java/com/onekey/nativelist/NativeListContainerSlotAdapter.kt +12 -0
  7. package/android/src/main/java/com/onekey/nativelist/NativeListDataRowRenderer.kt +87 -7
  8. package/android/src/main/java/com/onekey/nativelist/NativeListIdentityRenderer.kt +177 -1
  9. package/android/src/main/java/com/onekey/nativelist/NativeListLeadingVisual.kt +43 -14
  10. package/android/src/main/java/com/onekey/nativelist/NativeListMessageRenderer.kt +25 -0
  11. package/android/src/main/java/com/onekey/nativelist/NativeListModels.kt +33 -2
  12. package/android/src/main/java/com/onekey/nativelist/NativeListPrimitives.kt +22 -0
  13. package/android/src/main/java/com/onekey/nativelist/NativeListRendererRegistry.kt +2 -0
  14. package/android/src/main/java/com/onekey/nativelist/NativeListRendererRowView.kt +2 -0
  15. package/android/src/main/java/com/onekey/nativelist/NativeListResolvedText.kt +3 -2
  16. package/android/src/main/java/com/onekey/nativelist/NativeListSectionHeaderRenderer.kt +69 -0
  17. package/android/src/main/java/com/onekey/nativelist/NativeListTextView.kt +55 -1
  18. package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +1912 -107
  19. package/android/src/test/java/com/margelo/nitro/nativelist/NativeListPressPolicyTest.kt +1 -0
  20. package/docs/ROW_TEMPLATES.md +60 -9
  21. package/docs/SPEC.md +450 -5
  22. package/docs/STYLE_SPEC.md +6 -0
  23. package/ios/HybridNativeList.swift +21 -2
  24. package/ios/NativeListAccessoryStack.swift +227 -39
  25. package/ios/NativeListActivityDetailsView.swift +18 -8
  26. package/ios/NativeListActivityRenderer.swift +8 -2
  27. package/ios/NativeListButtonGroupRenderer.swift +89 -0
  28. package/ios/NativeListDataRowRenderer.swift +155 -27
  29. package/ios/NativeListDesignAssets.swift +3 -0
  30. package/ios/NativeListIdentityRenderer.swift +166 -30
  31. package/ios/NativeListLeadingVisual.swift +49 -14
  32. package/ios/NativeListMessageRenderer.swift +34 -1
  33. package/ios/NativeListModels.swift +42 -2
  34. package/ios/NativeListRendererRegistry.swift +1 -0
  35. package/ios/NativeListResolvedText.swift +2 -1
  36. package/ios/NativeListRowHost.swift +14 -0
  37. package/ios/NativeListSectionHeaderRenderer.swift +88 -0
  38. package/ios/RNCNativeListView.swift +1013 -55
  39. package/ios/Resources/NativeListIcons.xcassets/onekey_crypto_coin_outline.imageset/Contents.json +15 -0
  40. package/ios/Resources/NativeListIcons.xcassets/onekey_crypto_coin_outline.imageset/icon.svg +4 -0
  41. package/ios/Resources/NativeListIcons.xcassets/onekey_gas_solid.imageset/Contents.json +15 -0
  42. package/ios/Resources/NativeListIcons.xcassets/onekey_gas_solid.imageset/icon.svg +3 -0
  43. package/ios/Resources/NativeListIcons.xcassets/onekey_refresh_ccw_outline.imageset/Contents.json +15 -0
  44. package/ios/Resources/NativeListIcons.xcassets/onekey_refresh_ccw_outline.imageset/icon.svg +3 -0
  45. package/ios/tests/NativeListActivityPresentationTests.swift +56 -0
  46. package/ios/tests/NativeListLifecycleTests.swift +2720 -3
  47. package/lib/module/NativeList.js +175 -28
  48. package/lib/module/NativeList.web.js +129 -30
  49. package/lib/module/index.js +1 -1
  50. package/lib/module/models.js +1 -0
  51. package/lib/module/selection.js +1 -1
  52. package/lib/module/validation.js +421 -8
  53. package/lib/module/web/NativeListWebEngine.js +507 -19
  54. package/lib/module/web/templates/ActivityRowRenderer.js +8 -7
  55. package/lib/module/web/templates/ButtonGroupRowRenderer.js +47 -0
  56. package/lib/module/web/templates/DataRowRenderer.js +40 -5
  57. package/lib/module/web/templates/IdentityRowRenderer.js +90 -4
  58. package/lib/module/web/templates/MessageRowRenderer.js +18 -2
  59. package/lib/module/web/templates/RowContainerStyle.js +1 -0
  60. package/lib/module/web/templates/RowRendererRegistry.js +3 -0
  61. package/lib/module/web/templates/SectionHeaderRowRenderer.js +36 -2
  62. package/lib/nitrogen/generated/android/c++/JHybridNativeListSpec.cpp +26 -0
  63. package/lib/nitrogen/generated/android/c++/JHybridNativeListSpec.hpp +4 -0
  64. package/lib/nitrogen/generated/android/c++/views/JHybridNativeListStateUpdater.cpp +10 -0
  65. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/HybridNativeListSpec.kt +20 -0
  66. package/lib/nitrogen/generated/ios/c++/HybridNativeListSpecSwift.hpp +14 -0
  67. package/lib/nitrogen/generated/ios/c++/views/HybridNativeListComponent.mm +12 -0
  68. package/lib/nitrogen/generated/ios/swift/HybridNativeListSpec.swift +2 -0
  69. package/lib/nitrogen/generated/ios/swift/HybridNativeListSpec_cxx.swift +43 -0
  70. package/lib/nitrogen/generated/shared/c++/HybridNativeListSpec.cpp +4 -0
  71. package/lib/nitrogen/generated/shared/c++/HybridNativeListSpec.hpp +4 -0
  72. package/lib/nitrogen/generated/shared/c++/views/HybridNativeListComponent.cpp +4 -0
  73. package/lib/nitrogen/generated/shared/c++/views/HybridNativeListComponent.hpp +6 -0
  74. package/lib/nitrogen/generated/shared/json/NativeListConfig.json +2 -0
  75. package/lib/typescript/src/NativeList.d.ts +1 -1
  76. package/lib/typescript/src/NativeList.nitro.d.ts +2 -0
  77. package/lib/typescript/src/NativeList.types.d.ts +18 -0
  78. package/lib/typescript/src/index.d.ts +2 -2
  79. package/lib/typescript/src/models.d.ts +105 -9
  80. package/lib/typescript/src/validation.d.ts +12 -0
  81. package/lib/typescript/src/web/NativeListWebEngine.d.ts +32 -0
  82. package/lib/typescript/src/web/templates/ButtonGroupRowRenderer.d.ts +14 -0
  83. package/lib/typescript/src/web/templates/IdentityRowRenderer.d.ts +1 -1
  84. package/lib/typescript/src/web/templates/MessageRowRenderer.d.ts +9 -3
  85. package/nitrogen/generated/android/c++/JHybridNativeListSpec.cpp +26 -0
  86. package/nitrogen/generated/android/c++/JHybridNativeListSpec.hpp +4 -0
  87. package/nitrogen/generated/android/c++/views/JHybridNativeListStateUpdater.cpp +10 -0
  88. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/HybridNativeListSpec.kt +20 -0
  89. package/nitrogen/generated/ios/c++/HybridNativeListSpecSwift.hpp +14 -0
  90. package/nitrogen/generated/ios/c++/views/HybridNativeListComponent.mm +12 -0
  91. package/nitrogen/generated/ios/swift/HybridNativeListSpec.swift +2 -0
  92. package/nitrogen/generated/ios/swift/HybridNativeListSpec_cxx.swift +43 -0
  93. package/nitrogen/generated/shared/c++/HybridNativeListSpec.cpp +4 -0
  94. package/nitrogen/generated/shared/c++/HybridNativeListSpec.hpp +4 -0
  95. package/nitrogen/generated/shared/c++/views/HybridNativeListComponent.cpp +4 -0
  96. package/nitrogen/generated/shared/c++/views/HybridNativeListComponent.hpp +6 -0
  97. package/nitrogen/generated/shared/json/NativeListConfig.json +2 -0
  98. package/package.json +3 -3
  99. package/react-native-native-list.podspec +1 -0
  100. package/src/NativeList.nitro.ts +2 -0
  101. package/src/NativeList.tsx +313 -44
  102. package/src/NativeList.types.ts +22 -0
  103. package/src/NativeList.web.tsx +204 -48
  104. package/src/index.ts +2 -0
  105. package/src/models.ts +117 -3
  106. package/src/selection.ts +1 -0
  107. package/src/validation.ts +852 -6
  108. package/src/web/NativeListWebEngine.ts +803 -20
  109. package/src/web/templates/ActivityRowRenderer.ts +14 -6
  110. package/src/web/templates/ButtonGroupRowRenderer.ts +58 -0
  111. package/src/web/templates/DataRowRenderer.ts +93 -20
  112. package/src/web/templates/IdentityRowRenderer.ts +150 -5
  113. package/src/web/templates/MessageRowRenderer.ts +23 -7
  114. package/src/web/templates/RowContainerStyle.ts +2 -0
  115. package/src/web/templates/RowRendererRegistry.ts +11 -1
  116. 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(nativeListHex: "#8D8D8D", fallback: .gray)
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
- sourceFallback ? UIColor(nativeListHex: placeholder, fallback: .clear) : visualBackground
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 || sourceFallback
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: placeholder, fallback: .clear)
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 child?.isHidden = !loaded
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, weight: overlayTextLineHeight == nil ? .medium : .regular,
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
- if child.superview !== frame {
211
- frame.subviews.forEach { $0.removeFromSuperview() }
212
- frame.addSubview(child)
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
- !sources.isEmpty && fallbackIcon != nil
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 (frame, data) in overlays {
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
- frame.subviews.first?.frame = frame.bounds.insetBy(
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), fonts: fonts)
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
- self.lines = min(3, max(1, style.int("lines", default: lines)))
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() {