@onekeyfe/react-native-native-list 3.0.157-alpha.249 → 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,444 @@
1
+ import UIKit
2
+
3
+ final class NativeListDataRowCell: NativeListRendererCell {
4
+ override class func measure(
5
+ _ item: NativeListItem, width: CGFloat, theme: [String: Any]?, layout: String
6
+ ) -> CGFloat? {
7
+ let secondary = item.data.dictionaries("columns").contains {
8
+ !$0.string("secondaryText").isEmpty
9
+ }
10
+ return secondary ? 60 : layout == "table" ? 48 : 56
11
+ }
12
+
13
+ private let visual = NativeListLeadingVisual()
14
+ private let favorite = UIImageView()
15
+ private let accessories = NativeListAccessoryStack()
16
+ private let columns = UIStackView()
17
+ private let cells = (0..<4).map { _ in NativeListTableColumnView() }
18
+ private var rowConstraints: [NSLayoutConstraint] = []
19
+ override func unselectedBackground(
20
+ _ item: NativeListItem, theme: [String: Any]?, layout: String, itemIndex: Int?
21
+ ) -> UIColor {
22
+ let index = item.data["index"] == nil ? itemIndex : item.data.int("index")
23
+ return layout == "table" && index.map({ $0 % 2 == 0 }) == true
24
+ ? nativeListColor(theme, "subduedBackground", "#F9F9F9")
25
+ : super.unselectedBackground(item, theme: theme, layout: layout, itemIndex: itemIndex)
26
+ }
27
+ override var assetFields: [String] { ["leading"] }
28
+ override init(frame: CGRect) {
29
+ super.init(frame: frame)
30
+ root.axis = .horizontal
31
+ favorite.contentMode = .scaleAspectFit
32
+ columns.axis = .horizontal
33
+ columns.alignment = .center
34
+ columns.spacing = 8
35
+ cells.forEach(columns.addArrangedSubview)
36
+ [favorite, visual, accessories, columns].forEach(root.addArrangedSubview)
37
+ accessories.onAction = { [weak self] key, view, slot, target in
38
+ self?.emitAction(key, from: view, source: "trailingAccessory", slot: slot, target: target)
39
+ }
40
+ }
41
+ required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
42
+ override func bindContent(
43
+ _ item: NativeListItem, theme: [String: Any]?, layout: String,
44
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
45
+ ) {
46
+ let data = item.data
47
+ let style = data.dictionary("style") ?? [:]
48
+ let image = style.dictionary("image") ?? [:]
49
+ root.spacing = layout == "table" ? 10 : 8
50
+ root.alignment =
51
+ style.dictionary("container")?.string("contentVerticalAlignment") == "top"
52
+ ? .top
53
+ : style.dictionary("container")?.string("contentVerticalAlignment") == "bottom"
54
+ ? .bottom : .center
55
+ let hp = CGFloat(style.double("horizontalPadding", default: layout == "table" ? 20 : 12))
56
+ let vp = CGFloat(style.double("verticalPadding", default: layout == "table" ? 10 : 8))
57
+ contentInsets = UIEdgeInsets(top: vp, left: hp, bottom: vp, right: hp)
58
+ favorite.isHidden = !data.bool("favorite") && !data.bool("favoriteActive")
59
+ favorite.image = nativeListIcon(
60
+ named: data.bool("favoriteActive") ? "StarSolid" : "StarOutline")
61
+ favorite.tintColor = nativeListColor(
62
+ theme, data.bool("favoriteActive") ? "icon" : "iconSubdued",
63
+ data.bool("favoriteActive") ? "#646464" : "#8D8D8D")
64
+ root.setCustomSpacing(layout == "table" ? 8 : root.spacing, after: favorite)
65
+ visual.isHidden = data.dictionary("leading") == nil
66
+ if let leading = data.dictionary("leading") {
67
+ visual.bind(leading, style: image, key: item.key, theme: theme, isUnread: false)
68
+ } else {
69
+ visual.recycle()
70
+ }
71
+ root.setCustomSpacing(
72
+ CGFloat(style.double("leadingGap", default: Double(root.spacing))), after: visual)
73
+ var descriptors: [[String: Any]] = []
74
+ if data["index"] != nil {
75
+ descriptors.append(["kind": "value", "text": String(data.int("index"))])
76
+ }
77
+ if var checkbox = data.dictionary("checkbox") {
78
+ // Legacy bound the checkbox field directly; it does not need an explicit kind.
79
+ if checkbox["kind"] == nil { checkbox["kind"] = "checkbox" }
80
+ descriptors.append(checkbox)
81
+ }
82
+ var accessoryStyle = style
83
+ accessoryStyle["value"] = style.dictionary("index")
84
+ accessories.bind(
85
+ item, descriptors: descriptors, theme: theme, style: accessoryStyle,
86
+ checkboxState: checkboxState)
87
+ accessories.isHidden = descriptors.isEmpty
88
+ NSLayoutConstraint.deactivate(rowConstraints)
89
+ rowConstraints = [
90
+ favorite.widthAnchor.constraint(equalToConstant: 20),
91
+ favorite.heightAnchor.constraint(equalToConstant: 20),
92
+ visual.widthAnchor.constraint(equalToConstant: CGFloat(image.double("width", default: 40))),
93
+ visual.heightAnchor.constraint(equalToConstant: CGFloat(image.double("height", default: 40))),
94
+ ]
95
+ let values = Array(data.dictionaries("columns").prefix(4))
96
+ for (index, cell) in cells.enumerated() {
97
+ cell.isHidden = index >= values.count
98
+ guard index < values.count else {
99
+ cell.reset()
100
+ continue
101
+ }
102
+ let badges = index == 0 ? data.dictionaries("badges") : []
103
+ if layout == "table" {
104
+ cell.bind(column: values[index], badges: badges, theme: theme)
105
+ } else {
106
+ cell.bindLinear(column: values[index], badges: badges, theme: theme, style: style)
107
+ }
108
+ cell.applyStyle(style, text: NativeListTextStyles.applyStyledText)
109
+ if index > 0 {
110
+ rowConstraints.append(
111
+ cell.widthAnchor.constraint(
112
+ equalTo: cells[0].widthAnchor,
113
+ multiplier: CGFloat(max(1, values[index].int("weight", default: 1)))
114
+ / CGFloat(max(1, values[0].int("weight", default: 1)))))
115
+ }
116
+ }
117
+ NSLayoutConstraint.activate(rowConstraints)
118
+ }
119
+ override func bindSelectionContent(
120
+ _ item: NativeListItem,
121
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
122
+ ) { accessories.updateSelection(item, checkboxState: checkboxState) }
123
+ override func recycleContent() {
124
+ visual.recycle()
125
+ accessories.reset()
126
+ cells.forEach { $0.reset() }
127
+ }
128
+ }
129
+
130
+ private final class NativeListTableColumnView: UIStackView {
131
+ private let primaryLine = UIStackView()
132
+ private let primaryLabel = NativeListTextLabel()
133
+ private let badgesStack = UIStackView()
134
+ private let secondaryLine = UIStackView()
135
+ private let secondaryLeadingLabel = NativeListTextLabel()
136
+ private let secondaryLabel = NativeListTextLabel()
137
+ private var linear = false
138
+
139
+ override init(frame: CGRect) {
140
+ super.init(frame: frame)
141
+ axis = .vertical
142
+ alignment = .leading
143
+ distribution = .fill
144
+ spacing = 4
145
+
146
+ primaryLine.axis = .horizontal
147
+ primaryLine.alignment = .center
148
+ primaryLine.spacing = 6
149
+ primaryLabel.numberOfLines = 1
150
+ primaryLabel.lineBreakMode = .byTruncatingTail
151
+ primaryLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
152
+ badgesStack.axis = .horizontal
153
+ badgesStack.alignment = .center
154
+ badgesStack.spacing = 4
155
+ badgesStack.setContentCompressionResistancePriority(.required, for: .horizontal)
156
+ primaryLine.addArrangedSubview(primaryLabel)
157
+ primaryLine.addArrangedSubview(badgesStack)
158
+
159
+ secondaryLine.axis = .horizontal
160
+ secondaryLine.alignment = .center
161
+ secondaryLine.spacing = 4
162
+ for label in [secondaryLeadingLabel, secondaryLabel] {
163
+ label.numberOfLines = 1
164
+ label.lineBreakMode = .byTruncatingTail
165
+ secondaryLine.addArrangedSubview(label)
166
+ }
167
+ secondaryLeadingLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
168
+ secondaryLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
169
+ secondaryLeadingLabel.widthAnchor.constraint(lessThanOrEqualToConstant: 120).isActive = true
170
+ addArrangedSubview(primaryLine)
171
+ addArrangedSubview(secondaryLine)
172
+ }
173
+
174
+ required init(coder: NSCoder) {
175
+ fatalError("init(coder:) has not been implemented")
176
+ }
177
+
178
+ func reset() {
179
+ linear = false
180
+ for label in [primaryLabel, secondaryLeadingLabel, secondaryLabel] {
181
+ label.rowVerticalAlignment = nil
182
+ label.rowOffsetY = 0
183
+ label.lineBreakMode = .byTruncatingTail
184
+ }
185
+ spacing = 4
186
+ primaryLine.spacing = 6
187
+ primaryLabel.numberOfLines = 1
188
+ secondaryLeadingLabel.numberOfLines = 1
189
+ secondaryLabel.numberOfLines = 1
190
+ primaryLabel.attributedText = nil
191
+ secondaryLeadingLabel.attributedText = nil
192
+ secondaryLabel.attributedText = nil
193
+ secondaryLeadingLabel.isHidden = true
194
+ secondaryLabel.isHidden = true
195
+ secondaryLine.isHidden = true
196
+ badgesStack.arrangedSubviews.forEach {
197
+ badgesStack.removeArrangedSubview($0)
198
+ $0.removeFromSuperview()
199
+ }
200
+ }
201
+
202
+ func bind(
203
+ column: [String: Any],
204
+ badges: [[String: Any]],
205
+ theme: [String: Any]?
206
+ ) {
207
+ reset()
208
+ let textAlignment: NSTextAlignment
209
+ switch column.string("alignment") {
210
+ case "end":
211
+ alignment = .trailing
212
+ textAlignment = .right
213
+ case "center":
214
+ alignment = .center
215
+ textAlignment = .center
216
+ default:
217
+ alignment = .leading
218
+ textAlignment = .left
219
+ }
220
+ primaryLabel.textAlignment = textAlignment
221
+ secondaryLeadingLabel.textAlignment = textAlignment
222
+ secondaryLabel.textAlignment = textAlignment
223
+ primaryLabel.font = nativeListFont(ofSize: 14, weight: .medium)
224
+ secondaryLeadingLabel.font = nativeListFont(ofSize: 12)
225
+ secondaryLabel.font = nativeListFont(ofSize: 12)
226
+ primaryLabel.attributedText = line(
227
+ column.string("text"),
228
+ font: nativeListFont(ofSize: 14, weight: .medium),
229
+ color: textColor(column.string("tone"), theme: theme),
230
+ height: 20
231
+ )
232
+
233
+ for badge in badges.prefix(2) {
234
+ let label = NativeListInsetLabel()
235
+ label.horizontalInset = 6
236
+ label.text = badge.string("text")
237
+ label.font = nativeListFont(ofSize: 10)
238
+ label.textColor = nativeListColor(theme, "info", "#0D74CE")
239
+ label.textAlignment = .center
240
+ label.backgroundColor = UIColor(nativeListHex: "#008FF519", fallback: .systemBlue)
241
+ label.layer.cornerRadius = 4
242
+ label.clipsToBounds = true
243
+ label.translatesAutoresizingMaskIntoConstraints = false
244
+ label.heightAnchor.constraint(equalToConstant: 16).isActive = true
245
+ badgesStack.addArrangedSubview(label)
246
+ }
247
+ badgesStack.isHidden = badges.isEmpty
248
+
249
+ let secondaryLeading = column.string("secondaryLeadingText")
250
+ if !secondaryLeading.isEmpty {
251
+ secondaryLeadingLabel.attributedText = line(
252
+ secondaryLeading,
253
+ font: nativeListFont(ofSize: 12),
254
+ color: nativeListColor(theme, "secondaryText", "#646464"),
255
+ height: 16
256
+ )
257
+ secondaryLeadingLabel.isHidden = false
258
+ secondaryLine.isHidden = false
259
+ }
260
+ let secondary = column.string("secondaryText")
261
+ if !secondary.isEmpty {
262
+ secondaryLabel.attributedText = line(
263
+ secondary,
264
+ font: nativeListFont(ofSize: 12),
265
+ color: textColor(
266
+ column.string("secondaryTone", default: "secondary"),
267
+ theme: theme
268
+ ),
269
+ height: 16
270
+ )
271
+ secondaryLabel.isHidden = false
272
+ secondaryLine.isHidden = false
273
+ }
274
+ }
275
+
276
+ /// Legacy linear layout: one label per column holding the primary text, up to two inline
277
+ /// badge runs and the secondary text on a second line. `secondaryLeadingText` is not shown.
278
+ /// Unstyled output is exactly the legacy label. `style.columns` / `style.columnSecondary`
279
+ /// map onto their attributed ranges (font, weight, color) and paragraphs (lineHeight,
280
+ /// alignment); `lines`/`truncate` and vertical alignment/offset act on the single label.
281
+ func bindLinear(
282
+ column: [String: Any], badges: [[String: Any]], theme: [String: Any]?,
283
+ style: [String: Any] = [:]
284
+ ) {
285
+ reset()
286
+ linear = true
287
+ alignment = .fill
288
+ badgesStack.isHidden = true
289
+ let primaryStyle = style.dictionary("columns") ?? [:]
290
+ let secondaryStyle = style.dictionary("columnSecondary") ?? [:]
291
+ func font(_ textStyle: [String: Any], size: CGFloat, weight: NativeListFontWeight) -> UIFont {
292
+ guard textStyle["fontSize"] != nil || textStyle["fontWeight"] != nil else {
293
+ return nativeListFont(ofSize: size, weight: weight)
294
+ }
295
+ return nativeListFont(
296
+ ofSize: CGFloat(textStyle.double("fontSize", default: Double(size))),
297
+ weight: NativeListTextStyles.marketFontWeight(
298
+ textStyle.string("fontWeight"), fallback: weight))
299
+ }
300
+ func color(_ textStyle: [String: Any], _ fallback: UIColor) -> UIColor {
301
+ (textStyle["color"] as? String).map { UIColor(nativeListHex: $0, fallback: fallback) }
302
+ ?? fallback
303
+ }
304
+ let direction = primaryLabel.effectiveUserInterfaceLayoutDirection
305
+ /// Paragraph attributes only when the style asks for them, so unstyled text stays legacy.
306
+ func paragraph(_ textStyle: [String: Any], font: UIFont, spacing: CGFloat?)
307
+ -> [NSAttributedString.Key: Any]
308
+ {
309
+ guard textStyle["lineHeight"] != nil || textStyle["alignment"] != nil || spacing != nil
310
+ else { return [:] }
311
+ let paragraph = NSMutableParagraphStyle()
312
+ paragraph.alignment = NativeListTextStyles.marketTextAlignment(
313
+ textStyle.string("alignment"), direction: direction)
314
+ if textStyle["alignment"] == nil { paragraph.alignment = primaryLabel.textAlignment }
315
+ var attributes: [NSAttributedString.Key: Any] = [.paragraphStyle: paragraph]
316
+ if textStyle["lineHeight"] != nil {
317
+ let box = CGFloat(textStyle.double("lineHeight"))
318
+ paragraph.minimumLineHeight = box
319
+ paragraph.maximumLineHeight = box
320
+ attributes[.baselineOffset] = max(0, (box - font.lineHeight) / 2)
321
+ }
322
+ if let spacing { paragraph.paragraphSpacing = spacing }
323
+ return attributes
324
+ }
325
+ // Legacy physical alignment from the column data; set before the attributed text so that
326
+ // unstyled paragraphs inherit it.
327
+ primaryLabel.textAlignment =
328
+ column.string("alignment") == "end"
329
+ ? .right : column.string("alignment") == "center" ? .center : .left
330
+ let secondary = column.string("secondaryText")
331
+ let primaryFont = font(primaryStyle, size: 14, weight: .medium)
332
+ let attributed = NSMutableAttributedString(
333
+ string: column.string("text"),
334
+ attributes: [
335
+ .font: primaryFont,
336
+ .foregroundColor: color(primaryStyle, textColor(column.string("tone"), theme: theme)),
337
+ ])
338
+ let primaryLength = attributed.length
339
+ // Every badge run is the legacy " text " pill: both lead spaces sit inside the badge
340
+ // background and are its left padding. `titleBadgeGap` adds an exact, unfilled gap between
341
+ // the title and the first pill as kern on the title's last composed character; unstyled
342
+ // rows add no kern and stay byte-identical to legacy.
343
+ if style["titleBadgeGap"] != nil, !badges.isEmpty, primaryLength > 0 {
344
+ attributed.addAttribute(
345
+ .kern, value: CGFloat(style.double("titleBadgeGap")),
346
+ range: (attributed.string as NSString).rangeOfComposedCharacterSequence(
347
+ at: primaryLength - 1))
348
+ }
349
+ for badge in badges.prefix(2) {
350
+ attributed.append(
351
+ NSAttributedString(
352
+ string: " \(badge.string("text")) ",
353
+ attributes: [
354
+ .font: nativeListFont(ofSize: 12, weight: .medium),
355
+ .foregroundColor: nativeListColor(theme, "info", "#0D74CE"),
356
+ .backgroundColor: UIColor(nativeListHex: "#008FF519", fallback: .systemBlue),
357
+ ]))
358
+ }
359
+ let primaryParagraph = paragraph(
360
+ primaryStyle, font: primaryFont,
361
+ spacing: !secondary.isEmpty && style["lineGap"] != nil
362
+ ? CGFloat(style.double("lineGap")) : nil)
363
+ if !primaryParagraph.isEmpty {
364
+ attributed.addAttributes(
365
+ primaryParagraph, range: NSRange(location: 0, length: attributed.length))
366
+ }
367
+ var lines = min(3, max(1, primaryStyle.int("lines", default: 1)))
368
+ if !secondary.isEmpty {
369
+ let secondaryFont = font(secondaryStyle, size: 12, weight: .regular)
370
+ var attributes: [NSAttributedString.Key: Any] = [
371
+ .font: secondaryFont,
372
+ .foregroundColor: color(
373
+ secondaryStyle,
374
+ textColor(column.string("secondaryTone", default: "secondary"), theme: theme)),
375
+ ]
376
+ attributes.merge(paragraph(secondaryStyle, font: secondaryFont, spacing: nil)) { $1 }
377
+ attributed.append(NSAttributedString(string: "\n\(secondary)", attributes: attributes))
378
+ lines = min(3, lines + max(1, secondaryStyle.int("lines", default: 1)))
379
+ }
380
+ primaryLabel.font = nativeListFont(ofSize: 12)
381
+ primaryLabel.numberOfLines = lines
382
+ primaryLabel.attributedText = attributed
383
+ let truncation =
384
+ primaryStyle["truncate"] != nil || primaryStyle["lines"] != nil
385
+ ? primaryStyle
386
+ : secondaryStyle["truncate"] != nil || secondaryStyle["lines"] != nil ? secondaryStyle : nil
387
+ primaryLabel.lineBreakMode =
388
+ truncation.map { NativeListTextStyles.styledLineBreakMode($0, lines: lines) }
389
+ ?? .byTruncatingTail
390
+ if let vertical = (primaryStyle["verticalAlignment"] ?? secondaryStyle["verticalAlignment"])
391
+ as? String
392
+ {
393
+ primaryLabel.rowVerticalAlignment = vertical
394
+ }
395
+ primaryLabel.rowOffsetY = CGFloat(
396
+ primaryStyle.double("offsetY", default: secondaryStyle.double("offsetY")))
397
+ }
398
+
399
+ func applyStyle(_ style: [String: Any], text: (UILabel, [String: Any]) -> Void) {
400
+ // The linear single label already mapped its styles onto attributed ranges.
401
+ guard !linear else { return }
402
+ if let primary = style.dictionary("columns") {
403
+ if primary["alignment"] != nil { alignment = .fill }
404
+ text(primaryLabel, primary)
405
+ }
406
+ if let secondary = style.dictionary("columnSecondary") {
407
+ if secondary["alignment"] != nil { alignment = .fill }
408
+ text(secondaryLeadingLabel, secondary)
409
+ text(secondaryLabel, secondary)
410
+ }
411
+ if style["lineGap"] != nil { spacing = CGFloat(style.double("lineGap")) }
412
+ if style["titleBadgeGap"] != nil {
413
+ primaryLine.spacing = CGFloat(style.double("titleBadgeGap"))
414
+ }
415
+ }
416
+
417
+ private func line(_ text: String, font: UIFont, color: UIColor, height: CGFloat)
418
+ -> NSAttributedString
419
+ {
420
+ let paragraph = NSMutableParagraphStyle()
421
+ paragraph.minimumLineHeight = height
422
+ paragraph.maximumLineHeight = height
423
+ return NSAttributedString(
424
+ string: text,
425
+ attributes: [
426
+ .font: font,
427
+ .foregroundColor: color,
428
+ .paragraphStyle: paragraph,
429
+ ]
430
+ )
431
+ }
432
+
433
+ private func textColor(_ tone: String, theme: [String: Any]?) -> UIColor {
434
+ switch tone {
435
+ // OneKey patch: account warnings and hidden balances use existing theme tokens.
436
+ case "disabled": return nativeListColor(theme, "disabledText", "#8D8D8D")
437
+ case "caution": return nativeListColor(theme, "caution", "#AB6400")
438
+ case "secondary": return nativeListColor(theme, "secondaryText", "#646464")
439
+ case "positive": return nativeListColor(theme, "positive", "#218358")
440
+ case "negative": return nativeListColor(theme, "negative", "#CE2C31")
441
+ default: return nativeListColor(theme, "primaryText", "#202020")
442
+ }
443
+ }
444
+ }