@onekeyfe/react-native-native-list 6.7.0-alpha.378 → 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
@@ -3,12 +3,72 @@ import UIKit
3
3
  // OneKey patch: explicit summary actions use the source text's physical-pixel line box.
4
4
  final class NativeListAccessoryButton: UIButton {
5
5
  var pressedBackgroundColor: UIColor?
6
+ var normalBackgroundColor: UIColor = .clear
7
+ var fixedTextLineHeight: CGFloat?
6
8
  var rowTextOffsetY: CGFloat = 0
9
+ var helpBindingGeneration = 0
10
+ private var helpTrackingGeneration = 0
11
+ override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
12
+ helpTrackingGeneration = helpBindingGeneration
13
+ return super.beginTracking(touch, with: event)
14
+ }
15
+ override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
16
+ guard helpTrackingGeneration == helpBindingGeneration else {
17
+ cancelTracking(with: event)
18
+ return
19
+ }
20
+ super.endTracking(touch, with: event)
21
+ }
22
+ var helpPrimaryLineOnly = false
23
+ var showsHelpUnderline = false
24
+ let helpAnchor = UIView()
25
+ private let helpUnderline = CAShapeLayer()
26
+
27
+ override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
28
+ if helpPrimaryLineOnly { return helpAnchor.frame.contains(point) }
29
+ return super.point(inside: point, with: event)
30
+ }
31
+
32
+ private func layoutHelp() {
33
+ guard helpPrimaryLineOnly || showsHelpUnderline else {
34
+ helpUnderline.isHidden = true
35
+ return
36
+ }
37
+ guard let titleLabel else { return }
38
+ if helpAnchor.superview == nil {
39
+ helpAnchor.isUserInteractionEnabled = false
40
+ helpAnchor.isAccessibilityElement = false
41
+ addSubview(helpAnchor)
42
+ helpUnderline.fillColor = UIColor.clear.cgColor
43
+ helpUnderline.lineWidth = 1
44
+ helpUnderline.lineDashPattern = [3, 2]
45
+ helpAnchor.layer.addSublayer(helpUnderline)
46
+ }
47
+ var frame = titleLabel.frame
48
+ if helpPrimaryLineOnly, let text = attributedTitle(for: .normal) {
49
+ let range = (text.string as NSString).range(of: "\n")
50
+ let primary = text.attributedSubstring(from: NSRange(location: 0, length: range.location == NSNotFound ? text.length : range.location))
51
+ let width = min(frame.width, ceil(primary.size().width))
52
+ frame.origin.x += frame.width - width
53
+ frame.size.width = width
54
+ let paragraph = primary.length > 0 ? primary.attribute(.paragraphStyle, at: 0, effectiveRange: nil) as? NSParagraphStyle : nil
55
+ frame.size.height = paragraph?.minimumLineHeight ?? 20
56
+ }
57
+ helpAnchor.frame = frame
58
+ let text = attributedTitle(for: .normal)
59
+ let color = text.flatMap { $0.length > 0 ? $0.attribute(.foregroundColor, at: 0, effectiveRange: nil) as? UIColor : nil }
60
+ helpUnderline.strokeColor = (color ?? titleColor(for: .normal))?.cgColor
61
+ helpUnderline.isHidden = !showsHelpUnderline
62
+ let path = UIBezierPath()
63
+ path.move(to: CGPoint(x: 0, y: frame.height - 1))
64
+ path.addLine(to: CGPoint(x: frame.width, y: frame.height - 1))
65
+ helpUnderline.path = path.cgPath
66
+ }
7
67
 
8
68
  override var isHighlighted: Bool {
9
69
  didSet {
10
70
  guard let pressedBackgroundColor else { return }
11
- backgroundColor = isHighlighted ? pressedBackgroundColor : .clear
71
+ backgroundColor = isHighlighted ? pressedBackgroundColor : normalBackgroundColor
12
72
  }
13
73
  }
14
74
 
@@ -32,6 +92,13 @@ final class NativeListAccessoryButton: UIButton {
32
92
  }
33
93
  }
34
94
 
