@onekeyfe/react-native-native-list 3.0.157-alpha.248 → 3.0.157-alpha.250

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,685 @@
1
+ import UIKit
2
+
3
+ final class NativeListIdentityCell: NativeListRendererCell {
4
+ override class func appliesSizePreset(_ item: NativeListItem) -> Bool {
5
+ !["walletSidebar", "networkSelector"].contains(item.data.string("presentation"))
6
+ }
7
+ override class func measure(
8
+ _ item: NativeListItem, width: CGFloat, theme: [String: Any]?, layout: String
9
+ ) -> CGFloat? {
10
+ if item.data.string("presentation") == "walletSidebar" {
11
+ return item.data.dictionaries("badges").isEmpty ? 68 : 92
12
+ }
13
+ if item.data.string("presentation") == "networkSelector" { return 47 }
14
+ return !item.data.string("tertiary").isEmpty
15
+ ? 72 : !item.data.string("subtitle").isEmpty ? 60 : 56
16
+ }
17
+ private let visual = NativeListLeadingVisual(frame: .zero)
18
+ private var fallbackLabel: UILabel { visual.fallbackTextView }
19
+ private let mainStack = UIStackView()
20
+ private let titleRowStack = UIStackView()
21
+ private let titleLabel = NativeListTextLabel()
22
+ private let subtitleLabel = NativeListTextLabel()
23
+ private let tertiaryLabel = NativeListTextLabel()
24
+ private let badgeLabel = NativeListInsetLabel()
25
+ private let trailingStack = NativeListAccessoryStack(frame: .zero)
26
+ private let leadingActionButton = UIButton(type: .system)
27
+ private var leadingWidth: NSLayoutConstraint!
28
+ private var leadingHeight: NSLayoutConstraint!
29
+ private var leftInset: CGFloat = 12
30
+ private var rightInset: CGFloat = -12
31
+ private var topInset: CGFloat = 8
32
+ private var bottomInset: CGFloat = -8
33
+ private var selectorConstraints: [NSLayoutConstraint] = []
34
+ private var selectorViews: [UIView] = []
35
+ private var selectorTypographyRestorers: [() -> Void] = []
36
+ private var semanticBadgeLabels: [UILabel] = []
37
+ private var semanticSubtitleLabels: [UILabel] = []
38
+ private weak var walletBadgeLine: UIStackView?
39
+ private var currentItem: NativeListItem?
40
+ private var currentTheme: [String: Any]?
41
+ private var leadingActionKey: String?
42
+ override var assetFields: [String] { ["leading"] }
43
+ override var defaultSeparatorInset: CGFloat { 60 }
44
+ override var defaultCornerRadius: CGFloat {
45
+ guard let item = currentItem else { return 0 }
46
+ let presentation = item.data.string("presentation")
47
+ if presentation == "walletSidebar", isHighlighted || item.hasExplicitHeight { return 20 }
48
+ if isHighlighted
49
+ || (item.hasExplicitHeight
50
+ && ["accountSelector", "networkSelector"].contains(presentation))
51
+ {
52
+ return 12
53
+ }
54
+ return 0
55
+ }
56
+ private var isWalletSidebar: Bool { currentItem?.data.string("presentation") == "walletSidebar" }
57
+ override var pressedCornerRadius: CGFloat? { isWalletSidebar ? 20 : 12 }
58
+ // Legacy grouped wallet sidebar rows used the sidebar radius.
59
+ override var groupCornerRadius: CGFloat { isWalletSidebar ? 20 : 12 }
60
+ override var defaultCornerCurve: CALayerCornerCurve {
61
+ currentItem?.data.string("presentation") == "walletSidebar" ? .continuous : .circular
62
+ }
63
+ override var defaultVerticalAlignment: String? {
64
+ currentItem?.data.string("presentation") == "walletSidebar" ? "center" : nil
65
+ }
66
+ override init(frame: CGRect) {
67
+ super.init(frame: frame)
68
+ root.axis = .horizontal
69
+ root.alignment = .center
70
+ mainStack.axis = .vertical
71
+ mainStack.alignment = .fill
72
+ mainStack.setContentHuggingPriority(UILayoutPriority(1), for: .horizontal)
73
+ mainStack.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
74
+ titleRowStack.axis = .horizontal
75
+ titleRowStack.alignment = .center
76
+ titleRowStack.addArrangedSubview(titleLabel)
77
+ titleRowStack.addArrangedSubview(badgeLabel)
78
+ [titleRowStack, subtitleLabel, tertiaryLabel].forEach(mainStack.addArrangedSubview)
79
+ visual.translatesAutoresizingMaskIntoConstraints = false
80
+ leadingWidth = visual.widthAnchor.constraint(equalToConstant: 40)
81
+ leadingHeight = visual.heightAnchor.constraint(equalToConstant: 40)
82
+ leadingActionButton.translatesAutoresizingMaskIntoConstraints = false
83
+ NSLayoutConstraint.activate([
84
+ leadingWidth, leadingHeight, leadingActionButton.widthAnchor.constraint(equalToConstant: 36),
85
+ leadingActionButton.heightAnchor.constraint(equalToConstant: 36),
86
+ ])
87
+ leadingActionButton.adjustsImageWhenDisabled = false
88
+ leadingActionButton.tintAdjustmentMode = .normal
89
+ leadingActionButton.addTarget(self, action: #selector(leadingPressed), for: .touchUpInside)
90
+ trailingStack.onAction = { [weak self] key, view, slot, target in
91
+ self?.emitAction(
92
+ key, from: view, source: "trailingAccessory", slot: slot, target: target,
93
+ anchorInset: self?.trailingStack.anchorInset(for: view) ?? 0)
94
+ }
95
+ }
96
+ required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
97
+ override func bindContent(
98
+ _ item: NativeListItem, theme: [String: Any]?, layout: String,
99
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
100
+ ) {
101
+ selectorTypographyRestorers.reversed().forEach { $0() }
102
+ selectorTypographyRestorers = []
103
+ currentItem = item
104
+ currentTheme = theme
105
+ NSLayoutConstraint.deactivate(selectorConstraints)
106
+ selectorConstraints = []
107
+ selectorViews.forEach {
108
+ if let stack = $0.superview as? UIStackView { stack.removeArrangedSubview($0) }
109
+ $0.removeFromSuperview()
110
+ }
111
+ selectorViews = []
112
+ walletBadgeLine = nil
113
+ semanticBadgeLabels = []
114
+ semanticSubtitleLabels = []
115
+ root.arrangedSubviews.forEach {
116
+ root.removeArrangedSubview($0)
117
+ $0.removeFromSuperview()
118
+ }
119
+ root.axis = .horizontal
120
+ root.alignment = .center
121
+ root.spacing = 12
122
+ leftInset = layout == "table" ? 16 : 12
123
+ rightInset = -leftInset
124
+ topInset = 8
125
+ bottomInset = -8
126
+ mainStack.alignment = .fill
127
+ mainStack.spacing = 2
128
+ mainStack.arrangedSubviews.forEach {
129
+ mainStack.setCustomSpacing(UIStackView.spacingUseDefault, after: $0)
130
+ }
131
+ titleRowStack.arrangedSubviews.forEach {
132
+ titleRowStack.setCustomSpacing(UIStackView.spacingUseDefault, after: $0)
133
+ }
134
+ titleRowStack.spacing = 8
135
+ titleRowStack.setContentHuggingPriority(.defaultLow, for: .horizontal)
136
+ titleRowStack.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
137
+ for label in [titleLabel, subtitleLabel, tertiaryLabel, badgeLabel] {
138
+ label.attributedText = nil
139
+ label.text = nil
140
+ label.isHidden = true
141
+ label.numberOfLines = 1
142
+ label.textAlignment = .natural
143
+ label.lineBreakMode = .byTruncatingTail
144
+ label.transform = .identity
145
+ label.setContentHuggingPriority(.defaultLow, for: .horizontal)
146
+ label.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
147
+ if let text = label as? NativeListTextLabel {
148
+ text.rowOffsetY = 0
149
+ text.rowVerticalAlignment = nil
150
+ }
151
+ }
152
+ titleLabel.font = nativeListFont(ofSize: 16, weight: .medium)
153
+ titleLabel.textColor = nativeListColor(theme, "primaryText", "#202020")
154
+ titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
155
+ subtitleLabel.font = nativeListFont(ofSize: 14)
156
+ subtitleLabel.textColor = nativeListColor(theme, "secondaryText", "#646464")
157
+ tertiaryLabel.font = nativeListFont(ofSize: 14)
158
+ tertiaryLabel.textColor = subtitleLabel.textColor
159
+ badgeLabel.font = nativeListFont(ofSize: 12, weight: .medium)
160
+ badgeLabel.horizontalInset = 0
161
+ badgeLabel.topInset = 0
162
+ badgeLabel.bottomInset = 0
163
+ badgeLabel.setContentHuggingPriority(.required, for: .horizontal)
164
+ leadingWidth.constant = 40
165
+ leadingHeight.constant = 40
166
+ visual.glyphSize = 18
167
+ fallbackLabel.attributedText = nil
168
+ fallbackLabel.font = nativeListFont(ofSize: 13, weight: .bold)
169
+ trailingStack.reset()
170
+ trailingStack.axis = .vertical
171
+ trailingStack.alignment = .trailing
172
+ trailingStack.spacing = 2
173
+ leadingActionKey = nil
174
+ leadingActionButton.setImage(nil, for: .normal)
175
+ bindIdentity(item, theme: theme, selected: item.data.bool("selected"), checkboxState)
176
+ let style = item.data.dictionary("style") ?? [:]
177
+ if style["horizontalPadding"] != nil {
178
+ leftInset = CGFloat(style.double("horizontalPadding"))
179
+ rightInset = -leftInset
180
+ }
181
+ if style["verticalPadding"] != nil {
182
+ topInset = CGFloat(style.double("verticalPadding"))
183
+ bottomInset = -topInset
184
+ }
185
+ if style["lineGap"] != nil {
186
+ mainStack.spacing = CGFloat(style.double("lineGap"))
187
+ mainStack.arrangedSubviews.forEach {
188
+ mainStack.setCustomSpacing(mainStack.spacing, after: $0)
189
+ }
190
+ }
191
+ if style["leadingGap"] != nil, root.arrangedSubviews.contains(visual) {
192
+ root.setCustomSpacing(CGFloat(style.double("leadingGap")), after: visual)
193
+ }
194
+ if style["titleBadgeGap"] != nil {
195
+ if walletBadgeLine != nil {
196
+ mainStack.setCustomSpacing(CGFloat(style.double("titleBadgeGap")), after: titleRowStack)
197
+ } else {
198
+ titleRowStack.setCustomSpacing(CGFloat(style.double("titleBadgeGap")), after: titleLabel)
199
+ }
200
+ }
201
+ if style["trailingGap"] != nil { trailingStack.spacing = CGFloat(style.double("trailingGap")) }
202
+ for (slot, labels) in [
203
+ ("title", [titleLabel]),
204
+ ("subtitle", semanticSubtitleLabels.isEmpty ? [subtitleLabel] : semanticSubtitleLabels),
205
+ ("tertiary", [tertiaryLabel]),
206
+ ("badge", semanticBadgeLabels.isEmpty ? [badgeLabel] : semanticBadgeLabels),
207
+ ] {
208
+ if let text = style.dictionary(slot) {
209
+ labels.forEach { NativeListTextStyles.applyStyledText($0, text) }
210
+ }
211
+ }
212
+ contentInsets = UIEdgeInsets(
213
+ top: topInset, left: leftInset, bottom: -bottomInset, right: -rightInset)
214
+ if root.axis == .horizontal {
215
+ root.alignment =
216
+ style.dictionary("container")?.string("contentVerticalAlignment") == "top"
217
+ ? .top
218
+ : style.dictionary("container")?.string("contentVerticalAlignment") == "bottom"
219
+ ? .bottom : .center
220
+ }
221
+ applySelectorTypography(item)
222
+ }
223
+ override func bindSelectionContent(
224
+ _ item: NativeListItem,
225
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
226
+ ) {
227
+ currentItem = item
228
+ trailingStack.updateSelection(item, checkboxState: checkboxState)
229
+ if item.data.string("presentation") == "walletSidebar" {
230
+ let color = nativeListColor(
231
+ currentTheme, selectedState ? "primaryText" : "secondaryText",
232
+ selectedState ? "#FFFFFFED" : "#FFFFFFAF")
233
+ titleLabel.textColor = color
234
+ if let original = titleLabel.attributedText {
235
+ let text = NSMutableAttributedString(attributedString: original)
236
+ text.addAttribute(
237
+ .foregroundColor, value: color, range: NSRange(location: 0, length: text.length))
238
+ titleLabel.attributedText = text
239
+ }
240
+ if let style = item.data.dictionary("style")?.dictionary("title") {
241
+ NativeListTextStyles.applyStyledText(titleLabel, style)
242
+ }
243
+ }
244
+ }
245
+ override func layoutSubviews() {
246
+ super.layoutSubviews()
247
+ if currentItem?.hasExplicitHeight == true,
248
+ currentItem?.data.string("presentation") == "accountSelector",
249
+ currentItem?.data.dictionary("style")?.dictionary("container")?["contentVerticalAlignment"]
250
+ == nil, let descriptor = currentItem?.data.dictionaries("trailing").first,
251
+ descriptor.string("kind") == "icon", descriptor.string("name") == "PlusSmallOutline",
252
+ let control = trailingStack.firstVisibleControl
253
+ {
254
+ control.transform = .identity
255
+ let origin = control.convert(control.bounds, to: contentView).minY
256
+ control.transform = CGAffineTransform(translationX: 0, y: 11 - origin)
257
+ }
258
+ }
259
+ override func recycleContent() {
260
+ visual.recycle()
261
+ trailingStack.reset()
262
+ currentItem = nil
263
+ }
264
+ @objc private func leadingPressed() {
265
+ if let key = leadingActionKey {
266
+ emitAction(key, from: leadingActionButton, source: "leadingAction")
267
+ }
268
+ }
269
+ private func show(_ label: UILabel, _ value: String, lines: Int) {
270
+ label.text = value
271
+ label.numberOfLines = lines
272
+ label.isHidden = value.isEmpty
273
+ }
274
+ private func addLeading(_ descriptor: [String: Any]?, key: String) {
275
+ root.addArrangedSubview(visual)
276
+ guard let descriptor else {
277
+ // Legacy reset the leading container: no stale avatar, initials, border or overlays.
278
+ visual.clear()
279
+ return
280
+ }
281
+ let image = currentItem?.data.dictionary("style")?.dictionary("image") ?? [:]
282
+ if image["width"] != nil { leadingWidth.constant = CGFloat(image.double("width")) }
283
+ if image["height"] != nil { leadingHeight.constant = CGFloat(image.double("height")) }
284
+ let wallet =
285
+ currentItem?.data.string("presentation") == "walletSidebar"
286
+ && currentItem?.hasExplicitHeight == true
287
+ visual.singleOuterMask =
288
+ currentItem?.data.string("presentation") == "networkSelector"
289
+ && currentItem?.hasExplicitHeight == true
290
+ visual.dashedBorderWidth = wallet ? 1 : 2
291
+ visual.overlayTextFontSize = wallet ? 12 : 10
292
+ visual.overlayTextLineHeight = wallet ? 16 : nil
293
+ visual.overlayTextHorizontalInset = wallet ? 2 : nil
294
+ if let fallback = descriptor.dictionary("fallbackIcon") {
295
+ visual.glyphSize =
296
+ wallet && fallback.string("name") == "LockSolid"
297
+ ? 40
298
+ : descriptor.dictionary("image") != nil
299
+ ? min(leadingWidth.constant, leadingHeight.constant)
300
+ * (fallback.string("name") == "GlobusOutline" ? 1.2 : 1) : 18
301
+ }
302
+ visual.bind(descriptor, style: image, key: key, theme: currentTheme, isUnread: false)
303
+ }
304
+ private func bindIdentity(
305
+ _ item: NativeListItem,
306
+ theme: [String: Any]?,
307
+ selected: Bool,
308
+ _ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
309
+ ) {
310
+ if item.data.string("presentation") == "walletSidebar" {
311
+ root.axis = .vertical
312
+ root.alignment = .center
313
+ root.spacing = 4
314
+ leftInset = 4
315
+ rightInset = -4
316
+ topInset = 4
317
+ bottomInset = -4
318
+ mainStack.alignment = .center
319
+ mainStack.spacing = 0
320
+ titleRowStack.setContentHuggingPriority(.required, for: .horizontal)
321
+ titleLabel.setContentHuggingPriority(.required, for: .horizontal)
322
+ titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
323
+ titleLabel.font = nativeListFont(ofSize: 12)
324
+ titleLabel.textAlignment = .center
325
+ fallbackLabel.font = nativeListFont(ofSize: 28)
326
+ addLeading(item.data.dictionary("leading"), key: item.key)
327
+ root.addArrangedSubview(mainStack)
328
+ // OneKey patch: activate width constraints only after both stacks share an ancestor.
329
+ if item.hasExplicitHeight {
330
+ titleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
331
+ titleRowStack.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
332
+ let width = mainStack.widthAnchor.constraint(equalTo: root.widthAnchor)
333
+ width.isActive = true
334
+ selectorConstraints.append(width)
335
+ let titleWidth = titleRowStack.widthAnchor.constraint(
336
+ lessThanOrEqualTo: mainStack.widthAnchor)
337
+ titleWidth.isActive = true
338
+ selectorConstraints.append(titleWidth)
339
+ }
340
+ show(titleLabel, item.data.string("title"), lines: 1)
341
+ setLineHeight(titleLabel, text: item.data.string("title"), lineHeight: 16)
342
+ titleLabel.textColor = nativeListColor(
343
+ theme,
344
+ selected ? "primaryText" : "secondaryText",
345
+ selected ? "#FFFFFFED" : "#FFFFFFAF"
346
+ )
347
+ // OneKey patch: wallet tags belong below the centered name.
348
+ let badges = item.data.dictionaries("badges")
349
+ if !badges.isEmpty {
350
+ let line = UIStackView()
351
+ line.axis = .horizontal
352
+ line.spacing = 4
353
+ line.alignment = .center
354
+ for badge in badges {
355
+ let label = NativeListInsetLabel()
356
+ let isSelector = item.hasExplicitHeight
357
+ let isWarning = badge.string("tone") == "warning"
358
+ label.font = nativeListFont(ofSize: isSelector ? 11 : 12)
359
+ label.textColor = nativeListColor(
360
+ theme, isSelector && isWarning ? "caution" : "secondaryText",
361
+ isSelector && isWarning ? "#AB6400" : "#646464")
362
+ label.backgroundColor = nativeListColor(
363
+ theme,
364
+ isSelector
365
+ ? (isWarning ? "cautionBackground" : "subduedBackground") : "strongBackground",
366
+ isSelector && isWarning ? "#FFF8C5" : "#F0F0F0")
367
+ label.horizontalInset = isSelector ? 6 : 4
368
+ label.topInset = 2
369
+ label.bottomInset = 2
370
+ label.layer.cornerRadius = 4
371
+ label.clipsToBounds = true
372
+ setLineHeight(label, text: badge.string("text"), lineHeight: isSelector ? 14 : 16)
373
+ semanticBadgeLabels.append(label)
374
+ line.addArrangedSubview(label)
375
+ }
376
+ walletBadgeLine = line
377
+ mainStack.spacing = 4
378
+ mainStack.addArrangedSubview(line)
379
+ selectorViews.append(line)
380
+ }
381
+ let accessories = item.data.dictionaries("trailing")
382
+ if !accessories.isEmpty {
383
+ trailingStack.axis = .horizontal
384
+ trailingStack.alignment = .center
385
+ trailingStack.spacing = 8
386
+ root.addArrangedSubview(trailingStack)
387
+ trailingStack.bind(
388
+ item, descriptors: accessories, theme: theme,
389
+ style: item.data.dictionary("style") ?? [:], defaultSpacing: 8,
390
+ checkboxState: checkboxState)
391
+ }
392
+ return
393
+ }
394
+ if item.data.string("presentation") == "accountSelector" {
395
+ leadingWidth.constant = 32
396
+ leadingHeight.constant = 32
397
+ titleLabel.font = nativeListFont(ofSize: 16)
398
+ }
399
+ if item.data.dictionary("leading")?.string("kind") == "network" {
400
+ leadingWidth.constant = 32
401
+ leadingHeight.constant = 32
402
+ }
403
+ if let leadingAction = item.data.dictionary("leadingAction") {
404
+ leadingActionButton.isHidden = false
405
+ let tintColor = UIColor(
406
+ nativeListHex: leadingAction.string("tintColor", default: "#646464"),
407
+ fallback: .darkGray
408
+ )
409
+ leadingActionButton.tintColor = tintColor
410
+ if let image = nativeListIcon(named: leadingAction.string("name")) {
411
+ leadingActionButton.setImage(image, for: .normal)
412
+ leadingActionButton.setImage(
413
+ image.withTintColor(tintColor, renderingMode: .alwaysOriginal),
414
+ for: .disabled
415
+ )
416
+ }
417
+ leadingActionButton.isEnabled = !leadingAction.bool("disabled")
418
+ leadingActionButton.alpha = leadingActionButton.isEnabled ? 1 : 0.4
419
+ leadingActionKey = leadingAction.string("actionKey")
420
+ root.addArrangedSubview(leadingActionButton)
421
+ // ListItem.IconButton is a medium tertiary button: its 36-point frame
422
+ // carries m=-7. The collection already contributes ListItem's outer
423
+ // mx=8, so move the frame 7 points into that inset and reduce only the
424
+ // following gap by 7. This preserves the source button, avatar and text
425
+ // positions without shrinking the 24-point SVG glyph or its hit target.
426
+ leftInset = 5
427
+ root.setCustomSpacing(5, after: leadingActionButton)
428
+ }
429
+ addLeading(item.data.dictionary("leading"), key: item.key)
430
+ // OneKey patch: custom network initials match LetterAvatar size 32.
431
+ if item.data.string("presentation") == "networkSelector",
432
+ let leading = item.data.dictionary("leading"), leading.dictionary("image") == nil,
433
+ leading.dictionary("fallbackIcon") == nil, !leading.string("fallbackText").isEmpty
434
+ {
435
+ fallbackLabel.font = nativeListFont(ofSize: 19, weight: .semibold)
436
+ fallbackLabel.textColor = nativeListColor(theme, "inverseText", "#FCFCFC")
437
+ setLineHeight(fallbackLabel, text: leading.string("fallbackText"), lineHeight: 27)
438
+ }
439
+ root.addArrangedSubview(mainStack)
440
+ show(titleLabel, item.data.string("title"), lines: item.data.int("titleLines", default: 1))
441
+ show(
442
+ subtitleLabel, item.data.string("subtitle"), lines: item.data.int("subtitleLines", default: 1)
443
+ )
444
+ show(tertiaryLabel, item.data.string("tertiary"), lines: 1)
445
+ if !item.data.string("subtitle").isEmpty, item.data.string("tertiary").isEmpty {
446
+ mainStack.spacing = 0
447
+ setLineHeight(titleLabel, text: item.data.string("title"), lineHeight: 24)
448
+ setLineHeight(subtitleLabel, text: item.data.string("subtitle"), lineHeight: 20)
449
+ }
450
+ // OneKey patch: preserve independent balance/address truncation and warning tones.
451
+ let segments = item.data.dictionaries("subtitleSegments")
452
+ if !segments.isEmpty {
453
+ subtitleLabel.isHidden = true
454
+ mainStack.spacing = 0
455
+ setLineHeight(titleLabel, text: item.data.string("title"), lineHeight: 24)
456
+ let line = UIStackView()
457
+ line.axis = .horizontal
458
+ line.alignment = .center
459
+ line.spacing = 0
460
+ for segment in segments {
461
+ if segment.bool("separatorBefore") {
462
+ let gap = UIView()
463
+ gap.translatesAutoresizingMaskIntoConstraints = false
464
+ let dot = UIView()
465
+ dot.translatesAutoresizingMaskIntoConstraints = false
466
+ dot.backgroundColor = nativeListColor(theme, "disabledText", "#8D8D8D")
467
+ dot.layer.cornerRadius = 2
468
+ gap.addSubview(dot)
469
+ NSLayoutConstraint.activate([
470
+ gap.widthAnchor.constraint(equalToConstant: 16),
471
+ gap.heightAnchor.constraint(equalToConstant: 20),
472
+ dot.widthAnchor.constraint(equalToConstant: 4),
473
+ dot.heightAnchor.constraint(equalToConstant: 4),
474
+ dot.centerXAnchor.constraint(equalTo: gap.centerXAnchor),
475
+ dot.centerYAnchor.constraint(equalTo: gap.centerYAnchor),
476
+ ])
477
+ line.addArrangedSubview(gap)
478
+ }
479
+ let label = NativeListTextLabel()
480
+ label.font = nativeListFont(ofSize: 14)
481
+ label.lineBreakMode = .byTruncatingTail
482
+ // Match V1: the balance may shrink, but keep the shortened address intact.
483
+ label.setContentCompressionResistancePriority(
484
+ item.data.string("presentation") == "accountSelector" && segment.bool("separatorBefore")
485
+ ? .defaultHigh
486
+ : .defaultLow,
487
+ for: .horizontal
488
+ )
489
+ label.textColor = dataTextColor(segment.string("tone", default: "secondary"), theme: theme)
490
+ setLineHeight(label, text: segment.string("text"), lineHeight: 20)
491
+ let runs = segment.dictionaries("textSegments")
492
+ if !runs.isEmpty {
493
+ let value = NSMutableAttributedString(string: "")
494
+ let paragraph = NSMutableParagraphStyle()
495
+ paragraph.minimumLineHeight = 20
496
+ paragraph.maximumLineHeight = 20
497
+ for run in runs {
498
+ value.append(
499
+ NSAttributedString(
500
+ string: run.string("text"),
501
+ attributes: [
502
+ .font: nativeListFont(ofSize: run.string("style") == "subscript" ? 9 : 14),
503
+ .foregroundColor: label.textColor as Any,
504
+ .paragraphStyle: paragraph,
505
+ .baselineOffset: max(0, (20 - nativeListFont(ofSize: 14).lineHeight) / 2),
506
+ ]))
507
+ }
508
+ label.attributedText = value
509
+ }
510
+ semanticSubtitleLabels.append(label)
511
+ line.addArrangedSubview(label)
512
+ }
513
+ let filler = UIView()
514
+ filler.setContentHuggingPriority(UILayoutPriority(1), for: .horizontal)
515
+ line.addArrangedSubview(filler)
516
+ mainStack.insertArrangedSubview(line, at: 2)
517
+ selectorViews.append(line)
518
+ }
519
+ let matches = item.data.dictionaries("titleMatch")
520
+ if !matches.isEmpty {
521
+ let text = NSMutableAttributedString(
522
+ attributedString: titleLabel.attributedText
523
+ ?? NSAttributedString(string: item.data.string("title")))
524
+ for match in matches {
525
+ let start = match.int("start")
526
+ let end = match.int("end")
527
+ if start >= 0 && end > start && end <= text.length {
528
+ text.addAttribute(
529
+ .foregroundColor, value: nativeListColor(theme, "info", "#0D74CE"),
530
+ range: NSRange(location: start, length: end - start))
531
+ }
532
+ }
533
+ titleLabel.attributedText = text
534
+ }
535
+ tertiaryLabel.textColor = nativeListColor(
536
+ theme,
537
+ item.data.string("tertiaryTone") == "info" ? "info" : "secondaryText",
538
+ item.data.string("tertiaryTone") == "info" ? "#0D74CE" : "#646464"
539
+ )
540
+ let badges = item.data.dictionaries("badges").prefix(2).map { $0.string("text") }
541
+ if !badges.isEmpty {
542
+ let badgeText = badges.joined(separator: " · ")
543
+ show(badgeLabel, badgeText, lines: 1)
544
+ badgeLabel.horizontalInset = 8
545
+ badgeLabel.topInset = 2
546
+ badgeLabel.bottomInset = 2
547
+ setLineHeight(badgeLabel, text: badgeText, lineHeight: 16)
548
+ badgeLabel.backgroundColor = nativeListColor(theme, "strongBackground", "#F0F0F0")
549
+ badgeLabel.textColor = nativeListColor(theme, "secondaryText", "#646464")
550
+ badgeLabel.layer.cornerRadius = 4
551
+ badgeLabel.clipsToBounds = true
552
+ }
553
+ root.addArrangedSubview(trailingStack)
554
+ let accessories = item.data.dictionaries("trailing")
555
+ if accessories.contains(where: { $0.string("kind") == "checkbox" })
556
+ && accessories.contains(where: { $0.string("kind") == "value" })
557
+ {
558
+ trailingStack.axis = .horizontal
559
+ trailingStack.alignment = .center
560
+ trailingStack.spacing = 12
561
+ } else if accessories.count == 2,
562
+ accessories[0].string("kind") == "valuePair",
563
+ accessories[1].string("kind") == "menu"
564
+ {
565
+ trailingStack.axis = .horizontal
566
+ trailingStack.alignment = .center
567
+ trailingStack.spacing = 8
568
+ } else if accessories.count == 2,
569
+ accessories[0].string("kind") == "icon",
570
+ accessories[0].string("name") == "PencilOutline",
571
+ accessories[1].string("kind") == "icon",
572
+ accessories[1].string("name") == "DragOutline"
573
+ {
574
+ trailingStack.axis = .horizontal
575
+ trailingStack.alignment = .center
576
+ // Each source IconButton has a 36-point frame and m=-7. XStack gap=$6
577
+ // therefore places the physical frames 10 points apart (46-point
578
+ // center distance), not 24 points apart.
579
+ trailingStack.spacing = 10
580
+ }
581
+ trailingStack.bind(
582
+ item, descriptors: accessories, theme: theme, style: item.data.dictionary("style") ?? [:],
583
+ defaultSpacing: trailingStack.spacing, checkboxState: checkboxState)
584
+ // Legacy kept the (possibly empty) trailing stack arranged, so the text column always
585
+ // ends 12 points before the trailing edge inset.
586
+ trailingStack.isHidden = false
587
+ if let gap = trailingStack.contentGap { root.setCustomSpacing(gap, after: mainStack) }
588
+ if let inset = trailingStack.endInset { rightInset = -inset }
589
+ if item.data.string("presentation") == "accountSelector",
590
+ accessories.count == 1,
591
+ let accessory = trailingStack.firstVisibleControl
592
+ {
593
+ // The vertical trailing stack has no stable intrinsic width, so keep its
594
+ // flexible space in the account title and subtitle column.
595
+ let width = trailingStack.widthAnchor.constraint(equalTo: accessory.widthAnchor)
596
+ width.isActive = true
597
+ selectorConstraints.append(width)
598
+ }
599
+ }
600
+ private func dataTextColor(_ tone: String, theme: [String: Any]?) -> UIColor {
601
+ switch tone.isEmpty ? "primary" : tone {
602
+ // OneKey patch: account warnings and hidden balances use existing theme tokens.
603
+ case "disabled": return nativeListColor(theme, "disabledText", "#8D8D8D")
604
+ case "caution": return nativeListColor(theme, "caution", "#AB6400")
605
+ case "secondary": return nativeListColor(theme, "secondaryText", "#646464")
606
+ case "positive": return nativeListColor(theme, "positive", "#218358")
607
+ case "negative": return nativeListColor(theme, "negative", "#CE2C31")
608
+ default: return nativeListColor(theme, "primaryText", "#202020")
609
+ }
610
+ }
611
+ private func selectorTabularFont(_ font: UIFont) -> UIFont {
612
+ var settings =
613
+ font.fontDescriptor.fontAttributes[.featureSettings] as? [[UIFontDescriptor.FeatureKey: Int]]
614
+ ?? []
615
+ settings.removeAll { $0[.type] == kNumberSpacingType }
616
+ settings.append([.type: kNumberSpacingType, .selector: kMonospacedNumbersSelector])
617
+ return UIFont(
618
+ descriptor: font.fontDescriptor.addingAttributes([.featureSettings: settings]),
619
+ size: font.pointSize)
620
+ }
621
+ private func selectorTabularText(_ original: NSAttributedString) -> NSAttributedString {
622
+ let result = NSMutableAttributedString(attributedString: original)
623
+ // OneKey patch: body typography explicitly supplies letterSpacing=0 in Tamagui.
624
+ result.addAttribute(.kern, value: 0, range: NSRange(location: 0, length: result.length))
625
+ original.enumerateAttribute(.font, in: NSRange(location: 0, length: original.length)) {
626
+ value, range, _ in
627
+ if let font = value as? UIFont {
628
+ result.addAttribute(.font, value: self.selectorTabularFont(font), range: range)
629
+ }
630
+ }
631
+ return result
632
+ }
633
+ private func applySelectorTypography(_ item: NativeListItem) {
634
+ guard
635
+ ["accountSelector", "networkSelector", "walletSidebar"].contains(
636
+ item.data.string("presentation"))
637
+ else { return }
638
+ func visit(_ view: UIView) {
639
+ if let button = view as? UIButton {
640
+ if let original = button.attributedTitle(for: .normal) {
641
+ selectorTypographyRestorers.append { button.setAttributedTitle(original, for: .normal) }
642
+ button.setAttributedTitle(selectorTabularText(original), for: .normal)
643
+ }
644
+ } else if let label = view as? UILabel, let font = label.font {
645
+ let original = label.attributedText
646
+ selectorTypographyRestorers.append {
647
+ label.font = font
648
+ label.attributedText = original
649
+ }
650
+ label.font = selectorTabularFont(font)
651
+ if let original { label.attributedText = selectorTabularText(original) }
652
+ }
653
+ for child in view.subviews { visit(child) }
654
+ }
655
+ visit(contentView)
656
+ }
657
+ private func setLineHeight(
658
+ _ label: UILabel,
659
+ text: String,
660
+ lineHeight: CGFloat,
661
+ letterSpacing: CGFloat = 0
662
+ ) {
663
+ let paragraphStyle = NSMutableParagraphStyle()
664
+ paragraphStyle.minimumLineHeight = lineHeight
665
+ paragraphStyle.maximumLineHeight = lineHeight
666
+ paragraphStyle.alignment = label.textAlignment
667
+ let presentation = currentItem?.data.string("presentation") ?? ""
668
+ if presentation == "walletSidebar" {
669
+ paragraphStyle.lineBreakMode = label.lineBreakMode
670
+ }
671
+ var attributes: [NSAttributedString.Key: Any] = [
672
+ .font: label.font as Any,
673
+ .foregroundColor: label.textColor as Any,
674
+ .paragraphStyle: paragraphStyle,
675
+ ]
676
+ let isNetworkFallback = label === fallbackLabel && presentation == "networkSelector"
677
+ if (currentItem?.hasExplicitHeight == true
678
+ && ["accountSelector", "walletSidebar"].contains(presentation)) || isNetworkFallback
679
+ {
680
+ attributes[.baselineOffset] = max(0, (lineHeight - label.font.lineHeight) / 2)
681
+ }
682
+ if letterSpacing != 0 { attributes[.kern] = letterSpacing }
683
+ label.attributedText = NSAttributedString(string: text, attributes: attributes)
684
+ }
685
+ }