@onekeyfe/react-native-native-list 3.0.157-alpha.249 → 3.0.157-alpha.251

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 (130) hide show
  1. package/README.md +21 -5
  2. package/android/src/main/java/com/onekey/nativelist/NativeListAccessoryStack.kt +711 -0
  3. package/android/src/main/java/com/onekey/nativelist/NativeListActionRenderer.kt +169 -0
  4. package/android/src/main/java/com/onekey/nativelist/NativeListActivityRenderer.kt +243 -0
  5. package/android/src/main/java/com/onekey/nativelist/NativeListAdapter.kt +20 -6
  6. package/android/src/main/java/com/onekey/nativelist/NativeListDataRowRenderer.kt +487 -0
  7. package/android/src/main/java/com/onekey/nativelist/NativeListIdentityRenderer.kt +820 -0
  8. package/android/src/main/java/com/onekey/nativelist/NativeListImageSlot.kt +147 -0
  9. package/android/src/main/java/com/onekey/nativelist/NativeListLeadingVisual.kt +453 -0
  10. package/android/src/main/java/com/onekey/nativelist/NativeListMarketRenderer.kt +997 -0
  11. package/android/src/main/java/com/onekey/nativelist/NativeListMediaTileRenderer.kt +234 -0
  12. package/android/src/main/java/com/onekey/nativelist/NativeListMessageRenderer.kt +232 -0
  13. package/android/src/main/java/com/onekey/nativelist/NativeListMetricCardRenderer.kt +551 -0
  14. package/android/src/main/java/com/onekey/nativelist/NativeListModels.kt +24 -0
  15. package/android/src/main/java/com/onekey/nativelist/NativeListPrimitives.kt +475 -0
  16. package/android/src/main/java/com/onekey/nativelist/NativeListRailRenderer.kt +143 -0
  17. package/android/src/main/java/com/onekey/nativelist/NativeListRendererRegistry.kt +42 -0
  18. package/android/src/main/java/com/onekey/nativelist/NativeListRendererRowView.kt +415 -0
  19. package/android/src/main/java/com/onekey/nativelist/NativeListResolvedText.kt +163 -0
  20. package/android/src/main/java/com/onekey/nativelist/NativeListRowHost.kt +70 -0
  21. package/android/src/main/java/com/onekey/nativelist/NativeListSectionHeaderRenderer.kt +748 -0
  22. package/android/src/main/java/com/onekey/nativelist/NativeListSystemRenderer.kt +464 -0
  23. package/android/src/main/java/com/onekey/nativelist/NativeListTextView.kt +25 -0
  24. package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +174 -104
  25. package/android/src/main/java/com/onekey/nativelist/NativeListWalletGroupRenderer.kt +407 -0
  26. package/android/src/test/java/com/margelo/nitro/nativelist/NativeListSelectorBackgroundColorTest.kt +34 -0
  27. package/docs/DESIGN.md +518 -9
  28. package/docs/MIGRATION_BASELINE.md +117 -0
  29. package/docs/ROW_TEMPLATES.md +359 -0
  30. package/docs/SPEC.md +358 -0
  31. package/docs/STYLE_SPEC.md +978 -0
  32. package/docs/images/list-capabilities.svg +39 -0
  33. package/docs/images/row-action.svg +17 -0
  34. package/docs/images/row-activity.svg +24 -0
  35. package/docs/images/row-data.svg +24 -0
  36. package/docs/images/row-identity.svg +25 -0
  37. package/docs/images/row-market.svg +23 -0
  38. package/docs/images/row-media-tile.svg +26 -0
  39. package/docs/images/row-message.svg +20 -0
  40. package/docs/images/row-metric-card.svg +33 -0
  41. package/docs/images/row-rail.svg +18 -0
  42. package/docs/images/row-section-header.svg +19 -0
  43. package/docs/images/row-style-regions.svg +30 -0
  44. package/docs/images/row-system.svg +30 -0
  45. package/docs/images/row-wallet-group.svg +26 -0
  46. package/ios/NativeListAccessoryStack.swift +680 -0
  47. package/ios/NativeListActionRenderer.swift +108 -0
  48. package/ios/NativeListActivityRenderer.swift +179 -0
  49. package/ios/NativeListDataRowRenderer.swift +444 -0
  50. package/ios/NativeListIdentityRenderer.swift +685 -0
  51. package/ios/NativeListImageSlot.swift +140 -0
  52. package/ios/NativeListLeadingVisual.swift +372 -0
  53. package/ios/NativeListMarketRenderer.swift +698 -0
  54. package/ios/NativeListMediaTileRenderer.swift +184 -0
  55. package/ios/NativeListMessageRenderer.swift +168 -0
  56. package/ios/NativeListMetricCardRenderer.swift +420 -0
  57. package/ios/NativeListModels.swift +32 -0
  58. package/ios/NativeListRailRenderer.swift +152 -0
  59. package/ios/NativeListRendererCell.swift +311 -0
  60. package/ios/NativeListRendererRegistry.swift +50 -0
  61. package/ios/NativeListResolvedText.swift +122 -0
  62. package/ios/NativeListRowHost.swift +63 -0
  63. package/ios/NativeListSectionHeaderRenderer.swift +708 -0
  64. package/ios/NativeListSystemRenderer.swift +568 -0
  65. package/ios/NativeListTextLabel.swift +42 -0
  66. package/ios/NativeListTextStyles.swift +236 -0
  67. package/ios/NativeListWalletGroupRenderer.swift +255 -0
  68. package/ios/RNCNativeListView.swift +111 -180
  69. package/lib/module/models.js +54 -0
  70. package/lib/module/validation.js +324 -26
  71. package/lib/module/web/NativeListWebEngine.js +148 -936
  72. package/lib/module/web/templates/ActionRowRenderer.js +97 -0
  73. package/lib/module/web/templates/ActivityRowRenderer.js +95 -0
  74. package/lib/module/web/templates/DataRowRenderer.js +145 -0
  75. package/lib/module/web/templates/IdentityRowRenderer.js +289 -0
  76. package/lib/module/web/templates/MarketRowRenderer.js +310 -0
  77. package/lib/module/web/templates/MediaTileRowRenderer.js +132 -0
  78. package/lib/module/web/templates/MessageRowRenderer.js +165 -0
  79. package/lib/module/web/templates/MetricCardRowRenderer.js +172 -0
  80. package/lib/module/web/templates/RailRowRenderer.js +98 -0
  81. package/lib/module/web/templates/RowContainerStyle.js +100 -0
  82. package/lib/module/web/templates/RowElements.js +93 -0
  83. package/lib/module/web/templates/RowRendererRegistry.js +70 -0
  84. package/lib/module/web/templates/RowText.js +121 -0
  85. package/lib/module/web/templates/RowVisual.js +43 -0
  86. package/lib/module/web/templates/SectionHeaderRowRenderer.js +146 -0
  87. package/lib/module/web/templates/SystemRowRenderer.js +161 -0
  88. package/lib/module/web/templates/WalletGroupRowRenderer.js +96 -0
  89. package/lib/typescript/src/models.d.ts +166 -13
  90. package/lib/typescript/src/web/NativeListWebEngine.d.ts +18 -16
  91. package/lib/typescript/src/web/templates/ActionRowRenderer.d.ts +15 -0
  92. package/lib/typescript/src/web/templates/ActivityRowRenderer.d.ts +15 -0
  93. package/lib/typescript/src/web/templates/DataRowRenderer.d.ts +15 -0
  94. package/lib/typescript/src/web/templates/IdentityRowRenderer.d.ts +16 -0
  95. package/lib/typescript/src/web/templates/MarketRowRenderer.d.ts +30 -0
  96. package/lib/typescript/src/web/templates/MediaTileRowRenderer.d.ts +15 -0
  97. package/lib/typescript/src/web/templates/MessageRowRenderer.d.ts +44 -0
  98. package/lib/typescript/src/web/templates/MetricCardRowRenderer.d.ts +15 -0
  99. package/lib/typescript/src/web/templates/RailRowRenderer.d.ts +28 -0
  100. package/lib/typescript/src/web/templates/RowContainerStyle.d.ts +9 -0
  101. package/lib/typescript/src/web/templates/RowElements.d.ts +38 -0
  102. package/lib/typescript/src/web/templates/RowRendererRegistry.d.ts +83 -0
  103. package/lib/typescript/src/web/templates/RowText.d.ts +9 -0
  104. package/lib/typescript/src/web/templates/RowVisual.d.ts +20 -0
  105. package/lib/typescript/src/web/templates/SectionHeaderRowRenderer.d.ts +15 -0
  106. package/lib/typescript/src/web/templates/SystemRowRenderer.d.ts +16 -0
  107. package/lib/typescript/src/web/templates/WalletGroupRowRenderer.d.ts +20 -0
  108. package/package.json +3 -3
  109. package/src/models.ts +281 -19
  110. package/src/validation.ts +501 -46
  111. package/src/web/NativeListWebEngine.ts +361 -1932
  112. package/src/web/templates/ActionRowRenderer.ts +123 -0
  113. package/src/web/templates/ActivityRowRenderer.ts +159 -0
  114. package/src/web/templates/DataRowRenderer.ts +212 -0
  115. package/src/web/templates/IdentityRowRenderer.ts +527 -0
  116. package/src/web/templates/MarketRowRenderer.ts +467 -0
  117. package/src/web/templates/MediaTileRowRenderer.ts +182 -0
  118. package/src/web/templates/MessageRowRenderer.ts +306 -0
  119. package/src/web/templates/MetricCardRowRenderer.ts +337 -0
  120. package/src/web/templates/RailRowRenderer.ts +136 -0
  121. package/src/web/templates/RowContainerStyle.ts +131 -0
  122. package/src/web/templates/RowElements.ts +160 -0
  123. package/src/web/templates/RowRendererRegistry.ts +111 -0
  124. package/src/web/templates/RowText.ts +181 -0
  125. package/src/web/templates/RowVisual.ts +115 -0
  126. package/src/web/templates/SectionHeaderRowRenderer.ts +243 -0
  127. package/src/web/templates/SystemRowRenderer.ts +282 -0
  128. package/src/web/templates/WalletGroupRowRenderer.ts +131 -0
  129. package/android/src/main/java/com/onekey/nativelist/NativeListRowView.kt +0 -4538
  130. package/ios/NativeListCell.swift +0 -4300