95
+ func clearTitlesForReuse() {
96
+ UIView.performWithoutAnimation {
97
+ super.setAttributedTitle(nil, for: .normal)
98
+ super.setTitle(nil, for: .normal)
99
+ }
100
+ }
101
+
35
102
  var selectorSummaryLineHeight: CGFloat? {
36
103
  didSet {
37
104
  invalidateIntrinsicContentSize()
@@ -52,6 +119,11 @@ final class NativeListAccessoryButton: UIButton {
52
119
 
53
120
  override var intrinsicContentSize: CGSize {
54
121
  var size = super.intrinsicContentSize
122
+ if let fixedTextLineHeight {
123
+ size.width += 2
124
+ size.height = fixedTextLineHeight + contentEdgeInsets.top + contentEdgeInsets.bottom + 2
125
+ return size
126
+ }
55
127
  guard selectorSummaryLineHeight != nil || marketLineHeight != nil,
56
128
  let title = attributedTitle(for: .normal)
57
129
  else { return size }
@@ -71,6 +143,10 @@ final class NativeListAccessoryButton: UIButton {
71
143
  defer {
72
144
  if let titleLabel {
73
145
  var frame = titleLabel.frame
146
+ if let fixedTextLineHeight {
147
+ frame.size.height = fixedTextLineHeight
148
+ frame.origin.y = (bounds.height - fixedTextLineHeight) / 2
149
+ }
74
150
  if contentVerticalAlignment == .top {
75
151
  frame.origin.y = contentEdgeInsets.top
76
152
  } else if contentVerticalAlignment == .bottom {
@@ -79,6 +155,7 @@ final class NativeListAccessoryButton: UIButton {
79
155
  frame.origin.y += rowTextOffsetY
80
156
  titleLabel.frame = frame
81
157
  }
158
+ layoutHelp()
82
159
  }
83
160
  if let lineHeight = marketLineHeight, let titleLabel, let title = attributedTitle(for: .normal)
84
161
  {
@@ -127,6 +204,8 @@ final class NativeListAccessoryStack: UIStackView {
127
204
  private var boundCheckboxData: [String: Any]?
128
205
  private var boundCheckboxTarget: NativeSelectionTarget?
129
206
  private var semanticValueButtons: [UIButton] = []
207
+ private var valuePairSubscriptRanges: [ObjectIdentifier: [(NSRange, Bool)]] = [:]
208
+ private var tertiaryIconButtons: Set<ObjectIdentifier> = []
130
209
  private var accessorySizeConstraints: [NSLayoutConstraint] = []
131
210
  private var currentItem: NativeListItem?
132
211
  private var currentTheme: [String: Any]?
@@ -210,14 +289,19 @@ final class NativeListAccessoryStack: UIStackView {
210
289
  }
211
290
  }
212
291
  func anchorInset(for view: UIView) -> CGFloat {
213
- currentItem?.data.string("presentation") == "accountSelector"
214
- && accessoryButtons.contains { $0 === view } && view.bounds.width == 38 ? 7 : 0
292
+ guard accessoryButtons.contains(where: { $0 === view }), view.bounds.width == 38 else {
293
+ return 0
294
+ }
295
+ if tertiaryIconButtons.contains(ObjectIdentifier(view)) { return 7 }
296
+ return currentItem?.data.string("presentation") == "accountSelector" ? 7 : 0
215
297
  }
216
298
  func reset() {
217
299
  NSLayoutConstraint.deactivate(accessorySizeConstraints)
218
300
  accessorySizeConstraints.removeAll()
219
301
  accessoryActions.removeAll()
220
302
  semanticValueButtons.removeAll()
303
+ valuePairSubscriptRanges.removeAll()
304
+ tertiaryIconButtons.removeAll()
221
305
  checkboxAction = nil
222
306
  boundCheckboxData = nil
223
307
  boundCheckboxTarget = nil
@@ -225,8 +309,8 @@ final class NativeListAccessoryStack: UIStackView {
225
309
  contentGap = nil
226
310
  currentItem = nil
227
311
  for (index, button) in accessoryButtons.enumerated() {
228
- button.setAttributedTitle(nil, for: .normal)
229
- button.setTitle(nil, for: .normal)
312
+ button.tag = index
313
+ button.clearTitlesForReuse()
230
314
  button.setImage(nil, for: .normal)
231
315
  button.setImage(nil, for: .disabled)
232
316
  button.titleLabel?.font = nativeListFont(
@@ -236,11 +320,19 @@ final class NativeListAccessoryStack: UIStackView {
236
320
  button.contentHorizontalAlignment = .center
237
321
  button.contentVerticalAlignment = .center
238
322
  button.rowTextOffsetY = 0
323
+ button.helpBindingGeneration += 1
324
+ button.helpPrimaryLineOnly = false
325
+ button.showsHelpUnderline = false
239
326
  button.transform = .identity
240
327
  button.marketLineHeight = nil
241
328
  button.selectorSummaryLineHeight = nil
242
329
  button.backgroundColor = .clear
243
330
  button.pressedBackgroundColor = nil
331
+ button.normalBackgroundColor = .clear
332
+ button.fixedTextLineHeight = nil
333
+ button.contentEdgeInsets = .zero
334
+ button.layer.borderWidth = 0
335
+ button.layer.borderColor = nil
244
336
  button.layer.cornerRadius = 0
245
337
  button.isHidden = true
246
338
  button.isEnabled = true
@@ -254,9 +346,13 @@ final class NativeListAccessoryStack: UIStackView {
254
346
  spinner.alpha = 1
255
347
  }
256
348
  @objc private func pressed(_ button: UIButton) {
257
- guard accessoryActions.indices.contains(button.tag) else { return }
258
- let action = accessoryActions[button.tag]
259
- if !action.0.isEmpty { onAction?(action.0, button, button.tag, action.1) }
349
+ guard let controlIndex = accessoryButtons.firstIndex(where: { $0 === button }),
350
+ accessoryActions.indices.contains(controlIndex) else { return }
351
+ let action = accessoryActions[controlIndex]
352
+ if !action.0.isEmpty {
353
+ let anchor = accessoryButtons[controlIndex]
354
+ onAction?(action.0, anchor.helpPrimaryLineOnly ? anchor.helpAnchor : button, button.tag, action.1)
355
+ }
260
356
  }
261
357
  @objc private func checkboxPressed() {
262
358
  if let action = checkboxAction { onAction?(action.0, checkboxButton, nil, action.1) }
@@ -268,8 +364,42 @@ final class NativeListAccessoryStack: UIStackView {
268
364
  _ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
269
365
  ) {
270
366
  var textIndex = 0
271
- for accessory in accessories.prefix(2) {
367
+ for (slot, accessory) in accessories.prefix(2).enumerated() {
368
+ let controlIndex = textIndex
272
369
  switch accessory.string("kind") {
370
+ case "button":
371
+ showAccessory(
372
+ textIndex, accessory.string("label"),
373
+ action: accessory.bool("disabled") || item.data.bool("disabled")
374
+ ? nil : (accessory.string("actionKey"), nil))
375
+ let button = accessoryButtons[textIndex]
376
+ button.titleLabel?.font = nativeListFont(ofSize: 14, weight: .medium, fonts: fonts)
377
+ button.fixedTextLineHeight = 20
378
+ button.contentEdgeInsets = UIEdgeInsets(top: 4, left: 10, bottom: 4, right: 10)
379
+ button.layer.borderWidth = 1
380
+ button.layer.borderColor = UIColor.clear.cgColor
381
+ button.layer.cornerRadius = 15
382
+ button.backgroundColor = UIColor(
383
+ nativeListHex: accessory.string("backgroundColor",
384
+ default: theme?["strongBackground"] as? String ?? "#F0F0F0"),
385
+ fallback: .clear)
386
+ button.normalBackgroundColor = button.backgroundColor ?? .clear
387
+ button.pressedBackgroundColor = UIColor(
388
+ nativeListHex: accessory.string("pressedBackgroundColor",
389
+ default: theme?["rowPressedBackground"] as? String ?? "#E4E4E4"),
390
+ fallback: .clear)
391
+ button.setTitleColor(
392
+ UIColor(
393
+ nativeListHex: accessory.string("textColor",
394
+ default: theme?["primaryText"] as? String ?? "#202020"),
395
+ fallback: .darkGray),
396
+ for: .normal)
397
+ button.accessibilityIdentifier = accessory.string("testID")
398
+ button.accessibilityLabel =
399
+ accessory.string("accessibilityLabel", default: accessory.string("label"))
400
+ button.isEnabled = !item.data.bool("disabled") && !accessory.bool("disabled")
401
+ button.alpha = button.isEnabled ? 1 : 0.4
402
+ textIndex += 1
273
403
  case "value":
274
404
  showAccessory(textIndex, accessory.string("text"))
275
405
  semanticValueButtons.append(accessoryButtons[textIndex])
@@ -326,6 +456,7 @@ final class NativeListAccessoryStack: UIStackView {
326
456
  textIndex += 1
327
457
  default: break
328
458
  }
459
+ if textIndex > controlIndex { accessoryButtons[controlIndex].tag = slot }
329
460
  }
330
461
  }
331
462
 
@@ -411,8 +542,12 @@ final class NativeListAccessoryStack: UIStackView {
411
542
  theme: [String: Any]?
412
543
  ) {
413
544
  guard accessoryButtons.indices.contains(index) else { return }
414
- let primary = accessory.string("primary")
415
- let secondary = accessory.string("secondary")
545
+ let primarySegments = accessory.dictionaries("primaryTextSegments")
546
+ let secondarySegments = accessory.dictionaries("secondaryTextSegments")
547
+ let primary = primarySegments.isEmpty
548
+ ? accessory.string("primary") : primarySegments.map { $0.string("text") }.joined()
549
+ let secondary = secondarySegments.isEmpty
550
+ ? accessory.string("secondary") : secondarySegments.map { $0.string("text") }.joined()
416
551
  guard !primary.isEmpty || !secondary.isEmpty else { return }
417
552
  let button = accessoryButtons[index]
418
553
  // Value pairs never carry an action; the row owns their touches.
@@ -422,45 +557,66 @@ final class NativeListAccessoryStack: UIStackView {
422
557
  primaryParagraph.alignment = .right
423
558
  primaryParagraph.minimumLineHeight = 20
424
559
  primaryParagraph.maximumLineHeight = 20
425
- text.append(
426
- NSAttributedString(
427
- string: primary,
428
- attributes: [
429
- .font: nativeListFont(ofSize: 16, weight: .medium, fonts: fonts),
430
- .foregroundColor: accessoryTextColor(
431
- accessory.string("primaryTone"),
432
- defaultTone: "primary",
433
- theme: theme
434
- ),
435
- .paragraphStyle: primaryParagraph,
436
- ]
437
- ))
560
+ var subscriptRanges: [(NSRange, Bool)] = []
561
+ appendValuePairLine(
562
+ to: text, fallback: primary, segments: primarySegments, fontSize: 16,
563
+ weight: .medium,
564
+ color: accessoryTextColor(accessory.string("primaryTone"), defaultTone: "primary", theme: theme),
565
+ paragraph: primaryParagraph, isPrimary: true, subscriptRanges: &subscriptRanges)
438
566
  if !primary.isEmpty, !secondary.isEmpty { text.append(NSAttributedString(string: "\n")) }
439
567
  let secondaryParagraph = NSMutableParagraphStyle()
440
568
  secondaryParagraph.alignment = .right
441
569
  secondaryParagraph.minimumLineHeight = 20
442
570
  secondaryParagraph.maximumLineHeight = 20
443
- text.append(
444
- NSAttributedString(
445
- string: secondary,
446
- attributes: [
447
- .font: nativeListFont(ofSize: 14, fonts: fonts),
448
- .foregroundColor: accessoryTextColor(
449
- accessory.string("secondaryTone"),
450
- defaultTone: "secondary",
451
- theme: theme
452
- ),
453
- .paragraphStyle: secondaryParagraph,
454
- ]
455
- ))
571
+ appendValuePairLine(
572
+ to: text, fallback: secondary, segments: secondarySegments, fontSize: 14,
573
+ weight: .regular,
574
+ color: accessoryTextColor(accessory.string("secondaryTone"), defaultTone: "secondary", theme: theme),
575
+ paragraph: secondaryParagraph, isPrimary: false, subscriptRanges: &subscriptRanges)
456
576
  button.isHidden = false
457
577
  button.titleLabel?.numberOfLines = 2
458
578
  button.contentHorizontalAlignment = .trailing
459
579
  button.setAttributedTitle(text, for: .normal)
580
+ valuePairSubscriptRanges[ObjectIdentifier(button)] = subscriptRanges
460
581
  button.setContentHuggingPriority(.required, for: .horizontal)
461
582
  button.setContentCompressionResistancePriority(.required, for: .horizontal)
462
583
  while accessoryActions.count <= index { accessoryActions.append(("", nil)) }
463
584
  accessoryActions[index] = ("", nil)
585
+ if let help = accessory.dictionary("primaryHelp") {
586
+ button.helpPrimaryLineOnly = true
587
+ button.showsHelpUnderline = help.string("underline") == "dashed"
588
+ button.isUserInteractionEnabled = help.string("presentation") == "popover"
589
+ accessoryActions[index] = (help.string("presentation") == "popover" ? help.string("actionKey") : "", nil)
590
+ button.setNeedsLayout()
591
+ }
592
+ }
593
+
594
+ private func appendValuePairLine(
595
+ to text: NSMutableAttributedString,
596
+ fallback: String,
597
+ segments: [[String: Any]],
598
+ fontSize: CGFloat,
599
+ weight: NativeListFontWeight,
600
+ color: UIColor,
601
+ paragraph: NSParagraphStyle,
602
+ isPrimary: Bool,
603
+ subscriptRanges: inout [(NSRange, Bool)]
604
+ ) {
605
+ let runs = segments.isEmpty ? [["text": fallback]] : segments
606
+ for run in runs {
607
+ let content = run.string("text")
608
+ let isSubscript = run.string("style") == "subscript"
609
+ let start = text.length
610
+ text.append(NSAttributedString(string: content, attributes: [
611
+ .font: nativeListFont(
612
+ ofSize: isSubscript ? ceil(fontSize * 0.6) : fontSize, weight: weight, fonts: fonts),
613
+ .foregroundColor: color,
614
+ .paragraphStyle: paragraph,
615
+ ]))
616
+ if isSubscript && text.length > start {
617
+ subscriptRanges.append((NSRange(location: start, length: text.length - start), isPrimary))
618
+ }
619
+ }
464
620
  }
465
621
 
466
622
  private func showMenuAccessory(
@@ -508,7 +664,12 @@ final class NativeListAccessoryStack: UIStackView {
508
664
  ? nativeListColor(currentTheme, "iconSubdued", "#8D8D8D")
509
665
  : UIColor(nativeListHex: data.string("tintColor", default: "#646464"), fallback: .darkGray)
510
666
  button.tintColor = tintColor
511
- if let image = nativeListIcon(named: data.string("name")) {
667
+ let isDecorativeRefresh =
668
+ data.string("name") == "RefreshCcwOutline" && data.string("actionKey").isEmpty
669
+ let image = isDecorativeRefresh
670
+ ? nativeListIcon(named: data.string("name"), size: CGSize(width: 16, height: 16))
671
+ : nativeListIcon(named: data.string("name"))
672
+ if let image {
512
673
  button.setImage(image, for: .normal)
513
674
  button.setImage(
514
675
  image.withTintColor(tintColor, renderingMode: .alwaysOriginal),
@@ -516,8 +677,22 @@ final class NativeListAccessoryStack: UIStackView {
516
677
  )
517
678
  }
518
679
  let isDrillIn = data.string("kind") == "chevron"
680
+ let isTertiary = data.string("buttonStyle") == "tertiary"
681
+ if data.string("kind") == "icon" && isTertiary {
682
+ tertiaryIconButtons.insert(ObjectIdentifier(button))
683
+ }
519
684
  let isAccountIcon = currentItem?.data.string("presentation") == "accountSelector" && !isDrillIn
520
- let size: CGFloat = isDrillIn ? 24 : isAccountIcon && !isAccountCreate ? 38 : 36
685
+ button.isUserInteractionEnabled = !isDecorativeRefresh
686
+ let size: CGFloat =
687
+ isDecorativeRefresh ? 16 : isDrillIn ? 24 : isTertiary || isAccountIcon && !isAccountCreate ? 38 : 36
688
+ if isTertiary {
689
+ button.pressedBackgroundColor = nativeListColor(
690
+ currentTheme, "rowPressedBackground", "#E8E8E8")
691
+ button.layer.borderWidth = 1
692
+ button.layer.borderColor = UIColor.clear.cgColor
693
+ button.layer.cornerRadius = 19
694
+ button.contentEdgeInsets = UIEdgeInsets(top: 6, left: 6, bottom: 6, right: 6)
695
+ }
521
696
  if isAccountCreate { button.layer.cornerRadius = 8 }
522
697
  if isAccountIcon && !isAccountCreate && !data.string("actionKey").isEmpty {
523
698
  button.pressedBackgroundColor = nativeListColor(
@@ -655,6 +830,19 @@ final class NativeListAccessoryStack: UIStackView {
655
830
  }
656
831
  button.titleLabel?.font = font
657
832
  attributed.addAttributes(attributes, range: NSRange(location: 0, length: attributed.length))
833
+ if style["fontSize"] != nil,
834
+ let ranges = valuePairSubscriptRanges[ObjectIdentifier(button)]
835
+ {
836
+ for (range, isPrimary) in ranges {
837
+ let weight = style["fontWeight"] == nil
838
+ ? (isPrimary ? NativeListFontWeight.medium : NativeListFontWeight.regular)
839
+ : marketFontWeight(style.string("fontWeight"), fallback: .regular)
840
+ attributed.addAttribute(
841
+ .font,
842
+ value: nativeListFont(ofSize: ceil(size * 0.6), weight: weight, fonts: fonts),
843
+ range: range)
844
+ }
845
+ }
658
846
  button.setAttributedTitle(attributed, for: .normal)
659
847
  }
660
848
 
@@ -70,8 +70,10 @@ final class NativeListActivityDetailsView: UIStackView {
70
70
  let verticalPadding = CGFloat(style.double("verticalPadding", default: 8))
71
71
  let lineGap = CGFloat(style.double("lineGap"))
72
72
  let gap = CGFloat(style.double("leadingGap", default: 12))
73
+ let hideLeading = data.bool("hideLeading")
73
74
  let imageWidth = CGFloat(image.double("width", default: 40))
74
75
  let imageHeight = CGFloat(image.double("height", default: 40))
76
+ let leadingWidth = hideLeading ? 0 : imageWidth + gap
75
77
  let mainWidth = max(0, width - horizontalPadding * 2)
76
78
 
77
79
  let amounts = data.dictionaries("amounts").map { amount -> (width: CGFloat, height: CGFloat) in
@@ -112,10 +114,10 @@ final class NativeListActivityDetailsView: UIStackView {
112
114
  let feeColumn = data.dictionary("fee") == nil ? 0 : gap + feeWidth
113
115
  let identityWidth: CGFloat
114
116
  if table {
115
- let shared = (mainWidth - imageWidth - gap * 2 - feeColumn) / 2
117
+ let shared = (mainWidth - leadingWidth - gap - feeColumn) / 2
116
118
  identityWidth = min(mainWidth * 0.42, shared)
117
119
  } else {
118
- identityWidth = mainWidth - imageWidth - gap * 2 - feeColumn - min(amountsWidth, mainWidth * 0.5)
120
+ identityWidth = mainWidth - leadingWidth - gap - feeColumn - min(amountsWidth, mainWidth * 0.5)
119
121
  }
120
122
 
121
123
  let title = NativeListResolvedText(data.string("title"), style: style.dictionary("title"), size: 16,
@@ -140,7 +142,7 @@ final class NativeListActivityDetailsView: UIStackView {
140
142
  return max(height, ceil(sizingButton.intrinsicContentSize.height))
141
143
  } + 8
142
144
  }
143
- return max(imageHeight, identityHeight, amountsHeight, feeHeight) + actionsHeight + verticalPadding * 2
145
+ return max(hideLeading ? 0 : imageHeight, identityHeight, amountsHeight, feeHeight) + actionsHeight + verticalPadding * 2
144
146
  }
145
147
 
146
148
  func bind(_ item: NativeListItem, theme: [String: Any]?, fonts: NativeListFontFamilies) {
@@ -150,20 +152,28 @@ final class NativeListActivityDetailsView: UIStackView {
150
152
  let image = style.dictionary("image") ?? [:]
151
153
  let table = data.string("presentation") == "table"
152
154
  let gap = CGFloat(style.double("leadingGap", default: 12))
155
+ let hideLeading = data.bool("hideLeading")
153
156
  main.spacing = gap
154
157
  identity.spacing = CGFloat(style.double("lineGap"))
155
158
  amounts.spacing = CGFloat(style.double("trailingGap", default: 2))
156
159
  titleLine.spacing = CGFloat(style.double("titleBadgeGap", default: 4))
157
160
  NSLayoutConstraint.deactivate(dimensions)
161
+ let visualWidth = visual.widthAnchor.constraint(equalToConstant: CGFloat(image.double("width", default: 40)))
162
+ if hideLeading { visualWidth.priority = UILayoutPriority(999) }
158
163
  dimensions = [
159
- visual.widthAnchor.constraint(equalToConstant: CGFloat(image.double("width", default: 40))),
164
+ visualWidth,
160
165
  visual.heightAnchor.constraint(equalToConstant: CGFloat(image.double("height", default: 40))),
161
166
  amounts.widthAnchor.constraint(lessThanOrEqualTo: main.widthAnchor, multiplier: table ? 0.42 : 0.5),
162
167
  ]
163
168
  if table { dimensions.append(amounts.widthAnchor.constraint(equalTo: identity.widthAnchor)) }
164
169
  NSLayoutConstraint.activate(dimensions)
165
- visual.bind(data.dictionary("leading") ?? [:], style: image, key: item.key, theme: theme,
166
- isUnread: false, secondaryVisual: data.dictionary("secondaryLeading"), fonts: fonts)
170
+ visual.isHidden = hideLeading
171
+ if hideLeading {
172
+ visual.recycle()
173
+ } else {
174
+ visual.bind(data.dictionary("leading") ?? [:], style: image, key: item.key, theme: theme,
175
+ isUnread: false, secondaryVisual: data.dictionary("secondaryLeading"), fonts: fonts)
176
+ }
167
177
  NativeListResolvedText(data.string("title"), style: style.dictionary("title"), size: 16,
168
178
  weight: .medium, color: nativeListColor(theme, "primaryText", "#202020"), lineHeight: 24,
169
179
  lines: 1, fonts: fonts
@@ -225,7 +235,7 @@ final class NativeListActivityDetailsView: UIStackView {
225
235
  let actionData = data.dictionaries("footerActions")
226
236
  actionKeys = actionData.map { $0.string("key") }
227
237
  actions.isHidden = actionData.isEmpty
228
- actions.directionalLayoutMargins = NSDirectionalEdgeInsets(top: 0, leading: CGFloat(image.double("width", default: 40)) + gap, bottom: 0, trailing: 0)
238
+ actions.directionalLayoutMargins = NSDirectionalEdgeInsets(top: 0, leading: hideLeading ? 0 : CGFloat(image.double("width", default: 40)) + gap, bottom: 0, trailing: 0)
229
239
  for (index, descriptor) in actionData.enumerated() {
230
240
  let button = UIButton(type: .system); button.tag = index
231
241
  button.setTitle(descriptor.string("label"), for: .normal)
@@ -249,7 +259,7 @@ final class NativeListActivityDetailsView: UIStackView {
249
259
  onAction?(actionKeys[button.tag], button, "footerAction", button.tag)
250
260
  }
251
261
  func recycle() {
252
- visual.recycle(); amountViews.forEach { $0.recycle() }; actionKeys = []; descriptionKey = ""
262
+ visual.recycle(); visual.isHidden = false; amountViews.forEach { $0.recycle() }; actionKeys = []; descriptionKey = ""
253
263
  }
254
264
  }
255
265
 
@@ -97,6 +97,7 @@ final class NativeListActivityCell: NativeListRendererCell {
97
97
  details.recycle()
98
98
  let style = data.dictionary("style") ?? [:]
99
99
  let image = style.dictionary("image") ?? [:]
100
+ let hideLeading = data.bool("hideLeading")
100
101
  root.spacing = 12
101
102
  root.setCustomSpacing(CGFloat(style.double("leadingGap", default: 12)), after: visual)
102
103
  switch style.dictionary("container")?.string("contentVerticalAlignment") {
@@ -113,12 +114,17 @@ final class NativeListActivityCell: NativeListRendererCell {
113
114
  bottom: CGFloat(style.double("verticalPadding", default: 8)),
114
115
  right: CGFloat(style.double("horizontalPadding", default: layout == "table" ? 16 : 12)))
115
116
  NSLayoutConstraint.deactivate(dimensions)
117
+ let visualWidth = visual.widthAnchor.constraint(equalToConstant: CGFloat(image.double("width", default: 40)))
118
+ if hideLeading { visualWidth.priority = UILayoutPriority(999) }
116
119
  dimensions = [
117
- visual.widthAnchor.constraint(equalToConstant: CGFloat(image.double("width", default: 40))),
120
+ visualWidth,
118
121
  visual.heightAnchor.constraint(equalToConstant: CGFloat(image.double("height", default: 40))),
119
122
  ]
120
123
  NSLayoutConstraint.activate(dimensions)
121
- if let leading = data.dictionary("leading") {
124
+ visual.isHidden = hideLeading
125
+ if hideLeading {
126
+ visual.recycle()
127
+ } else if let leading = data.dictionary("leading") {
122
128
  visual.bind(
123
129
  leading, style: image, key: item.key, theme: theme, isUnread: false,
124
130
  secondaryVisual: data.dictionary("secondaryLeading"), fonts: fonts)
@@ -0,0 +1,89 @@
1
+ import UIKit
2
+
3
+ final class NativeListButtonGroupCell: NativeListRendererCell {
4
+ override class func measure(
5
+ _ item: NativeListItem, width: CGFloat, theme: [String: Any]?, layout: String,
6
+ fonts: NativeListFontFamilies
7
+ ) -> CGFloat? { 64 }
8
+
9
+ private var buttons: [UIButton] = []
10
+ private var actionKeys: [String] = []
11
+ private var rowOpacity: CGFloat = 1
12
+
13
+ override func applyAppearance() {
14
+ super.applyAppearance()
15
+ contentView.alpha = rowOpacity
16
+ }
17
+
18
+ override init(frame: CGRect) {
19
+ super.init(frame: frame)
20
+ root.axis = .horizontal
21
+ root.alignment = .fill
22
+ root.distribution = .fillEqually
23
+ root.spacing = 10
24
+ }
25
+ required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
26
+
27
+ override func bindContent(
28
+ _ item: NativeListItem, theme: [String: Any]?, layout: String,
29
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
30
+ ) {
31
+ recycleContent()
32
+ rowOpacity = CGFloat(item.data.dictionary("style")?.dictionary("container")?.double("opacity", default: item.data.double("opacity", default: 1)) ?? item.data.double("opacity", default: 1))
33
+ contentInsets = UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 0)
34
+ let info = nativeListColor(theme, "info", "#0F69F6")
35
+ for (index, descriptor) in item.data.dictionaries("buttons").enumerated() {
36
+ let button = NativeListAccessoryButton(type: .custom)
37
+ button.tag = index
38
+ button.setTitle(descriptor.string("label"), for: .normal)
39
+ button.titleLabel?.font = nativeListFont(ofSize: 14, weight: .medium, fonts: fonts)
40
+ button.titleLabel?.lineBreakMode = .byTruncatingTail
41
+ button.setTitleColor(info, for: .normal)
42
+ button.contentEdgeInsets = UIEdgeInsets(top: 12, left: 8, bottom: 12, right: 8)
43
+ button.layer.cornerRadius = 12
44
+ button.layer.borderWidth = 1
45
+ button.layer.borderColor = info.withAlphaComponent(0.35).cgColor
46
+ button.normalBackgroundColor = .clear
47
+ button.pressedBackgroundColor = info.withAlphaComponent(0.17)
48
+ button.isEnabled = !item.data.bool("disabled") && !descriptor.bool("disabled") && !descriptor.bool("loading")
49
+ button.alpha = button.isEnabled ? 1 : 0.4
50
+ button.accessibilityIdentifier = descriptor["testID"] as? String
51
+ button.accessibilityLabel = descriptor.string("accessibilityLabel", default: descriptor.string("label"))
52
+ button.addTarget(self, action: #selector(pressed(_:)), for: .touchUpInside)
53
+ if descriptor.bool("loading"), let titleLabel = button.titleLabel {
54
+ let spinner = UIActivityIndicatorView(style: .medium)
55
+ spinner.color = info
56
+ spinner.isUserInteractionEnabled = false
57
+ spinner.translatesAutoresizingMaskIntoConstraints = false
58
+ button.addSubview(spinner)
59
+ button.contentEdgeInsets = UIEdgeInsets(top: 12, left: 36, bottom: 12, right: 8)
60
+ NSLayoutConstraint.activate([
61
+ spinner.trailingAnchor.constraint(equalTo: titleLabel.leadingAnchor, constant: -8),
62
+ spinner.centerYAnchor.constraint(equalTo: button.centerYAnchor),
63
+ spinner.widthAnchor.constraint(equalToConstant: 20),
64
+ spinner.heightAnchor.constraint(equalToConstant: 20),
65
+ ])
66
+ spinner.startAnimating()
67
+ }
68
+ buttons.append(button)
69
+ actionKeys.append(descriptor.string("actionKey"))
70
+ root.addArrangedSubview(button)
71
+ }
72
+ }
73
+
74
+ @objc private func pressed(_ button: UIButton) {
75
+ guard button.isEnabled, buttons.indices.contains(button.tag), buttons[button.tag] === button else { return }
76
+ emitAction(actionKeys[button.tag], from: button, source: "buttonGroup", slot: button.tag)
77
+ }
78
+
79
+ override func recycleContent() {
80
+ for button in buttons {
81
+ button.removeTarget(self, action: nil, for: .allEvents)
82
+ button.subviews.compactMap { $0 as? UIActivityIndicatorView }.forEach { $0.stopAnimating() }
83
+ root.removeArrangedSubview(button)
84
+ button.removeFromSuperview()
85
+ }
86
+ buttons.removeAll()
87
+ actionKeys.removeAll()
88
+ }
89
+ }