@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,420 @@
1
+ import UIKit
2
+
3
+ final class NativeListMetricCardCell: NativeListRendererCell {
4
+ override class func measure(
5
+ _ item: NativeListItem, width: CGFloat, theme: [String: Any]?, layout: String
6
+ ) -> CGFloat? {
7
+ return item.data.string("variant") == "activity"
8
+ ? 160 + 1 / UIScreen.main.scale : item.data.string("variant") == "performance" ? 178 : 132
9
+ }
10
+
11
+ private let visual = NativeListLeadingVisual()
12
+ private let mainStack = UIStackView()
13
+ private let titleLine = UIStackView()
14
+ private let titleLabel = NativeListTextLabel()
15
+ private let subtitleLabel = NativeListTextLabel()
16
+ private let statusLabel = NativeListTextLabel()
17
+ private let detailLabel = NativeListTextLabel()
18
+ private let badgeLabel = NativeListTextLabel()
19
+ private let metricCompositeStack = UIStackView()
20
+ private var dimensions: [NSLayoutConstraint] = []
21
+ private var metricVisuals: [String: NativeListMetricVisual] = [:]
22
+ private var usedVisuals = Set<String>()
23
+ private var rowKey = ""
24
+ private var boundTheme: [String: Any]?
25
+ override var assetFields: [String] { ["visual", "metrics"] }
26
+ override var defaultCornerRadius: CGFloat { 12 }
27
+ override func unselectedBackground(
28
+ _ item: NativeListItem, theme: [String: Any]?, layout: String, itemIndex: Int?
29
+ ) -> UIColor {
30
+ // Legacy: a selected card in a sectioned list without its own `selected` flag falls back
31
+ // to rowBackground (the checkbox carries selection), not the subdued card fill.
32
+ selectedState && layout == "sectioned"
33
+ ? nativeListColor(theme, "rowBackground", "#FFFFFF")
34
+ : nativeListColor(theme, "subduedBackground", "#F9F9F9")
35
+ }
36
+ override init(frame: CGRect) {
37
+ super.init(frame: frame)
38
+ mainStack.axis = .vertical
39
+ mainStack.alignment = .fill
40
+ titleLine.axis = .horizontal
41
+ titleLine.alignment = .center
42
+ titleLine.spacing = 8
43
+ titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
44
+ badgeLabel.setContentHuggingPriority(.required, for: .horizontal)
45
+ titleLine.addArrangedSubview(titleLabel)
46
+ titleLine.addArrangedSubview(badgeLabel)
47
+ [titleLine, subtitleLabel, statusLabel, detailLabel].forEach(mainStack.addArrangedSubview)
48
+ metricCompositeStack.axis = .vertical
49
+ metricCompositeStack.alignment = .fill
50
+ // Legacy clipped the standard visual container to its shape even with overlays.
51
+ visual.alwaysClips = true
52
+ }
53
+ required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
54
+ override func bindContent(
55
+ _ item: NativeListItem, theme: [String: Any]?, layout: String,
56
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
57
+ ) {
58
+ rowKey = item.key
59
+ boundTheme = theme
60
+ usedVisuals = []
61
+ contentView.clipsToBounds = true
62
+ root.arrangedSubviews.forEach {
63
+ root.removeArrangedSubview($0)
64
+ $0.removeFromSuperview()
65
+ }
66
+ mainStack.removeArrangedSubview(metricCompositeStack)
67
+ metricCompositeStack.removeFromSuperview()
68
+ metricCompositeStack.arrangedSubviews.forEach {
69
+ metricCompositeStack.removeArrangedSubview($0)
70
+ $0.removeFromSuperview()
71
+ }
72
+ let data = item.data
73
+ let style = data.dictionary("style") ?? [:]
74
+ let image = style.dictionary("image") ?? [:]
75
+ root.axis = .vertical
76
+ root.alignment = .leading
77
+ root.spacing = CGFloat(style.double("lineGap", default: 4))
78
+ mainStack.spacing = CGFloat(style.double("lineGap", default: 2))
79
+ let hp = CGFloat(style.double("horizontalPadding", default: 14))
80
+ let vp = CGFloat(style.double("verticalPadding", default: 14))
81
+ contentInsets = UIEdgeInsets(top: vp, left: hp, bottom: vp, right: hp)
82
+ for label in [titleLabel, subtitleLabel, statusLabel, detailLabel, badgeLabel] {
83
+ label.attributedText = nil
84
+ label.text = nil
85
+ label.isHidden = true
86
+ label.numberOfLines = 1
87
+ label.lineBreakMode = .byTruncatingTail
88
+ label.textAlignment = .natural
89
+ label.rowVerticalAlignment = nil
90
+ label.rowOffsetY = 0
91
+ label.transform = .identity
92
+ }
93
+ titleLabel.font = nativeListFont(
94
+ ofSize: data.string("size") == "large" ? 24 : 18, weight: .semibold)
95
+ subtitleLabel.font = nativeListFont(ofSize: 11)
96
+ statusLabel.font = nativeListFont(ofSize: 12)
97
+ detailLabel.font = nativeListFont(ofSize: 12)
98
+ badgeLabel.font = nativeListFont(ofSize: 12, weight: .medium)
99
+ titleLabel.textColor = nativeListColor(theme, "primaryText", "#202020")
100
+ subtitleLabel.textColor = nativeListColor(theme, "disabledText", "#8D8D8D")
101
+ [statusLabel, detailLabel].forEach {
102
+ $0.textColor = nativeListColor(theme, "secondaryText", "#646464")
103
+ }
104
+ badgeLabel.textColor = nativeListColor(theme, "accent", "#108303")
105
+ let variant = data.string("variant", default: "standard")
106
+ NSLayoutConstraint.deactivate(dimensions)
107
+ dimensions = []
108
+ if variant == "activity" || variant == "performance" {
109
+ visual.recycle()
110
+ bindCompositeMetricCard(item, theme: theme, variant: variant)
111
+ if style["lineGap"] != nil {
112
+ mainStack.spacing = CGFloat(style.double("lineGap"))
113
+ metricCompositeStack.spacing = CGFloat(style.double("lineGap"))
114
+ if variant == "performance",
115
+ let summary = metricCompositeStack.arrangedSubviews.first as? UIStackView
116
+ {
117
+ summary.spacing = CGFloat(style.double("lineGap"))
118
+ }
119
+ }
120
+ if let text = style.dictionary("title") {
121
+ NativeListTextStyles.applyStyledText(titleLabel, text)
122
+ }
123
+ } else {
124
+ if let descriptor = data.dictionary("visual") {
125
+ visual.bind(descriptor, style: image, key: item.key, theme: theme, isUnread: false)
126
+ root.addArrangedSubview(visual)
127
+ dimensions = [
128
+ visual.widthAnchor.constraint(
129
+ equalToConstant: CGFloat(image.double("width", default: 32))),
130
+ visual.heightAnchor.constraint(
131
+ equalToConstant: CGFloat(image.double("height", default: 32))),
132
+ ]
133
+ NSLayoutConstraint.activate(dimensions)
134
+ root.setCustomSpacing(
135
+ CGFloat(style.double("leadingGap", default: Double(root.spacing))), after: visual)
136
+ } else {
137
+ visual.recycle()
138
+ }
139
+ show(titleLabel, data.string("value"), lines: 1)
140
+ show(subtitleLabel, data.string("title"), lines: 1)
141
+ show(statusLabel, data.string("trend"), lines: 1)
142
+ show(detailLabel, data.string("subtitle"), lines: 1)
143
+ show(badgeLabel, data.dictionary("badge")?.string("text") ?? "", lines: 1)
144
+ let tone = data.string("trendTone")
145
+ if tone == "positive" || tone == "negative" {
146
+ statusLabel.textColor = nativeListColor(
147
+ theme, tone, tone == "positive" ? "#218358" : "#CE2C31")
148
+ }
149
+ for (slot, label) in [
150
+ ("title", subtitleLabel), ("value", titleLabel), ("subtitle", detailLabel),
151
+ ("trend", statusLabel),
152
+ ] {
153
+ if let text = style.dictionary(slot) { NativeListTextStyles.applyStyledText(label, text) }
154
+ }
155
+ root.addArrangedSubview(mainStack)
156
+ }
157
+ for key in Array(metricVisuals.keys) where !usedVisuals.contains(key) {
158
+ metricVisuals.removeValue(forKey: key)?.recycle()
159
+ }
160
+ }
161
+ override func recycleContent() {
162
+ visual.recycle()
163
+ metricVisuals.values.forEach { $0.recycle() }
164
+ metricVisuals = [:]
165
+ usedVisuals = []
166
+ }
167
+ private func show(_ view: UILabel, _ text: String, lines: Int) {
168
+ view.text = text
169
+ view.numberOfLines = lines
170
+ view.isHidden = text.isEmpty
171
+ }
172
+ private func setLineHeight(
173
+ _ view: UILabel, text: String, lineHeight: CGFloat, letterSpacing: CGFloat? = nil
174
+ ) {
175
+ let paragraph = NSMutableParagraphStyle()
176
+ paragraph.minimumLineHeight = lineHeight
177
+ paragraph.maximumLineHeight = lineHeight
178
+ paragraph.alignment = view.textAlignment
179
+ var attrs: [NSAttributedString.Key: Any] = [
180
+ .font: view.font as Any, .foregroundColor: view.textColor as Any, .paragraphStyle: paragraph,
181
+ ]
182
+ if let letterSpacing { attrs[.kern] = letterSpacing }
183
+ view.attributedText = NSAttributedString(string: text, attributes: attrs)
184
+ }
185
+ private func makeMetricVisual(_ visual: [String: Any], key: String, slot: Int, role: String)
186
+ -> UIView
187
+ {
188
+ let identity = "\(rowKey):\(role):\(slot):\(key)"
189
+ usedVisuals.insert(identity)
190
+ let view = metricVisuals[identity] ?? NativeListMetricVisual()
191
+ metricVisuals[identity] = view
192
+ // Legacy metric images used the themed strongBackground placeholder.
193
+ view.bind(
194
+ visual, key: identity,
195
+ placeholder: boundTheme?.string("strongBackground", default: "#0000000F") ?? "#0000000F")
196
+ return view
197
+ }
198
+ private func bindCompositeMetricCard(
199
+ _ item: NativeListItem,
200
+ theme: [String: Any]?,
201
+ variant: String
202
+ ) {
203
+ root.alignment = .fill
204
+ show(titleLabel, item.data.string("title"), lines: 1)
205
+ titleLabel.font = nativeListFont(ofSize: 11)
206
+ titleLabel.textColor = nativeListColor(theme, "disabledText", "#8D8D8D")
207
+ setLineHeight(
208
+ titleLabel,
209
+ text: item.data.string("title").uppercased(),
210
+ lineHeight: 14,
211
+ letterSpacing: 1.2
212
+ )
213
+ mainStack.spacing = 14
214
+ metricCompositeStack.spacing = 14
215
+ mainStack.addArrangedSubview(metricCompositeStack)
216
+ root.addArrangedSubview(mainStack)
217
+
218
+ let metrics = item.data.dictionaries("metrics")
219
+ let topCount = min(2, metrics.count)
220
+ if variant == "activity" {
221
+ metricCompositeStack.addArrangedSubview(
222
+ makeMetricRow(
223
+ Array(metrics.prefix(topCount)),
224
+ theme: theme,
225
+ style: "activityHero"
226
+ )
227
+ )
228
+ let divider = UIView()
229
+ divider.backgroundColor = nativeListColor(theme, "separator", "#E0E0E0")
230
+ divider.heightAnchor.constraint(equalToConstant: 1 / UIScreen.main.scale).isActive = true
231
+ metricCompositeStack.addArrangedSubview(divider)
232
+ metricCompositeStack.addArrangedSubview(
233
+ makeMetricRow(
234
+ Array(metrics.dropFirst(topCount)),
235
+ theme: theme,
236
+ style: "compact"
237
+ )
238
+ )
239
+ } else {
240
+ let performanceSummary = UIStackView()
241
+ performanceSummary.axis = .vertical
242
+ performanceSummary.alignment = .fill
243
+ performanceSummary.spacing = 8
244
+ performanceSummary.addArrangedSubview(
245
+ makeMetricRow(
246
+ Array(metrics.prefix(topCount)),
247
+ theme: theme,
248
+ style: "performanceHero"
249
+ )
250
+ )
251
+ let progress = min(1, max(0, item.data.double("progress")))
252
+ let progressRow = UIStackView()
253
+ progressRow.axis = .horizontal
254
+ progressRow.spacing = 0
255
+ progressRow.layer.cornerRadius = 2
256
+ progressRow.clipsToBounds = true
257
+ let wins = UIView()
258
+ wins.backgroundColor = nativeListColor(theme, "positive", "#218358")
259
+ let losses = UIView()
260
+ losses.backgroundColor = nativeListColor(theme, "negative", "#CE2C31")
261
+ progressRow.addArrangedSubview(wins)
262
+ progressRow.addArrangedSubview(losses)
263
+ progressRow.heightAnchor.constraint(equalToConstant: 4).isActive = true
264
+ if progress <= 0 {
265
+ wins.isHidden = true
266
+ } else if progress >= 1 {
267
+ losses.isHidden = true
268
+ } else {
269
+ wins.widthAnchor.constraint(
270
+ equalTo: progressRow.widthAnchor,
271
+ multiplier: CGFloat(progress)
272
+ ).isActive = true
273
+ }
274
+ performanceSummary.addArrangedSubview(progressRow)
275
+ metricCompositeStack.addArrangedSubview(performanceSummary)
276
+ metricCompositeStack.addArrangedSubview(
277
+ makeMetricRow(
278
+ Array(metrics.dropFirst(topCount)),
279
+ theme: theme,
280
+ style: "compactShaded"
281
+ )
282
+ )
283
+ }
284
+ contentView.layer.cornerRadius = 12
285
+ contentView.clipsToBounds = true
286
+ }
287
+ private func makeMetricRow(
288
+ _ metrics: [[String: Any]],
289
+ theme: [String: Any]?,
290
+ style: String
291
+ ) -> UIStackView {
292
+ let shaded = style == "compactShaded"
293
+ let row = UIStackView()
294
+ row.axis = .horizontal
295
+ row.alignment = style.hasSuffix("Hero") ? .bottom : .fill
296
+ row.distribution = .fillEqually
297
+ row.spacing = shaded ? 8 : 12
298
+ metrics.enumerated().forEach { index, metric in
299
+ let column = UIStackView()
300
+ column.axis = .vertical
301
+ column.alignment =
302
+ shaded
303
+ ? .leading
304
+ : index == 0 ? .leading : index == metrics.count - 1 ? .trailing : .center
305
+ column.spacing = 2
306
+ if shaded {
307
+ column.isLayoutMarginsRelativeArrangement = true
308
+ column.layoutMargins = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
309
+ column.backgroundColor = UIColor(nativeListHex: "#0000000F", fallback: .lightGray)
310
+ column.layer.cornerRadius = 8
311
+ }
312
+ let label = UILabel()
313
+ label.font = nativeListFont(ofSize: 11)
314
+ label.textColor = nativeListColor(theme, "disabledText", "#8D8D8D")
315
+ setLineHeight(label, text: metric.string("label"), lineHeight: 14)
316
+ let value = UILabel()
317
+ let valueSize: CGFloat
318
+ let valueLineHeight: CGFloat
319
+ let valueWeight: NativeListFontWeight
320
+ switch style {
321
+ case "activityHero" where index == 0:
322
+ valueSize = 16
323
+ valueLineHeight = 24
324
+ valueWeight = .semibold
325
+ case "performanceHero" where index == 0:
326
+ valueSize = 18
327
+ valueLineHeight = 24
328
+ valueWeight = .semibold
329
+ case "activityHero", "performanceHero":
330
+ valueSize = 14
331
+ valueLineHeight = 20
332
+ valueWeight = .semibold
333
+ default:
334
+ valueSize = 14
335
+ valueLineHeight = 20
336
+ valueWeight = .medium
337
+ }
338
+ value.font = nativeListTabularFont(ofSize: valueSize, weight: valueWeight)
339
+ value.textColor = dataTextColor(metric.string("tone"), theme: theme)
340
+ setLineHeight(value, text: metric.string("value"), lineHeight: valueLineHeight)
341
+ column.addArrangedSubview(label)
342
+ if let visual = metric.dictionary("visual") {
343
+ let valueRow = UIStackView()
344
+ valueRow.axis = .horizontal
345
+ valueRow.alignment = .center
346
+ valueRow.spacing = 6
347
+ valueRow.addArrangedSubview(
348
+ makeMetricVisual(visual, key: metric.string("key"), slot: index, role: style)
349
+ )
350
+ valueRow.addArrangedSubview(value)
351
+ column.addArrangedSubview(valueRow)
352
+ } else {
353
+ column.addArrangedSubview(value)
354
+ }
355
+ row.addArrangedSubview(column)
356
+ }
357
+ return row
358
+ }
359
+ private func dataTextColor(_ tone: String, theme: [String: Any]?) -> UIColor {
360
+ switch tone.isEmpty ? "primary" : tone {
361
+ // OneKey patch: account warnings and hidden balances use existing theme tokens.
362
+ case "disabled": return nativeListColor(theme, "disabledText", "#8D8D8D")
363
+ case "caution": return nativeListColor(theme, "caution", "#AB6400")
364
+ case "secondary": return nativeListColor(theme, "secondaryText", "#646464")
365
+ case "positive": return nativeListColor(theme, "positive", "#218358")
366
+ case "negative": return nativeListColor(theme, "negative", "#CE2C31")
367
+ default: return nativeListColor(theme, "primaryText", "#202020")
368
+ }
369
+ }
370
+ }
371
+
372
+ private final class NativeListMetricVisual: UIView {
373
+ private let icon = UIImageView()
374
+ private let image = NativeListImageSlot()
375
+ init() {
376
+ super.init(frame: .zero)
377
+ translatesAutoresizingMaskIntoConstraints = false
378
+ icon.contentMode = .scaleAspectFit
379
+ addSubview(icon)
380
+ addSubview(image.view)
381
+ NSLayoutConstraint.activate([
382
+ widthAnchor.constraint(equalToConstant: 16), heightAnchor.constraint(equalToConstant: 16),
383
+ ])
384
+ clipsToBounds = true
385
+ }
386
+ required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
387
+ func bind(_ visual: [String: Any], key: String, placeholder: String) {
388
+ backgroundColor = UIColor(
389
+ nativeListHex: visual.string("backgroundColor", default: "#0000000F"), fallback: .lightGray)
390
+ layer.cornerRadius = visual.string("shape") == "square" ? 0 : 8
391
+ let kind = visual.string("kind")
392
+ icon.isHidden = kind != "icon"
393
+ image.view.isHidden = true
394
+ if kind == "icon" {
395
+ image.recycle()
396
+ icon.image = nativeListIcon(named: visual.string("name"))
397
+ icon.tintColor = UIColor(
398
+ nativeListHex: visual.string("tintColor", default: "#00000072"), fallback: .darkGray)
399
+ } else if let source = kind == "stackedImages"
400
+ ? visual.dictionaries("images").first : visual.dictionary("image")
401
+ {
402
+ image.view.isHidden = false
403
+ image.bind(
404
+ source, key: key,
405
+ variant: kind == "token"
406
+ ? "token"
407
+ : kind == "network"
408
+ ? "network" : ["wallet", "account"].contains(kind) ? "avatar" : "generic", fit: nil,
409
+ placeholder: placeholder)
410
+ } else {
411
+ image.recycle()
412
+ }
413
+ }
414
+ override func layoutSubviews() {
415
+ super.layoutSubviews()
416
+ icon.frame = bounds
417
+ image.view.frame = bounds
418
+ }
419
+ func recycle() { image.recycle() }
420
+ }
@@ -1,6 +1,14 @@
1
1
  import Foundation