@@ -0,0 +1,680 @@
1
+ import UIKit
2
+
3
+ // OneKey patch: explicit summary actions use the source text's physical-pixel line box.
4
+ final class NativeListAccessoryButton: UIButton {
5
+ var pressedBackgroundColor: UIColor?
6
+ var rowTextOffsetY: CGFloat = 0
7
+
8
+ override var isHighlighted: Bool {
9
+ didSet {
10
+ guard let pressedBackgroundColor else { return }
11
+ backgroundColor = isHighlighted ? pressedBackgroundColor : .clear
12
+ }
13
+ }
14
+
15
+ var selectorSummaryLineHeight: CGFloat? {
16
+ didSet {
17
+ invalidateIntrinsicContentSize()
18
+ setNeedsLayout()
19
+ }
20
+ }
21
+
22
+ var marketLineHeight: CGFloat? {
23
+ didSet {
24
+ invalidateIntrinsicContentSize()
25
+ setNeedsLayout()
26
+ }
27
+ }
28
+
29
+ private var sourcePixelScale: CGFloat {
30
+ max(1, window?.screen.scale ?? traitCollection.displayScale)
31
+ }
32
+
33
+ override var intrinsicContentSize: CGSize {
34
+ var size = super.intrinsicContentSize
35
+ guard selectorSummaryLineHeight != nil || marketLineHeight != nil,
36
+ let title = attributedTitle(for: .normal)
37
+ else { return size }
38
+ let width = title.boundingRect(
39
+ with: CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude),
40
+ options: [.usesLineFragmentOrigin, .usesFontLeading],
41
+ context: nil
42
+ ).width
43
+ size.width = ceil(width * sourcePixelScale) / sourcePixelScale
44
+ return size
45
+ }
46
+
47
+ override func layoutSubviews() {
48
+ super.layoutSubviews()
49
+ defer {
50
+ if let titleLabel {
51
+ var frame = titleLabel.frame
52
+ if contentVerticalAlignment == .top {
53
+ frame.origin.y = contentEdgeInsets.top
54
+ } else if contentVerticalAlignment == .bottom {
55
+ frame.origin.y = bounds.height - contentEdgeInsets.bottom - frame.height
56
+ }
57
+ frame.origin.y += rowTextOffsetY
58
+ titleLabel.frame = frame
59
+ }
60
+ }
61
+ if let lineHeight = marketLineHeight, let titleLabel, let title = attributedTitle(for: .normal)
62
+ {
63
+ let measured = title.boundingRect(
64
+ with: CGSize(
65
+ width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude),
66
+ options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil
67
+ ).width
68
+ let width = min(bounds.width, ceil(measured * sourcePixelScale) / sourcePixelScale)
69
+ let x =
70
+ contentHorizontalAlignment == .trailing
71
+ ? bounds.width - width
72
+ : contentHorizontalAlignment == .leading ? 0 : (bounds.width - width) / 2
73
+ let textHeight =
74
+ titleLabel.numberOfLines > 1
75
+ ? min(
76
+ CGFloat(titleLabel.numberOfLines) * lineHeight,
77
+ titleLabel.sizeThatFits(CGSize(width: bounds.width, height: .greatestFiniteMagnitude))
78
+ .height)
79
+ : lineHeight
80
+ titleLabel.frame = CGRect(
81
+ x: floor(x * sourcePixelScale) / sourcePixelScale,
82
+ y: (bounds.height - textHeight) / 2,
83
+ width: width, height: textHeight
84
+ )
85
+ return
86
+ }
87
+ guard let lineHeight = selectorSummaryLineHeight, let titleLabel else { return }
88
+ // OneKey patch: position the final source line box after UIKit has measured the button.
89
+ let top = ceil((bounds.height - lineHeight) / 2 * sourcePixelScale) / sourcePixelScale
90
+ var frame = titleLabel.frame
91
+ frame.origin.y = top
92
+ titleLabel.frame = frame
93
+ }
94
+ }
95
+
96
+ // Bounded trailing controls shared by migrated templates. The owning host supplies
97
+ // action epochs; selection-only updates touch only the existing checkbox.
98
+ final class NativeListAccessoryStack: UIStackView {
99
+ private let accessoryButtons = (0..<2).map { _ in NativeListAccessoryButton(type: .system) }
100
+ private let checkboxButton = UIButton(type: .system)
101
+ private let spinner = UIActivityIndicatorView(style: .medium)
102
+ private var accessoryActions: [(String, NativeSelectionTarget?)] = []
103
+ private var checkboxAction: (String, NativeSelectionTarget?)?
104
+ private var boundCheckboxData: [String: Any]?
105
+ private var boundCheckboxTarget: NativeSelectionTarget?
106
+ private var semanticValueButtons: [UIButton] = []
107
+ private var accessorySizeConstraints: [NSLayoutConstraint] = []
108
+ private var currentItem: NativeListItem?
109
+ private var currentTheme: [String: Any]?
110
+ private var checkboxCheckedColor = UIColor.black
111
+ private var checkboxUncheckedColor = UIColor.white
112
+ private var checkboxIconColor = UIColor.white
113
+ private var checkboxBorderColor = UIColor.lightGray
114
+ var firstVisibleControl: UIView? { accessoryButtons.first { !$0.isHidden } }
115
+ var endInset: CGFloat?
116
+ var contentGap: CGFloat?
117
+ var onAction: ((String, UIView, Int?, NativeSelectionTarget?) -> Void)?
118
+
119
+ override init(frame: CGRect) {
120
+ super.init(frame: frame)
121
+ axis = .vertical
122
+ alignment = .trailing
123
+ setContentHuggingPriority(.required, for: .horizontal)
124
+ setContentCompressionResistancePriority(.required, for: .horizontal)
125
+ for (index, button) in accessoryButtons.enumerated() {
126
+ button.tag = index
127
+ button.adjustsImageWhenDisabled = false
128
+ button.tintAdjustmentMode = .normal
129
+ button.addTarget(self, action: #selector(pressed(_:)), for: .touchUpInside)
130
+ addArrangedSubview(button)
131
+ }
132
+ checkboxButton.layer.borderWidth = 2
133
+ checkboxButton.layer.cornerRadius = 4
134
+ checkboxButton.imageView?.contentMode = .center
135
+ checkboxButton.addTarget(self, action: #selector(checkboxPressed), for: .touchUpInside)
136
+ NSLayoutConstraint.activate([
137
+ checkboxButton.widthAnchor.constraint(equalToConstant: 20),
138
+ checkboxButton.heightAnchor.constraint(equalToConstant: 20),
139
+ ])
140
+ addArrangedSubview(checkboxButton)
141
+ addArrangedSubview(spinner)
142
+ }
143
+ required init(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
144
+
145
+ func bind(
146
+ _ item: NativeListItem, descriptors: [[String: Any]], theme: [String: Any]?,
147
+ style: [String: Any], defaultSpacing: CGFloat = 2,
148
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
149
+ ) {
150
+ reset()
151
+ currentItem = item
152
+ currentTheme = theme
153
+ spacing = CGFloat(style.double("trailingGap", default: Double(defaultSpacing)))
154
+ checkboxCheckedColor = nativeListColor(theme, "primaryText", "#202020")
155
+ checkboxUncheckedColor = nativeListColor(theme, "inverseText", "#FCFCFC")
156
+ checkboxIconColor = checkboxUncheckedColor
157
+ checkboxBorderColor = UIColor(nativeListHex: "#00000031", fallback: .lightGray)
158
+ if item.data.string("presentation") == "networkSelector" {
159
+ checkboxCheckedColor = nativeListColor(theme, "checkboxBackground", "#202020")
160
+ checkboxBorderColor = nativeListColor(theme, "checkboxBorder", "#00000031")
161
+ checkboxIconColor = nativeListColor(theme, "checkboxIcon", "#FFFFFF")
162
+ checkboxUncheckedColor = checkboxIconColor
163
+ }
164
+ accessoryButtons.forEach {
165
+ $0.setTitleColor(nativeListColor(theme, "primaryText", "#202020"), for: .normal)
166
+ }
167
+ bindAccessories(item, descriptors, theme, checkboxState)
168
+ for (index, key) in ["value", "valueSecondary"].enumerated() {
169
+ if let value = style.dictionary(key), semanticValueButtons.indices.contains(index) {
170
+ applyStyledButton(semanticValueButtons[index], value)
171
+ }
172
+ }
173
+ isHidden = arrangedSubviews.allSatisfy { $0.isHidden }
174
+ }
175
+ func updateSelection(
176
+ _ item: NativeListItem,
177
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
178
+ ) {
179
+ currentItem = item
180
+ if boundCheckboxData != nil,
181
+ let data = item.data.dictionaries("trailing").last(where: { $0.string("kind") == "checkbox" })
182
+ ?? item.data.dictionary("checkbox")
183
+ {
184
+ bindCheckbox(item, data, checkboxState)
185
+ }
186
+ }
187
+ func anchorInset(for view: UIView) -> CGFloat {
188
+ currentItem?.data.string("presentation") == "accountSelector"
189
+ && accessoryButtons.contains { $0 === view } && view.bounds.width == 38 ? 7 : 0
190
+ }
191
+ func reset() {
192
+ NSLayoutConstraint.deactivate(accessorySizeConstraints)
193
+ accessorySizeConstraints.removeAll()
194
+ accessoryActions.removeAll()
195
+ semanticValueButtons.removeAll()
196
+ checkboxAction = nil
197
+ boundCheckboxData = nil
198
+ boundCheckboxTarget = nil
199
+ endInset = nil
200
+ contentGap = nil
201
+ currentItem = nil
202
+ for (index, button) in accessoryButtons.enumerated() {
203
+ button.setAttributedTitle(nil, for: .normal)
204
+ button.setTitle(nil, for: .normal)
205
+ button.setImage(nil, for: .normal)
206
+ button.setImage(nil, for: .disabled)
207
+ button.titleLabel?.font = nativeListFont(
208
+ ofSize: index == 0 ? 16 : 14, weight: index == 0 ? .medium : .regular)
209
+ button.titleLabel?.numberOfLines = 1
210
+ button.titleLabel?.lineBreakMode = .byTruncatingTail
211
+ button.contentHorizontalAlignment = .center
212
+ button.contentVerticalAlignment = .center
213
+ button.rowTextOffsetY = 0
214
+ button.transform = .identity
215
+ button.marketLineHeight = nil
216
+ button.selectorSummaryLineHeight = nil
217
+ button.backgroundColor = .clear
218
+ button.pressedBackgroundColor = nil
219
+ button.layer.cornerRadius = 0
220
+ button.isHidden = true
221
+ button.isEnabled = true
222
+ button.alpha = 1
223
+ button.accessibilityIdentifier = nil
224
+ button.accessibilityLabel = nil
225
+ }
226
+ checkboxButton.isHidden = true
227
+ spinner.stopAnimating()
228
+ spinner.alpha = 1
229
+ }
230
+ @objc private func pressed(_ button: UIButton) {
231
+ guard accessoryActions.indices.contains(button.tag) else { return }
232
+ let action = accessoryActions[button.tag]
233
+ if !action.0.isEmpty { onAction?(action.0, button, button.tag, action.1) }
234
+ }
235
+ @objc private func checkboxPressed() {
236
+ if let action = checkboxAction { onAction?(action.0, checkboxButton, nil, action.1) }
237
+ }
238
+ private func bindAccessories(
239
+ _ item: NativeListItem,
240
+ _ accessories: [[String: Any]],
241
+ _ theme: [String: Any]?,
242
+ _ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
243
+ ) {
244
+ var textIndex = 0
245
+ for accessory in accessories.prefix(2) {
246
+ switch accessory.string("kind") {
247
+ case "value":
248
+ showAccessory(textIndex, accessory.string("text"))
249
+ semanticValueButtons.append(accessoryButtons[textIndex])
250
+ if item.data.string("presentation") == "networkSelector" && item.hasExplicitHeight {
251
+ accessoryButtons[textIndex].contentHorizontalAlignment = .trailing
252
+ accessoryButtons[textIndex].titleLabel?.textAlignment = .right
253
+ }
254
+ applyValueSegments(
255
+ accessory.dictionaries("textSegments"), to: accessoryButtons[textIndex], theme: theme)
256
+ textIndex += 1
257
+ case "valuePair":
258
+ showValuePairAccessory(textIndex, accessory, theme: theme)
259
+ semanticValueButtons.append(accessoryButtons[textIndex])
260
+ textIndex += 1
261
+ case "checkbox": bindCheckbox(item, accessory, checkboxState)
262
+ case "radio":
263
+ showAccessory(
264
+ textIndex, accessory.bool("checked") ? "●" : "○", action: accessoryAction(accessory))
265
+ textIndex += 1
266
+ case "switch":
267
+ showAccessory(
268
+ textIndex, accessory.bool("value") ? "ON" : "OFF", action: accessoryAction(accessory))
269
+ textIndex += 1
270
+ case "chevron":
271
+ var icon = accessory
272
+ icon["name"] = "ChevronRightSmallOutline"
273
+ if icon["tintColor"] == nil {
274
+ icon["tintColor"] = theme?["iconSubdued"] as? String ?? "#8D8D8D"
275
+ }
276
+ if icon.string("actionKey").isEmpty { icon["actionKey"] = "press" }
277
+ showAccessoryIcon(textIndex, icon)
278
+ textIndex += 1
279
+ case "menu":
280
+ showMenuAccessory(textIndex, action: accessoryAction(accessory))
281
+ textIndex += 1
282
+ case "drag":
283
+ var icon = accessory
284
+ icon["name"] = "DragOutline"
285
+ if icon["tintColor"] == nil {
286
+ icon["tintColor"] = theme?["iconSubdued"] as? String ?? "#8D8D8D"
287
+ }
288
+ showAccessoryIcon(textIndex, icon)
289
+ textIndex += 1
290
+ case "icon":
291
+ var icon = accessory
292
+ if icon["tintColor"] == nil {
293
+ icon["tintColor"] = theme?["iconSubdued"] as? String ?? "#8D8D8D"
294
+ }
295
+ showAccessoryIcon(textIndex, icon)
296
+ textIndex += 1
297
+ case "spinner": spinner.startAnimating()
298
+ case "progress":
299
+ showAccessory(textIndex, "\(Int(accessory.double("value") * 100))%")
300
+ textIndex += 1
301
+ default: break
302
+ }
303
+ }
304
+ }
305
+
306
+ private func bindCheckbox(
307
+ _ item: NativeListItem,
308
+ _ data: [String: Any],
309
+ _ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
310
+ ) {
311
+ let targetData = data.dictionary("target")
312
+ let scope = targetData?.string("scope", default: "row") ?? "row"
313
+ let key =
314
+ scope == "section" ? targetData?.string("sectionKey") : scope == "row" ? item.key : nil
315
+ let target = NativeSelectionTarget(scope: scope, key: key)
316
+ let state = checkboxState(item, target, data.string("state", default: "unchecked"))
317
+ boundCheckboxData = data
318
+ boundCheckboxTarget = target
319
+ updateCheckboxPresentation(item, data, target: target, state: state)
320
+ }
321
+
322
+ private func updateCheckboxPresentation(
323
+ _ item: NativeListItem,
324
+ _ data: [String: Any],
325
+ target: NativeSelectionTarget,
326
+ state: String
327
+ ) {
328
+ let accessoryDisabled = data.bool("disabled")
329
+ if data.bool("loading") {
330
+ checkboxButton.isHidden = true
331
+ spinner.startAnimating()
332
+ spinner.alpha = item.data.bool("disabled") ? 1 : accessoryDisabled ? 0.5 : 1
333
+ checkboxAction = nil
334
+ return
335
+ }
336
+ checkboxButton.isHidden = false
337
+ checkboxButton.backgroundColor =
338
+ state == "unchecked" ? checkboxUncheckedColor : checkboxCheckedColor
339
+ checkboxButton.layer.borderColor =
340
+ state == "unchecked"
341
+ ? checkboxBorderColor.cgColor
342
+ : UIColor.clear.cgColor
343
+ let glyphName =
344
+ state == "indeterminate"
345
+ ? "CheckboxIndeterminateCustom"
346
+ : "CheckboxCheckedCustom"
347
+ checkboxButton.setImage(
348
+ state == "unchecked" ? nil : nativeListIcon(named: glyphName),
349
+ for: .normal
350
+ )
351
+ checkboxButton.tintColor = checkboxIconColor
352
+ // A disabled ListItem already applies 0.5 to its complete content. Avoid
353
+ // multiplying that opacity on the nested control a second time.
354
+ checkboxButton.alpha = item.data.bool("disabled") ? 1 : accessoryDisabled ? 0.5 : 1
355
+ checkboxButton.isEnabled =
356
+ !item.data.bool("disabled") && !accessoryDisabled && !data.bool("loading")
357
+ checkboxAction = (data.string("actionKey", default: "selection"), target)
358
+ }
359
+
360
+ private func showAccessory(
361
+ _ index: Int,
362
+ _ value: String,
363
+ action: (String, NativeSelectionTarget?)? = nil,
364
+ color: UIColor? = nil
365
+ ) {
366
+ guard accessoryButtons.indices.contains(index), !value.isEmpty else { return }
367
+ let button = accessoryButtons[index]
368
+ button.isHidden = false
369
+ button.setTitle(value, for: .normal)
370
+ button.setContentHuggingPriority(.required, for: .horizontal)
371
+ button.setContentCompressionResistancePriority(.required, for: .horizontal)
372
+ if let color { button.setTitleColor(color, for: .normal) }
373
+ while accessoryActions.count <= index { accessoryActions.append(("", nil)) }
374
+ accessoryActions[index] = action ?? ("", nil)
375
+ }
376
+
377
+ private func showValuePairAccessory(
378
+ _ index: Int,
379
+ _ accessory: [String: Any],
380
+ theme: [String: Any]?
381
+ ) {
382
+ guard accessoryButtons.indices.contains(index) else { return }
383
+ let primary = accessory.string("primary")
384
+ let secondary = accessory.string("secondary")
385
+ guard !primary.isEmpty || !secondary.isEmpty else { return }
386
+ let button = accessoryButtons[index]
387
+ let text = NSMutableAttributedString()
388
+ let primaryParagraph = NSMutableParagraphStyle()
389
+ primaryParagraph.alignment = .right
390
+ primaryParagraph.minimumLineHeight = 20
391
+ primaryParagraph.maximumLineHeight = 20
392
+ text.append(
393
+ NSAttributedString(
394
+ string: primary,
395
+ attributes: [
396
+ .font: nativeListFont(ofSize: 16, weight: .medium),
397
+ .foregroundColor: accessoryTextColor(
398
+ accessory.string("primaryTone"),
399
+ defaultTone: "primary",
400
+ theme: theme
401
+ ),
402
+ .paragraphStyle: primaryParagraph,
403
+ ]
404
+ ))
405
+ if !primary.isEmpty, !secondary.isEmpty { text.append(NSAttributedString(string: "\n")) }
406
+ let secondaryParagraph = NSMutableParagraphStyle()
407
+ secondaryParagraph.alignment = .right
408
+ secondaryParagraph.minimumLineHeight = 20
409
+ secondaryParagraph.maximumLineHeight = 20
410
+ text.append(
411
+ NSAttributedString(
412
+ string: secondary,
413
+ attributes: [
414
+ .font: nativeListFont(ofSize: 14),
415
+ .foregroundColor: accessoryTextColor(
416
+ accessory.string("secondaryTone"),
417
+ defaultTone: "secondary",
418
+ theme: theme
419
+ ),
420
+ .paragraphStyle: secondaryParagraph,
421
+ ]
422
+ ))
423
+ button.isHidden = false
424
+ button.titleLabel?.numberOfLines = 2
425
+ button.contentHorizontalAlignment = .trailing
426
+ button.setAttributedTitle(text, for: .normal)
427
+ button.setContentHuggingPriority(.required, for: .horizontal)
428
+ button.setContentCompressionResistancePriority(.required, for: .horizontal)
429
+ while accessoryActions.count <= index { accessoryActions.append(("", nil)) }
430
+ accessoryActions[index] = ("", nil)
431
+ }
432
+
433
+ private func showMenuAccessory(
434
+ _ index: Int,
435
+ action: (String, NativeSelectionTarget?)?
436
+ ) {
437
+ showAccessory(index, "⋮", action: action)
438
+ guard accessoryButtons.indices.contains(index) else { return }
439
+ let button = accessoryButtons[index]
440
+ button.titleLabel?.font = nativeListFont(ofSize: 20, weight: .medium)
441
+ accessorySizeConstraints.append(contentsOf: [
442
+ button.widthAnchor.constraint(equalToConstant: 24),
443
+ button.heightAnchor.constraint(equalToConstant: 24),
444
+ ])
445
+ NSLayoutConstraint.activate(Array(accessorySizeConstraints.suffix(2)))
446
+ }
447
+
448
+ private func accessoryTextColor(
449
+ _ tone: String,
450
+ defaultTone: String,
451
+ theme: [String: Any]?
452
+ ) -> UIColor {
453
+ switch tone.isEmpty ? defaultTone : tone {
454
+ case "positive": return nativeListColor(theme, "positive", "#218358")
455
+ case "negative": return nativeListColor(theme, "negative", "#CE2C31")
456
+ // OneKey patch: account warnings and hidden balances use existing theme tokens.
457
+ case "disabled": return nativeListColor(theme, "disabledText", "#8D8D8D")
458
+ case "caution": return nativeListColor(theme, "caution", "#AB6400")
459
+ case "secondary": return nativeListColor(theme, "secondaryText", "#646464")
460
+ default: return nativeListColor(theme, "primaryText", "#202020")
461
+ }
462
+ }
463
+
464
+ private func showAccessoryIcon(_ index: Int, _ data: [String: Any]) {
465
+ guard accessoryButtons.indices.contains(index) else { return }
466
+ let button = accessoryButtons[index]
467
+ button.isHidden = false
468
+ button.isEnabled = !data.bool("disabled")
469
+ button.alpha = button.isEnabled ? 1 : 0.4
470
+ let isAccountCreate =
471
+ currentItem?.data.string("presentation") == "accountSelector"
472
+ && currentItem?.hasExplicitHeight == true && data.string("name") == "PlusSmallOutline"
473
+ let tintColor =
474
+ data["tintColor"] == nil && isAccountCreate
475
+ ? nativeListColor(currentTheme, "iconSubdued", "#8D8D8D")
476
+ : UIColor(nativeListHex: data.string("tintColor", default: "#646464"), fallback: .darkGray)
477
+ button.tintColor = tintColor
478
+ if let image = nativeListIcon(named: data.string("name")) {
479
+ button.setImage(image, for: .normal)
480
+ button.setImage(
481
+ image.withTintColor(tintColor, renderingMode: .alwaysOriginal),
482
+ for: .disabled
483
+ )
484
+ }
485
+ let isDrillIn = data.string("kind") == "chevron"
486
+ let isAccountIcon = currentItem?.data.string("presentation") == "accountSelector" && !isDrillIn
487
+ let size: CGFloat = isDrillIn ? 24 : isAccountIcon && !isAccountCreate ? 38 : 36
488
+ if isAccountCreate { button.layer.cornerRadius = 8 }
489
+ if isAccountIcon && !isAccountCreate && !data.string("actionKey").isEmpty {
490
+ button.pressedBackgroundColor = nativeListColor(
491
+ currentTheme, "rowPressedBackground", "#E8E8E8")
492
+ button.layer.cornerRadius = size / 2
493
+ }
494
+ if isAccountIcon { contentGap = 5 }
495
+ button.accessibilityIdentifier = data["testID"] as? String
496
+ button.accessibilityLabel = data["accessibilityLabel"] as? String
497
+ if !isDrillIn, !data.string("actionKey").isEmpty {
498
+ // Reproduce the trailing edge of IconButton's m=-7 while keeping its
499
+ // full 36-point frame for padding/highlight behavior.
500
+ endInset = 5
501
+ }
502
+ accessorySizeConstraints.append(contentsOf: [
503
+ button.widthAnchor.constraint(equalToConstant: size),
504
+ button.heightAnchor.constraint(equalToConstant: size),
505
+ ])
506
+ NSLayoutConstraint.activate(Array(accessorySizeConstraints.suffix(2)))
507
+ while accessoryActions.count <= index { accessoryActions.append(("", nil)) }
508
+ accessoryActions[index] =
509
+ data.bool("disabled")
510
+ ? ("", nil)
511
+ : (data.string("actionKey"), nil)
512
+ }
513
+
514
+ private func accessoryAction(
515
+ _ accessory: [String: Any],
516
+ defaultKey: String = ""
517
+ ) -> (String, NativeSelectionTarget?)? {
518
+ if accessory.bool("disabled") { return nil }
519
+ let key = accessory.string("actionKey", default: defaultKey)
520
+ return key.isEmpty ? nil : (key, nil)
521
+ }
522
+
523
+ private func applyValueSegments(
524
+ _ segments: [[String: Any]], to button: UIButton, theme: [String: Any]?
525
+ ) {
526
+ guard !segments.isEmpty else { return }
527
+ let value = NSMutableAttributedString(string: "")
528
+ let color = nativeListColor(theme, "primaryText", "#202020")
529
+ // OneKey patch: rich currency changes font runs without dropping the established line baseline.
530
+ let current = button.attributedTitle(for: .normal)
531
+ var attributes =
532
+ current.flatMap { $0.length > 0 ? $0.attributes(at: 0, effectiveRange: nil) : nil } ?? [:]
533
+ attributes[.foregroundColor] = color
534
+ if currentItem?.data.string("presentation") == "networkSelector"
535
+ && currentItem?.hasExplicitHeight == true
536
+ {
537
+ let paragraph =
538
+ (attributes[.paragraphStyle] as? NSParagraphStyle)?.mutableCopy()
539
+ as? NSMutableParagraphStyle ?? NSMutableParagraphStyle()
540
+ paragraph.alignment = .right
541
+ attributes[.paragraphStyle] = paragraph
542
+ }
543
+ for segment in segments {
544
+ attributes[.font] = nativeListTabularFont(
545
+ ofSize: segment.string("style") == "subscript" ? 10 : 16, weight: .medium)
546
+ value.append(NSAttributedString(string: segment.string("text"), attributes: attributes))
547
+ }
548
+ button.setAttributedTitle(value, for: .normal)
549
+ }
550
+
551
+ private func applyStyledButton(_ button: UIButton, _ style: [String: Any]) {
552
+ let text =
553
+ button.attributedTitle(for: .normal)?.string
554
+ ?? button.title(for: .normal)
555
+ ?? ""
556
+ guard !text.isEmpty, !style.isEmpty else { return }
557
+ let originalText = button.attributedTitle(for: .normal)
558
+ let accessory = button as? NativeListAccessoryButton
559
+ let baseFont =
560
+ (originalText?.attribute(.font, at: 0, effectiveRange: nil) as? UIFont) ?? button.titleLabel?
561
+ .font ?? nativeListFont(ofSize: 14)
562
+ let size = CGFloat(style.double("fontSize", default: Double(baseFont.pointSize)))
563
+ let font: UIFont
564
+ if let weightName = style["fontWeight"] as? String {
565
+ font = nativeListFont(
566
+ ofSize: size,
567
+ weight: marketFontWeight(weightName, fallback: .regular)
568
+ )
569
+ } else {
570
+ font = baseFont.withSize(size)
571
+ }
572
+ let fallbackColor = button.titleColor(for: .normal) ?? .black
573
+ let color =
574
+ (style["color"] as? String)
575
+ .map { UIColor(nativeListHex: $0, fallback: fallbackColor) }
576
+ ?? fallbackColor
577
+ let attributed =
578
+ button.attributedTitle(for: .normal)
579
+ .map { NSMutableAttributedString(attributedString: $0) }
580
+ ?? NSMutableAttributedString(string: text)
581
+ let paragraph =
582
+ (attributed.attribute(.paragraphStyle, at: 0, effectiveRange: nil)
583
+ as? NSParagraphStyle)?.mutableCopy() as? NSMutableParagraphStyle ?? NSMutableParagraphStyle()
584
+ if let alignmentName = style["alignment"] as? String {
585
+ paragraph.alignment = marketTextAlignment(alignmentName)
586
+ button.contentHorizontalAlignment =
587
+ alignmentName == "start"
588
+ ? .leading
589
+ : alignmentName == "end" ? .trailing : .center
590
+ }
591
+ var attributes: [NSAttributedString.Key: Any] = [:]
592
+ applyStyledFont(attributed, style, fallback: baseFont)
593
+ if style["color"] != nil { attributes[.foregroundColor] = color }
594
+ if style["alignment"] != nil || style["lineHeight"] != nil || style["lines"] != nil
595
+ || style["truncate"] != nil
596
+ {
597
+ attributes[.paragraphStyle] = paragraph
598
+ }
599
+ if style["lineHeight"] != nil {
600
+ let box = CGFloat(style.double("lineHeight"))
601
+ paragraph.minimumLineHeight = box
602
+ paragraph.maximumLineHeight = box
603
+ attributes[.baselineOffset] = max(0, (box - font.lineHeight) / 2)
604
+ if accessory?.marketLineHeight != nil { accessory?.marketLineHeight = box }
605
+ if accessory?.selectorSummaryLineHeight != nil { accessory?.selectorSummaryLineHeight = box }
606
+ }
607
+ if style["lines"] != nil {
608
+ button.titleLabel?.numberOfLines = min(3, max(1, style.int("lines", default: 1)))
609
+ }
610
+ if style["lines"] != nil || style["truncate"] != nil {
611
+ let mode = styledLineBreakMode(style, lines: button.titleLabel?.numberOfLines ?? 1)
612
+ button.titleLabel?.lineBreakMode = mode
613
+ paragraph.lineBreakMode = mode
614
+ if button.titleLabel?.numberOfLines == 1 { normalizeSingleLine(attributed) }
615
+ }
616
+ if let alignment = style["verticalAlignment"] as? String {
617
+ button.contentVerticalAlignment =
618
+ alignment == "top" ? .top : alignment == "bottom" ? .bottom : .center
619
+ }
620
+ if style["offsetY"] != nil {
621
+ (button as? NativeListAccessoryButton)?.rowTextOffsetY = CGFloat(style.double("offsetY"))
622
+ }
623
+ button.titleLabel?.font = font
624
+ attributed.addAttributes(attributes, range: NSRange(location: 0, length: attributed.length))
625
+ button.setAttributedTitle(attributed, for: .normal)
626
+ }
627
+
628
+ private func applyStyledFont(
629
+ _ text: NSMutableAttributedString, _ style: [String: Any], fallback: UIFont
630
+ ) {
631
+ guard style["fontSize"] != nil || style["fontWeight"] != nil else { return }
632
+ text.enumerateAttribute(.font, in: NSRange(location: 0, length: text.length)) {
633
+ value, range, _ in
634
+ let original = (value as? UIFont) ?? fallback
635
+ let size = CGFloat(style.double("fontSize", default: Double(original.pointSize)))
636
+ let font =
637
+ style["fontWeight"] == nil
638
+ ? original.withSize(size)
639
+ : nativeListFont(
640
+ ofSize: size, weight: marketFontWeight(style.string("fontWeight"), fallback: .regular))
641
+ text.addAttribute(.font, value: font, range: range)
642
+ }
643
+ }
644
+
645
+ private func normalizeSingleLine(_ text: NSMutableAttributedString) {
646
+ let pattern = try! NSRegularExpression(pattern: "\\r\\n|[\\r\\n]")
647
+ for match in pattern.matches(in: text.string, range: NSRange(location: 0, length: text.length))
648
+ .reversed()
649
+ {
650
+ text.replaceCharacters(in: match.range, with: " ")
651
+ }
652
+ }
653
+
654
+ private func styledLineBreakMode(_ style: [String: Any], lines: Int) -> NSLineBreakMode {
655
+ if style.string("truncate", default: "tail") != "clip" { return .byTruncatingTail }
656
+ // Clipping disables wrapping; multi-line clip must wrap without adding an ellipsis.
657
+ return lines == 1 ? .byClipping : .byWordWrapping
658
+ }
659
+
660
+ private func marketFontWeight(_ value: String, fallback: NativeListFontWeight)
661
+ -> NativeListFontWeight
662
+ {
663
+ switch value {
664
+ case "regular": return .regular
665
+ case "semibold": return .semibold
666
+ case "bold": return .bold
667
+ case "medium": return .medium
668
+ default: return fallback
669
+ }
670
+ }
671
+
672
+ private func marketTextAlignment(_ value: String) -> NSTextAlignment {
673
+ switch value {
674
+ case "center": return .center
675
+ case "start": return effectiveUserInterfaceLayoutDirection == .rightToLeft ? .right : .left
676
+ case "end": return effectiveUserInterfaceLayoutDirection == .rightToLeft ? .left : .right
677
+ default: return .natural
678
+ }
679
+ }
680
+ }