@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
@@ -6,7 +6,7 @@ final class NativeListDataRowCell: NativeListRendererCell {
6
6
  fonts: NativeListFontFamilies
7
7
  ) -> CGFloat? {
8
8
  let secondary = item.data.dictionaries("columns").contains {
9
- !$0.string("secondaryText").isEmpty
9
+ !nativeListDataColumnText($0, "secondaryText").isEmpty
10
10
  }
11
11
  return secondary ? 60 : layout == "table" ? 48 : 56
12
12
  }
@@ -14,9 +14,13 @@ final class NativeListDataRowCell: NativeListRendererCell {
14
14
  private let visual = NativeListLeadingVisual()
15
15
  private let favorite = UIImageView()
16
16
  private let accessories = NativeListAccessoryStack()
17
+ private let trailingAccessories = NativeListAccessoryStack()
17
18
  private let columns = UIStackView()
18
19
  private let cells = (0..<4).map { _ in NativeListTableColumnView() }
19
20
  private var rowConstraints: [NSLayoutConstraint] = []
21
+ private var trailingWidthConstraint: NSLayoutConstraint?
22
+ private var columnWidthConstraints: [NSLayoutConstraint] = []
23
+ private var columnWeights: [Int]?
20
24
  override func unselectedBackground(
21
25
  _ item: NativeListItem, theme: [String: Any]?, layout: String, itemIndex: Int?
22
26
  ) -> UIColor {
@@ -34,10 +38,13 @@ final class NativeListDataRowCell: NativeListRendererCell {
34
38
  columns.alignment = .center
35
39
  columns.spacing = 8
36
40
  cells.forEach(columns.addArrangedSubview)
37
- [favorite, visual, accessories, columns].forEach(root.addArrangedSubview)
41
+ [favorite, visual, accessories, columns, trailingAccessories].forEach(root.addArrangedSubview)
38
42
  accessories.onAction = { [weak self] key, view, slot, target in
39
43
  self?.emitAction(key, from: view, source: "trailingAccessory", slot: slot, target: target)
40
44
  }
45
+ trailingAccessories.onAction = { [weak self] key, view, slot, target in
46
+ self?.emitAction(key, from: view, source: "trailingAccessory", slot: slot, target: target)
47
+ }
41
48
  }
42
49
  required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
43
50
  override func bindContent(
@@ -86,16 +93,55 @@ final class NativeListDataRowCell: NativeListRendererCell {
86
93
  item, descriptors: descriptors, theme: theme, style: accessoryStyle,
87
94
  checkboxState: checkboxState, fonts: fonts)
88
95
  accessories.isHidden = descriptors.isEmpty
89
- NSLayoutConstraint.deactivate(rowConstraints)
90
- rowConstraints = [
91
- favorite.widthAnchor.constraint(equalToConstant: 20),
92
- favorite.heightAnchor.constraint(equalToConstant: 20),
93
- visual.widthAnchor.constraint(equalToConstant: CGFloat(image.double("width", default: 40))),
94
- visual.heightAnchor.constraint(equalToConstant: CGFloat(image.double("height", default: 40))),
95
- ]
96
+ let trailing = data.dictionaries("trailing")
97
+ trailingAccessories.bind(
98
+ item, descriptors: trailing, theme: theme, style: style,
99
+ checkboxState: checkboxState, fonts: fonts)
100
+ trailingAccessories.isHidden = trailing.isEmpty
101
+ if rowConstraints.isEmpty {
102
+ rowConstraints = [
103
+ favorite.widthAnchor.constraint(equalToConstant: 20),
104
+ favorite.heightAnchor.constraint(equalToConstant: 20),
105
+ visual.widthAnchor.constraint(equalToConstant: 40),
106
+ visual.heightAnchor.constraint(equalToConstant: 40),
107
+ ]
108
+ NSLayoutConstraint.activate(rowConstraints)
109
+ }
110
+ let visualWidth = CGFloat(image.double("width", default: 40))
111
+ let visualHeight = CGFloat(image.double("height", default: 40))
112
+ if rowConstraints[2].constant != visualWidth { rowConstraints[2].constant = visualWidth }
113
+ if rowConstraints[3].constant != visualHeight { rowConstraints[3].constant = visualHeight }
114
+ if !trailing.isEmpty {
115
+ // UIStackView has no intrinsic width of its own. Reserve the controls' fitted
116
+ // width so extra space goes to the weighted columns instead of this stack.
117
+ if trailingWidthConstraint == nil {
118
+ trailingWidthConstraint = trailingAccessories.widthAnchor.constraint(equalToConstant: 0)
119
+ }
120
+ if trailingWidthConstraint?.isActive == true { trailingWidthConstraint?.isActive = false }
121
+ let trailingWidth =
122
+ trailingAccessories.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).width
123
+ if trailingWidthConstraint?.constant != trailingWidth {
124
+ trailingWidthConstraint?.constant = trailingWidth
125
+ }
126
+ trailingWidthConstraint?.isActive = true
127
+ } else {
128
+ if trailingWidthConstraint?.isActive == true { trailingWidthConstraint?.isActive = false }
129
+ }
96
130
  let values = Array(data.dictionaries("columns").prefix(4))
131
+ let weights = values.map { max(1, $0.int("weight", default: 1)) }
132
+ var divisor = weights.first ?? 1
133
+ for weight in weights.dropFirst() {
134
+ var a = divisor
135
+ var b = weight
136
+ while b != 0 { (a, b) = (b, a % b) }
137
+ divisor = a
138
+ }
139
+ let normalizedWeights = weights.map { $0 / divisor }
140
+ let columnsChanged = columnWeights != normalizedWeights
141
+ if columnsChanged { NSLayoutConstraint.deactivate(columnWidthConstraints) }
97
142
  for (index, cell) in cells.enumerated() {
98
- cell.isHidden = index >= values.count
143
+ let hidden = index >= values.count
144
+ if cell.isHidden != hidden { cell.isHidden = hidden }
99
145
  guard index < values.count else {
100
146
  cell.reset()
101
147
  continue
@@ -109,23 +155,29 @@ final class NativeListDataRowCell: NativeListRendererCell {
109
155
  cell.applyStyle(style) { label, style in
110
156
  NativeListTextStyles.applyStyledText(label, style, fonts: fonts)
111
157
  }
112
- if index > 0 {
113
- rowConstraints.append(
114
- cell.widthAnchor.constraint(
115
- equalTo: cells[0].widthAnchor,
116
- multiplier: CGFloat(max(1, values[index].int("weight", default: 1)))
117
- / CGFloat(max(1, values[0].int("weight", default: 1)))))
118
- }
158
+ cell.applySegments(values[index])
159
+ cell.bindTitleIcons(values[index], theme: theme)
160
+ }
161
+ if columnsChanged {
162
+ columnWidthConstraints = values.count > 1 ? (1..<values.count).map { index in
163
+ cells[index].widthAnchor.constraint(equalTo: cells[0].widthAnchor,
164
+ multiplier: CGFloat(weights[index]) / CGFloat(weights[0]))
165
+ } : []
166
+ NSLayoutConstraint.activate(columnWidthConstraints)
167
+ columnWeights = normalizedWeights
119
168
  }
120
- NSLayoutConstraint.activate(rowConstraints)
121
169
  }
122
170
  override func bindSelectionContent(
123
171
  _ item: NativeListItem,
124
172
  checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
125
- ) { accessories.updateSelection(item, checkboxState: checkboxState) }
173
+ ) {
174
+ accessories.updateSelection(item, checkboxState: checkboxState)
175
+ trailingAccessories.updateSelection(item, checkboxState: checkboxState)
176
+ }
126
177
  override func recycleContent() {
127
178
  visual.recycle()
128
179
  accessories.reset()
180
+ trailingAccessories.reset()
129
181
  cells.forEach { $0.reset() }
130
182
  }
131
183
  }
@@ -133,6 +185,8 @@ final class NativeListDataRowCell: NativeListRendererCell {
133
185
  private final class NativeListTableColumnView: UIStackView {
134
186
  private let primaryLine = UIStackView()
135
187
  private let primaryLabel = NativeListTextLabel()
188
+ private let titleIconView = UIImageView()
189
+ private let secondaryTitleIconView = UIImageView()
136
190
  private let badgesStack = UIStackView()
137
191
  private let secondaryLine = UIStackView()
138
192
  private let secondaryLeadingLabel = NativeListTextLabel()
@@ -152,11 +206,21 @@ private final class NativeListTableColumnView: UIStackView {
152
206
  primaryLabel.numberOfLines = 1
153
207
  primaryLabel.lineBreakMode = .byTruncatingTail
154
208
  primaryLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
209
+ for icon in [titleIconView, secondaryTitleIconView] {
210
+ icon.contentMode = .scaleAspectFit
211
+ icon.translatesAutoresizingMaskIntoConstraints = false
212
+ icon.widthAnchor.constraint(equalToConstant: 20).isActive = true
213
+ icon.heightAnchor.constraint(equalToConstant: 20).isActive = true
214
+ icon.setContentHuggingPriority(.required, for: .horizontal)
215
+ icon.isHidden = true
216
+ }
155
217
  badgesStack.axis = .horizontal
156
218
  badgesStack.alignment = .center
157
219
  badgesStack.spacing = 4
158
220
  badgesStack.setContentCompressionResistancePriority(.required, for: .horizontal)
159
221
  primaryLine.addArrangedSubview(primaryLabel)
222
+ primaryLine.addArrangedSubview(titleIconView)
223
+ primaryLine.addArrangedSubview(secondaryTitleIconView)
160
224
  primaryLine.addArrangedSubview(badgesStack)
161
225
 
162
226
  secondaryLine.axis = .horizontal
@@ -178,8 +242,17 @@ private final class NativeListTableColumnView: UIStackView {
178
242
  fatalError("init(coder:) has not been implemented")
179
243
  }
180
244
 
181
- func reset() {
245
+ func reset(clearPrimaryText: Bool = true) {
182
246
  linear = false
247
+ primaryLine.alignment = .center
248
+ for icon in [titleIconView, secondaryTitleIconView] {
249
+ icon.image = nil
250
+ icon.isHidden = true
251
+ icon.accessibilityLabel = nil
252
+ icon.isAccessibilityElement = false
253
+ }
254
+ primaryLine.setCustomSpacing(UIStackView.spacingUseDefault, after: primaryLabel)
255
+ primaryLine.setCustomSpacing(UIStackView.spacingUseDefault, after: titleIconView)
183
256
  for label in [primaryLabel, secondaryLeadingLabel, secondaryLabel] {
184
257
  label.rowVerticalAlignment = nil
185
258
  label.rowOffsetY = 0
@@ -190,7 +263,7 @@ private final class NativeListTableColumnView: UIStackView {
190
263
  primaryLabel.numberOfLines = 1
191
264
  secondaryLeadingLabel.numberOfLines = 1
192
265
  secondaryLabel.numberOfLines = 1
193
- primaryLabel.attributedText = nil
266
+ if clearPrimaryText { primaryLabel.attributedText = nil }
194
267
  secondaryLeadingLabel.attributedText = nil
195
268
  secondaryLabel.attributedText = nil
196
269
  secondaryLeadingLabel.isHidden = true
@@ -202,6 +275,28 @@ private final class NativeListTableColumnView: UIStackView {
202
275
  }
203
276
  }
204
277
 
278
+ func bindTitleIcons(_ column: [String: Any], theme: [String: Any]?) {
279
+ let descriptors = [column.dictionary("titleIcon"), column.dictionary("secondaryTitleIcon")]
280
+ for (index, descriptor) in descriptors.enumerated() {
281
+ guard let descriptor else { continue }
282
+ let icon = index == 0 ? titleIconView : secondaryTitleIconView
283
+ icon.image = nativeListIcon(named: descriptor.string("name"))
284
+ icon.tintColor = UIColor(
285
+ nativeListHex: descriptor.string("tintColor",
286
+ default: theme?["iconSubdued"] as? String ?? "#8D8D8D"),
287
+ fallback: .gray)
288
+ icon.isHidden = false
289
+ let label = descriptor.string("accessibilityLabel")
290
+ icon.accessibilityLabel = label.isEmpty ? nil : label
291
+ icon.isAccessibilityElement = !label.isEmpty
292
+ }
293
+ if descriptors.contains(where: { $0 != nil }) {
294
+ primaryLine.setCustomSpacing(4, after: primaryLabel)
295
+ primaryLine.setCustomSpacing(4, after: titleIconView)
296
+ if linear { primaryLine.alignment = .top }
297
+ }
298
+ }
299
+
205
300
  func bind(
206
301
  column: [String: Any],
207
302
  badges: [[String: Any]],
@@ -228,7 +323,7 @@ private final class NativeListTableColumnView: UIStackView {
228
323
  secondaryLeadingLabel.font = nativeListFont(ofSize: 12, fonts: fonts)
229
324
  secondaryLabel.font = nativeListFont(ofSize: 12, fonts: fonts)
230
325
  primaryLabel.attributedText = line(
231
- column.string("text"),
326
+ nativeListDataColumnText(column, "text"),
232
327
  font: nativeListFont(ofSize: 14, weight: .medium, fonts: fonts),
233
328
  color: textColor(column.string("tone"), theme: theme),
234
329
  height: 20
@@ -261,7 +356,7 @@ private final class NativeListTableColumnView: UIStackView {
261
356
  secondaryLeadingLabel.isHidden = false
262
357
  secondaryLine.isHidden = false
263
358
  }
264
- let secondary = column.string("secondaryText")
359
+ let secondary = nativeListDataColumnText(column, "secondaryText")
265
360
  if !secondary.isEmpty {
266
361
  secondaryLabel.attributedText = line(
267
362
  secondary,
@@ -286,7 +381,7 @@ private final class NativeListTableColumnView: UIStackView {
286
381
  column: [String: Any], badges: [[String: Any]], theme: [String: Any]?,
287
382
  style: [String: Any] = [:], fonts: NativeListFontFamilies
288
383
  ) {
289
- reset()
384
+ reset(clearPrimaryText: !linear)
290
385
  linear = true
291
386
  alignment = .fill
292
387
  badgesStack.isHidden = true
@@ -331,14 +426,15 @@ private final class NativeListTableColumnView: UIStackView {
331
426
  primaryLabel.textAlignment =
332
427
  column.string("alignment") == "end"
333
428
  ? .right : column.string("alignment") == "center" ? .center : .left
334
- let secondary = column.string("secondaryText")
429
+ let secondary = nativeListDataColumnText(column, "secondaryText")
335
430
  let primaryFont = font(primaryStyle, size: 14, weight: .medium)
336
431
  let attributed = NSMutableAttributedString(
337
- string: column.string("text"),
432
+ string: nativeListDataColumnText(column, "text"),
338
433
  attributes: [
339
434
  .font: primaryFont,
340
435
  .foregroundColor: color(primaryStyle, textColor(column.string("tone"), theme: theme)),
341
436
  ])
437
+ attributed.setAttributedString(nativeListDataSegments(attributed, column.dictionaries("textSegments")))
342
438
  let primaryLength = attributed.length
343
439
  // Every badge run is the legacy " text " pill: both lead spaces sit inside the badge
344
440
  // background and are its left padding. `titleBadgeGap` adds an exact, unfilled gap between
@@ -378,7 +474,10 @@ private final class NativeListTableColumnView: UIStackView {
378
474
  textColor(column.string("secondaryTone", default: "secondary"), theme: theme)),
379
475
  ]
380
476
  attributes.merge(paragraph(secondaryStyle, font: secondaryFont, spacing: nil)) { $1 }
381
- attributed.append(NSAttributedString(string: "\n\(secondary)", attributes: attributes))
477
+ attributed.append(NSAttributedString(string: "\n", attributes: attributes))
478
+ attributed.append(nativeListDataSegments(
479
+ NSAttributedString(string: secondary, attributes: attributes),
480
+ column.dictionaries("secondaryTextSegments")))
382
481
  lines = min(3, lines + max(1, secondaryStyle.int("lines", default: 1)))
383
482
  }
384
483
  primaryLabel.font = nativeListFont(ofSize: 12, fonts: fonts)
@@ -400,6 +499,16 @@ private final class NativeListTableColumnView: UIStackView {
400
499
  primaryStyle.double("offsetY", default: secondaryStyle.double("offsetY")))
401
500
  }
402
501
 
502
+ func applySegments(_ column: [String: Any]) {
503
+ guard !linear else { return }
504
+ if let text = primaryLabel.attributedText {
505
+ primaryLabel.attributedText = nativeListDataSegments(text, column.dictionaries("textSegments"))
506
+ }
507
+ if let text = secondaryLabel.attributedText {
508
+ secondaryLabel.attributedText = nativeListDataSegments(text, column.dictionaries("secondaryTextSegments"))
509
+ }
510
+ }
511
+
403
512
  func applyStyle(_ style: [String: Any], text: (UILabel, [String: Any]) -> Void) {
404
513
  // The linear single label already mapped its styles onto attributed ranges.
405
514
  guard !linear else { return }
@@ -446,3 +555,22 @@ private final class NativeListTableColumnView: UIStackView {
446
555
  }
447
556
  }
448
557
  }
558
+
559
+ private func nativeListDataColumnText(_ column: [String: Any], _ field: String) -> String {
560
+ let segments = column.dictionaries("\(field)Segments")
561
+ return segments.isEmpty ? column.string(field) : segments.map { $0.string("text") }.joined()
562
+ }
563
+
564
+ private func nativeListDataSegments(_ fallback: NSAttributedString, _ segments: [[String: Any]]) -> NSAttributedString {
565
+ guard !segments.isEmpty, fallback.length > 0 else { return fallback }
566
+ let attributes = fallback.attributes(at: 0, effectiveRange: nil)
567
+ let result = NSMutableAttributedString(string: "")
568
+ for segment in segments {
569
+ var runAttributes = attributes
570
+ if segment.string("style") == "subscript", let font = attributes[.font] as? UIFont {
571
+ runAttributes[.font] = font.withSize(ceil(font.pointSize * 0.6))
572
+ }
573
+ result.append(NSAttributedString(string: segment.string("text"), attributes: runAttributes))
574
+ }
575
+ return result
576
+ }
@@ -110,6 +110,9 @@ func nativeListIcon(named name: String) -> UIImage? {
110
110
  case "LockSolid": assetName = "onekey_selector_lock_solid"
111
111
  // OneKey patch: official selector icon geometry.
112
112
  case "GlobusOutline": assetName = "onekey_selector_globus_outline"
113
+ case "GasSolid": assetName = "onekey_gas_solid"
114
+ case "RefreshCcwOutline": assetName = "onekey_refresh_ccw_outline"
115
+ case "CryptoCoinOutline": assetName = "onekey_crypto_coin_outline"
113
116
  case "ArrowBottomOutline": assetName = "onekey_arrow_bottom"
114
117
  case "ArrowTopOutline": assetName = "onekey_arrow_top"
115
118
  case "ChartTrendingUpOutline": assetName = "onekey_chart_trending_up"
@@ -4,6 +4,51 @@ final class NativeListIdentityCell: NativeListRendererCell {
4
4
  override class func appliesSizePreset(_ item: NativeListItem) -> Bool {
5
5
  !["walletSidebar", "networkSelector"].contains(item.data.string("presentation"))
6
6
  }
7
+ private class func configureTrailingStack(
8
+ _ stack: NativeListAccessoryStack, descriptors: [[String: Any]]
9
+ ) {
10
+ stack.axis = .vertical
11
+ stack.alignment = .trailing
12
+ stack.spacing = 2
13
+ if descriptors.contains(where: { $0.string("kind") == "checkbox" })
14
+ && descriptors.contains(where: { $0.string("kind") == "value" })
15
+ {
16
+ stack.axis = .horizontal
17
+ stack.alignment = .center
18
+ stack.spacing = 12
19
+ } else if descriptors.count == 2,
20
+ descriptors[0].string("kind") == "valuePair",
21
+ (descriptors[1].string("kind") == "menu"
22
+ || (descriptors[1].string("kind") == "icon"
23
+ && descriptors[1].string("name") == "DotVerSolid"
24
+ && descriptors[1].string("actionKey") == "menu"))
25
+ {
26
+ stack.axis = .horizontal
27
+ stack.alignment = .center
28
+ stack.spacing = 8
29
+ } else if descriptors.count == 2,
30
+ (descriptors[0].string("kind") == "valuePair"
31
+ && descriptors[1].string("kind") == "chevron")
32
+ || (descriptors[0].string("kind") == "icon"
33
+ && descriptors[0].string("name") == "RefreshCcwOutline"
34
+ && descriptors[0].string("actionKey").isEmpty
35
+ && descriptors[1].string("kind") == "chevron")
36
+ {
37
+ stack.axis = .horizontal
38
+ stack.alignment = .center
39
+ stack.spacing = 12
40
+ } else if descriptors.count == 2,
41
+ descriptors[0].string("kind") == "icon",
42
+ descriptors[0].string("name") == "PencilOutline",
43
+ descriptors[1].string("kind") == "icon",
44
+ descriptors[1].string("name") == "DragOutline"
45
+ {
46
+ stack.axis = .horizontal
47
+ stack.alignment = .center
48
+ // Source 36-point icon frames with -7 margins need a 10-point gap.
49
+ stack.spacing = 10
50
+ }
51
+ }
7
52
  override class func measure(
8
53
  _ item: NativeListItem, width: CGFloat, theme: [String: Any]?, layout: String,
9
54
  fonts: NativeListFontFamilies
@@ -12,6 +57,47 @@ final class NativeListIdentityCell: NativeListRendererCell {
12
57
  return item.data.dictionaries("badges").isEmpty ? 68 : 92
13
58
  }
14
59
  if item.data.string("presentation") == "networkSelector" { return 47 }
60
+ if item.data.string("contentHeight") == "auto" {
61
+ let style = item.data.dictionary("style") ?? [:]
62
+ let horizontalPadding = CGFloat(style.double("horizontalPadding", default: layout == "table" ? 16 : 12))
63
+ let verticalPadding = CGFloat(style.double("verticalPadding", default: 8))
64
+ let imageWidth = CGFloat(style.dictionary("image")?.double("width", default: 40) ?? 40)
65
+ let imageHeight = CGFloat(style.dictionary("image")?.double("height", default: 40) ?? 40)
66
+ let leadingGap = CGFloat(style.double("leadingGap", default: 12))
67
+ let accessories = item.data.dictionaries("trailing")
68
+ let trailing = NativeListAccessoryStack(frame: .zero)
69
+ configureTrailingStack(trailing, descriptors: accessories)
70
+ trailing.bind(
71
+ item, descriptors: accessories, theme: theme, style: style,
72
+ defaultSpacing: trailing.spacing, checkboxState: { _, _, fallback in fallback }, fonts: fonts)
73
+ trailing.isHidden = false
74
+ let trailingSize = trailing.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
75
+ let rightInset = style["horizontalPadding"] != nil
76
+ ? horizontalPadding : trailing.endInset ?? horizontalPadding
77
+ let contentGap = trailing.contentGap ?? 12
78
+ let textWidth = max(1,
79
+ width - horizontalPadding - rightInset - imageWidth - leadingGap
80
+ - trailingSize.width - contentGap)
81
+ let title = NativeListResolvedText(
82
+ item.data.string("title"), style: style.dictionary("title"), size: 16,
83
+ weight: .medium, color: .black, lineHeight: 24,
84
+ lines: item.data.int("titleLines", default: 1), fonts: fonts)
85
+ let subtitle = NativeListResolvedText(
86
+ item.data.string("subtitle"), style: style.dictionary("subtitle"), size: 14,
87
+ color: .black, lineHeight: 20,
88
+ lines: item.data.int("subtitleLines", default: 1),
89
+ breakMode: item.data.int("subtitleLines", default: 1) == 0 ? .byWordWrapping : .byTruncatingTail,
90
+ fonts: fonts)
91
+ let tertiary = NativeListResolvedText(
92
+ item.data.string("tertiary"), style: style.dictionary("tertiary"), size: 14,
93
+ color: .black, lineHeight: 20, lines: 1, fonts: fonts)
94
+ let textHeight = title.measure(width: textWidth)
95
+ + subtitle.measure(width: textWidth)
96
+ + tertiary.measure(width: textWidth)
97
+ + (item.data.string("subtitle").isEmpty ? 0 : CGFloat(style.double("lineGap", default: 0)))
98
+ return max(item.data.string("subtitle").isEmpty ? 56 : 60,
99
+ verticalPadding * 2 + max(imageHeight, textHeight, trailingSize.height))
100
+ }
15
101
  return !item.data.string("tertiary").isEmpty
16
102
  ? 72 : !item.data.string("subtitle").isEmpty ? 60 : 56
17
103
  }
@@ -20,6 +106,7 @@ final class NativeListIdentityCell: NativeListRendererCell {
20
106
  private let mainStack = UIStackView()
21
107
  private let titleRowStack = UIStackView()
22
108
  private let titleLabel = NativeListTextLabel()
109
+ private let titleIconView = UIImageView()
23
110
  private let subtitleLabel = NativeListTextLabel()
24
111
  private let tertiaryLabel = NativeListTextLabel()
25
112
  private let badgeLabel = NativeListInsetLabel()
@@ -75,7 +162,17 @@ final class NativeListIdentityCell: NativeListRendererCell {
75
162
  titleRowStack.axis = .horizontal
76
163
  titleRowStack.alignment = .center
77
164
  titleRowStack.addArrangedSubview(titleLabel)
165
+ titleRowStack.addArrangedSubview(titleIconView)
78
166
  titleRowStack.addArrangedSubview(badgeLabel)
167
+ titleIconView.translatesAutoresizingMaskIntoConstraints = false
168
+ let titleIconWidth = titleIconView.widthAnchor.constraint(equalToConstant: 20)
169
+ titleIconWidth.priority = UILayoutPriority(999)
170
+ NSLayoutConstraint.activate([
171
+ titleIconWidth,
172
+ titleIconView.heightAnchor.constraint(equalToConstant: 20),
173
+ ])
174
+ titleIconView.contentMode = .scaleAspectFit
175
+ titleIconView.setContentHuggingPriority(.required, for: .horizontal)
79
176
  [titleRowStack, subtitleLabel, tertiaryLabel].forEach(mainStack.addArrangedSubview)
80
177
  visual.translatesAutoresizingMaskIntoConstraints = false
81
178
  leadingWidth = visual.widthAnchor.constraint(equalToConstant: 40)
@@ -133,6 +230,10 @@ final class NativeListIdentityCell: NativeListRendererCell {
133
230
  titleRowStack.setCustomSpacing(UIStackView.spacingUseDefault, after: $0)
134
231
  }
135
232
  titleRowStack.spacing = 8
233
+ titleIconView.image = nil
234
+ titleIconView.isHidden = true
235
+ titleIconView.accessibilityLabel = nil
236
+ titleIconView.isAccessibilityElement = false
136
237
  titleRowStack.setContentHuggingPriority(.defaultLow, for: .horizontal)
137
238
  titleRowStack.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
138
239
  for label in [titleLabel, subtitleLabel, tertiaryLabel, badgeLabel] {
@@ -165,6 +266,10 @@ final class NativeListIdentityCell: NativeListRendererCell {
165
266
  leadingWidth.constant = 40
166
267
  leadingHeight.constant = 40
167
268
  visual.glyphSize = 18
269
+ visual.bitmapBorderWidth =
270
+ item.data.dictionary("leading")?.string("borderColor").isEmpty == false
271
+ && item.data.dictionary("leading")?.string("borderStyle") != "dashed"
272
+ ? 1 : 0
168
273
  fallbackLabel.attributedText = nil
169
274
  fallbackLabel.font = nativeListFont(ofSize: 13, weight: .bold, fonts: fonts)
170
275
  trailingStack.reset()
@@ -175,6 +280,31 @@ final class NativeListIdentityCell: NativeListRendererCell {
175
280
  leadingActionButton.setImage(nil, for: .normal)
176
281
  bindIdentity(item, theme: theme, selected: item.data.bool("selected"), checkboxState)
177
282
  let style = item.data.dictionary("style") ?? [:]
283
+ if let titleIcon = item.data.dictionary("titleIcon") {
284
+ titleIconView.image = nativeListIcon(named: titleIcon.string("name"))
285
+ titleIconView.tintColor = UIColor(
286
+ nativeListHex: titleIcon.string("tintColor",
287
+ default: theme?["iconSubdued"] as? String ?? "#8D8D8D"),
288
+ fallback: .gray)
289
+ titleIconView.isHidden = false
290
+ let label = titleIcon.string("accessibilityLabel")
291
+ titleIconView.accessibilityLabel = label.isEmpty ? nil : label
292
+ titleIconView.isAccessibilityElement = !label.isEmpty
293
+ titleRowStack.setCustomSpacing(4, after: titleLabel)
294
+ }
295
+ if let ratio = style["leadingContentMaxWidthRatio"] as? NSNumber,
296
+ root.arrangedSubviews.contains(visual)
297
+ {
298
+ titleLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
299
+ let visualWidth = CGFloat(style.dictionary("image")?.double("width", default: 40) ?? 40)
300
+ let leadingGap = CGFloat(style.double("leadingGap", default: 12))
301
+ let constraint = mainStack.widthAnchor.constraint(
302
+ lessThanOrEqualTo: root.widthAnchor,
303
+ multiplier: CGFloat(ratio.doubleValue),
304
+ constant: -(visualWidth + leadingGap))
305
+ constraint.isActive = true
306
+ selectorConstraints.append(constraint)
307
+ }
178
308
  if style["horizontalPadding"] != nil {
179
309
  leftInset = CGFloat(style.double("horizontalPadding"))
180
310
  rightInset = -leftInset
@@ -196,7 +326,9 @@ final class NativeListIdentityCell: NativeListRendererCell {
196
326
  if walletBadgeLine != nil {
197
327
  mainStack.setCustomSpacing(CGFloat(style.double("titleBadgeGap")), after: titleRowStack)
198
328
  } else {
199
- titleRowStack.setCustomSpacing(CGFloat(style.double("titleBadgeGap")), after: titleLabel)
329
+ titleRowStack.setCustomSpacing(
330
+ CGFloat(style.double("titleBadgeGap")),
331
+ after: titleIconView.isHidden ? titleLabel : titleIconView)
200
332
  }
201
333
  }
202
334
  if style["trailingGap"] != nil { trailingStack.spacing = CGFloat(style.double("trailingGap")) }
@@ -440,9 +572,37 @@ final class NativeListIdentityCell: NativeListRendererCell {
440
572
  }
441
573
  root.addArrangedSubview(mainStack)
442
574
  show(titleLabel, item.data.string("title"), lines: item.data.int("titleLines", default: 1))
575
+ let titleSuffixes = item.data.dictionaries("titleSuffixes")
576
+ if !titleSuffixes.isEmpty {
577
+ titleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
578
+ let minimumTitleWidth = titleLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 80)
579
+ minimumTitleWidth.priority = UILayoutPriority(751)
580
+ minimumTitleWidth.isActive = true
581
+ selectorConstraints.append(minimumTitleWidth)
582
+ for suffix in titleSuffixes {
583
+ let label = NativeListTextLabel()
584
+ label.font = nativeListFont(ofSize: 12, fonts: fonts)
585
+ label.textColor = dataTextColor(suffix.string("tone", default: "secondary"), theme: theme)
586
+ label.numberOfLines = 1
587
+ label.lineBreakMode = .byTruncatingTail
588
+ label.setContentHuggingPriority(.defaultHigh, for: .horizontal)
589
+ label.setContentCompressionResistancePriority(UILayoutPriority(749), for: .horizontal)
590
+ setLineHeight(label, text: suffix.string("text"), lineHeight: 16)
591
+ if let maxWidth = suffix["maxWidth"] as? NSNumber {
592
+ let limit = label.widthAnchor.constraint(lessThanOrEqualToConstant: CGFloat(truncating: maxWidth))
593
+ limit.isActive = true
594
+ selectorConstraints.append(limit)
595
+ }
596
+ titleRowStack.insertArrangedSubview(label, at: titleRowStack.arrangedSubviews.count - 1)
597
+ selectorViews.append(label)
598
+ }
599
+ }
443
600
  show(
444
601
  subtitleLabel, item.data.string("subtitle"), lines: item.data.int("subtitleLines", default: 1)
445
602
  )
603
+ if item.data.int("subtitleLines", default: 1) == 0 {
604
+ subtitleLabel.lineBreakMode = .byWordWrapping
605
+ }
446
606
  show(tertiaryLabel, item.data.string("tertiary"), lines: 1)
447
607
  if !item.data.string("subtitle").isEmpty, item.data.string("tertiary").isEmpty {
448
608
  mainStack.spacing = 0
@@ -556,32 +716,7 @@ final class NativeListIdentityCell: NativeListRendererCell {
556
716
  }
557
717
  root.addArrangedSubview(trailingStack)
558
718
  let accessories = item.data.dictionaries("trailing")
559
- if accessories.contains(where: { $0.string("kind") == "checkbox" })
560
- && accessories.contains(where: { $0.string("kind") == "value" })
561
- {
562
- trailingStack.axis = .horizontal
563
- trailingStack.alignment = .center
564
- trailingStack.spacing = 12
565
- } else if accessories.count == 2,
566
- accessories[0].string("kind") == "valuePair",
567
- accessories[1].string("kind") == "menu"
568
- {
569
- trailingStack.axis = .horizontal
570
- trailingStack.alignment = .center
571
- trailingStack.spacing = 8
572
- } else if accessories.count == 2,
573
- accessories[0].string("kind") == "icon",
574
- accessories[0].string("name") == "PencilOutline",
575
- accessories[1].string("kind") == "icon",
576
- accessories[1].string("name") == "DragOutline"
577
- {
578
- trailingStack.axis = .horizontal
579
- trailingStack.alignment = .center
580
- // Each source IconButton has a 36-point frame and m=-7. XStack gap=$6
581
- // therefore places the physical frames 10 points apart (46-point
582
- // center distance), not 24 points apart.
583
- trailingStack.spacing = 10
584
- }
719
+ Self.configureTrailingStack(trailingStack, descriptors: accessories)
585
720
  trailingStack.bind(
586
721
  item, descriptors: accessories, theme: theme, style: item.data.dictionary("style") ?? [:],
587
722
  defaultSpacing: trailingStack.spacing, checkboxState: checkboxState, fonts: fonts)
@@ -590,12 +725,13 @@ final class NativeListIdentityCell: NativeListRendererCell {
590
725
  trailingStack.isHidden = false
591
726
  if let gap = trailingStack.contentGap { root.setCustomSpacing(gap, after: mainStack) }
592
727
  if let inset = trailingStack.endInset { rightInset = -inset }
593
- if item.data.string("presentation") == "accountSelector",
594
- accessories.count == 1,
728
+ if accessories.count == 1,
729
+ (item.data.string("presentation") == "accountSelector"
730
+ || accessories[0].string("kind") == "valuePair"),
595
731
  let accessory = trailingStack.firstVisibleControl
596
732
  {
597
733
  // The vertical trailing stack has no stable intrinsic width, so keep its
598
- // flexible space in the account title and subtitle column.
734
+ // flexible space in the title and subtitle column.
599
735
  let width = trailingStack.widthAnchor.constraint(equalTo: accessory.widthAnchor)
600
736
  width.isActive = true
601
737
  selectorConstraints.append(width)