2
2
  import UIKit
3
3
 
4
+ // Template type alone partitions reuse; data keys and styles never select a renderer.
5
+ enum NativeListRendererKey: String, CaseIterable {
6
+ case message, rail, mediaTile, action, system, activity, dataRow, metricCard, market, identity, sectionHeader, walletGroup
7
+ /// Not a template: legacy accepted any non-empty type and drew an empty row with the
8
+ /// shared chrome. Unknown types keep that placeholder instead of failing the snapshot.
9
+ case unsupported = "__nativeListUnsupported"
10
+ }
11
+
4
12
  struct NativeListItem {
5
13
  let key: String
6
14
  let type: String
@@ -9,6 +17,18 @@ struct NativeListItem {
9
17
  let data: [String: Any]
10
18
  let content: String
11
19
 
20
+ let rendererKey: NativeListRendererKey
21
+
22
+ var styledHeight: CGFloat? {
23
+ (data.dictionary("style")?.dictionary("container")?["height"] as? Double).map { CGFloat($0) }
24
+ }
25
+
26
+ /// Selector presentations use their explicit-height geometry when either
27
+ /// `style.container.height` or `row.height` is set (Web `hasExplicitRowHeight`).
28
+ var hasExplicitHeight: Bool {
29
+ data.dictionary("style")?.dictionary("container")?["height"] != nil || data["height"] != nil
30
+ }
31
+
12
32
  var isSelectable: Bool {
13
33
  let disabled = data["disabled"] as? Bool ?? false
14
34
  return !disabled && Self.selectableTypes.contains(type)
@@ -33,8 +53,12 @@ struct NativeListItem {
33
53
  let type = data["type"] as? String, !type.isEmpty else {
34
54
  throw NativeListModelError.invalidRow
35
55
  }
56
+ let rendererKey = NativeListRendererKey(rawValue: type)
57
+ .flatMap { $0 == .unsupported ? nil : $0 } ?? .unsupported
58
+ if rendererKey == .unsupported { Self.logUnsupported(type) }
36
59
  self.key = key
37
60
  self.type = type
61
+ self.rendererKey = rendererKey
38
62
  self.sectionKey = data["sectionKey"] as? String
39
63
  self.revision = data["revision"] as? Int ?? 0
40
64
  self.data = data
@@ -42,6 +66,12 @@ struct NativeListItem {
42
66
  self.content = String(data: jsonData, encoding: .utf8) ?? ""
43
67
  }
44
68
 
69
+ private static var loggedUnsupportedTypes = Set<String>()
70
+ private static func logUnsupported(_ type: String) {
71
+ guard loggedUnsupportedTypes.insert(type).inserted else { return }
72
+ NSLog("[NativeList] Unsupported row type '%@' rendered as an empty placeholder row", type)
73
+ }
74
+
45
75
  private static let selectableTypes: Set<String> = [
46
76
  "identity", "rail", "activity", "message", "dataRow", "mediaTile", "metricCard",
47
77
  ]
@@ -70,6 +100,7 @@ struct NativeListConfig {
70
100
  let sectionIndexHapticsEnabled: Bool
71
101
  let sectionIndexCenteredInWindow: Bool
72
102
  let theme: [String: Any]?
103
+ let listStyle: [String: Any]?
73
104
  let fixedFooter: NativeListItem?
74
105
  var items: [NativeListItem]
75
106
 
@@ -132,6 +163,7 @@ struct NativeListConfig {
132
163
  sectionIndexHapticsEnabled: sectionIndex?["hapticsEnabled"] as? Bool ?? true,
133
164
  sectionIndexCenteredInWindow: sectionIndex?["centeredInWindow"] as? Bool ?? false,
134
165
  theme: root["theme"] as? [String: Any],
166
+ listStyle: root["listStyle"] as? [String: Any],
135
167
  fixedFooter: footer,
136
168
  items: items
137
169
  )
@@ -0,0 +1,152 @@
1
+ import UIKit
2
+
3
+ enum NativeListRailRenderer {
4
+ struct Resolved {
5
+ let title: NativeListResolvedText
6
+ let badge: NativeListResolvedText
7
+ let status: NativeListResolvedText
8
+ let horizontalPadding: CGFloat
9
+ let verticalPadding: CGFloat
10
+ let leadingGap: CGFloat
11
+ let badgeGap: CGFloat
12
+ let trailingGap: CGFloat
13
+ let imageWidth: CGFloat
14
+ let imageHeight: CGFloat
15
+ let imageStyle: [String: Any]
16
+ let visual: [String: Any]?
17
+ let alignment: UIStackView.Alignment
18
+
19
+ init(_ item: NativeListItem, theme: [String: Any]?) {
20
+ let style = item.data.dictionary("style") ?? [:]
21
+ let badge = item.data.dictionary("badge") ?? [:]
22
+ title = NativeListResolvedText(
23
+ item.data.string("title"), style: style.dictionary("title"),
24
+ size: 12, weight: .medium, color: nativeListColor(theme, "primaryText", "#202020"),
25
+ lineHeight: 16, lines: 1)
26
+ self.badge = NativeListResolvedText(
27
+ badge.string("text"), style: style.dictionary("badge"),
28
+ size: 12, weight: .medium,
29
+ color: nativeListColor(
30
+ theme,
31
+ badge.string("tone") == "success"
32
+ ? "positive" : badge.string("tone") == "danger" ? "negative" : "secondaryText",
33
+ badge.string("tone") == "success"
34
+ ? "#218358" : badge.string("tone") == "danger" ? "#CE2C31" : "#646464"), lineHeight: 16,
35
+ lines: 1, tabular: true)
36
+ let status = item.data.string("status")
37
+ self.status = NativeListResolvedText(
38
+ status == "none" ? "" : status, style: style.dictionary("status"),
39
+ size: 12, color: nativeListColor(theme, "secondaryText", "#646464"), lineHeight: 16,
40
+ lines: 1, tabular: true)
41
+ horizontalPadding = CGFloat(style.double("horizontalPadding", default: 4))
42
+ verticalPadding = CGFloat(style.double("verticalPadding", default: 4))
43
+ leadingGap = CGFloat(style.double("leadingGap", default: 6))
44
+ // The legacy iOS title/badge stack uses 8, independently of the rail's 6 gap.
45
+ badgeGap = CGFloat(style.double("titleBadgeGap", default: 8))
46
+ trailingGap = CGFloat(style.double("trailingGap", default: 6))
47
+ imageStyle = style.dictionary("image") ?? [:]
48
+ imageWidth = CGFloat(imageStyle.double("width", default: 20))
49
+ imageHeight = CGFloat(imageStyle.double("height", default: 20))
50
+ visual = item.data.dictionary("visual")
51
+ switch style.dictionary("container")?.string("contentVerticalAlignment") {
52
+ case "top": alignment = .top
53
+ case "bottom": alignment = .bottom
54
+ default: alignment = .center
55
+ }
56
+ }
57
+ var horizontalWidth: CGFloat {
58
+ func width(_ text: NativeListResolvedText) -> CGFloat {
59
+ (text.text as NSString).size(withAttributes: [.font: text.font]).width
60
+ }
61
+ // Preserve the legacy viewport bounds and trailing allowance. The old
62
+ // estimator budgeted 6 points for the actual 8-point title/badge gap.
63
+ return min(
64
+ 288,
65
+ max(
66
+ 72,
67
+ ceil(
68
+ horizontalPadding * 2 + imageWidth + leadingGap
69
+ + width(title) + (badge.text.isEmpty ? 0 : badgeGap + width(badge))
70
+ + (status.text.isEmpty ? 0 : trailingGap + width(status))
71
+ + (badge.text.isEmpty ? 8 : 6))))
72
+ }
73
+ }
74
+ }
75
+
76
+ final class NativeListRailCell: NativeListRendererCell {
77
+ override class func measure(
78
+ _ item: NativeListItem, width: CGFloat, theme: [String: Any]?, layout: String
79
+ ) -> CGFloat? {
80
+ return 40
81
+ }
82
+
83
+ private let title = NativeListTextLabel()
84
+ private let badge = NativeListTextLabel()
85
+ private let status = NativeListTextLabel()
86
+ private let titleLine = UIStackView()
87
+ private let textLine = UIStackView()
88
+ private var visual: NativeListLeadingVisual?
89
+ private var dimensions: [NSLayoutConstraint] = []
90
+ override var defaultCornerRadius: CGFloat { 8 }
91
+ override var pressedCornerRadius: CGFloat? { 8 }
92
+ override var assetFields: [String] { ["visual"] }
93
+ override var showsSelection: Bool { false }
94
+ // Legacy Rail used a fixed pressed tint, independent of theme.rowPressedBackground.
95
+ override func pressedBackground(_ theme: [String: Any]?) -> UIColor {
96
+ UIColor(nativeListHex: "#F0F0F0", fallback: .lightGray)
97
+ }
98
+
99
+ override init(frame: CGRect) {
100
+ super.init(frame: frame)
101
+ root.axis = .horizontal
102
+ titleLine.axis = .horizontal
103
+ textLine.axis = .horizontal
104
+ titleLine.alignment = .center
105
+ textLine.alignment = .center
106
+ [title, badge].forEach(titleLine.addArrangedSubview)
107
+ [titleLine, status].forEach(textLine.addArrangedSubview)
108
+ root.addArrangedSubview(textLine)
109
+ [title, titleLine, textLine].forEach {
110
+ $0.setContentHuggingPriority(.required, for: .horizontal)
111
+ $0.setContentCompressionResistancePriority(.required, for: .horizontal)
112
+ }
113
+ }
114
+ required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
115
+ override func bindContent(
116
+ _ item: NativeListItem, theme: [String: Any]?, layout: String,
117
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
118
+ ) {
119
+ let r = NativeListRailRenderer.Resolved(item, theme: theme)
120
+ r.title.bind(title)
121
+ r.badge.bind(badge)
122
+ r.status.bind(status)
123
+ badge.setContentHuggingPriority(.required, for: .horizontal)
124
+ title.setContentHuggingPriority(r.title.lines > 1 ? .defaultLow : .required, for: .horizontal)
125
+ status.setContentHuggingPriority(r.title.lines > 1 ? .required : .defaultLow, for: .horizontal)
126
+ root.alignment = r.alignment
127
+ root.spacing = r.leadingGap
128
+ titleLine.spacing = r.badgeGap
129
+ textLine.spacing = r.trailingGap
130
+ contentInsets = UIEdgeInsets(
131
+ top: r.verticalPadding, left: r.horizontalPadding,
132
+ bottom: r.verticalPadding, right: r.horizontalPadding)
133
+ NSLayoutConstraint.deactivate(dimensions)
134
+ dimensions.removeAll(keepingCapacity: true)
135
+ if let source = r.visual {
136
+ let view = visual ?? NativeListLeadingVisual(frame: .zero)
137
+ visual = view
138
+ if view.superview == nil { root.insertArrangedSubview(view, at: 0) }
139
+ view.bind(source, style: r.imageStyle, key: item.key, theme: theme, isUnread: false)
140
+ dimensions = [
141
+ view.widthAnchor.constraint(equalToConstant: r.imageWidth),
142
+ view.heightAnchor.constraint(equalToConstant: r.imageHeight),
143
+ ]
144
+ NSLayoutConstraint.activate(dimensions)
145
+ } else if let visual {
146
+ visual.recycle()
147
+ root.removeArrangedSubview(visual)
148
+ visual.removeFromSuperview()
149
+ }
150
+ }
151
+ override func recycleContent() { visual?.recycle() }
152
+ }