@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,698 @@
1
+ import UIKit
2
+
3
+ final class NativeListMarketCell: NativeListRendererCell {
4
+ override class func measure(
5
+ _ item: NativeListItem, width: CGFloat, theme: [String: Any]?, layout: String
6
+ ) -> CGFloat? {
7
+ let style = item.data.dictionary("style")
8
+ let stock = item.data.string("variant") == "stock"
9
+ return max(
10
+ stock ? 72 : 68,
11
+ CGFloat(
12
+ style?.dictionary("image")?.double("height", default: stock ? 40 : 32) ?? (stock ? 40 : 32))
13
+ + 2 * CGFloat(style?.double("verticalPadding", default: 12) ?? 12))
14
+ }
15
+ private let visual = NativeListLeadingVisual(frame: .zero)
16
+ private let mainStack = UIStackView()
17
+ private let titleRowStack = UIStackView()
18
+ private let titleLabel = NativeListTextLabel()
19
+ private let subtitleLabel = NativeListTextLabel()
20
+ private let tertiaryLabel = NativeListTextLabel()
21
+ private let marketSubtitleStack = UIStackView()
22
+ private let marketSubtitleSpacer = UIView()
23
+ private let trailingStack = UIStackView()
24
+ private let accessoryButtons = (0..<2).map { _ in NativeListAccessoryButton(type: .system) }
25
+ private let marketBadgeButtons = (0..<3).map { _ in NativeListAccessoryButton(type: .system) }
26
+ private let badgeSlots = (0..<3).map { _ in NativeListImageSlot() }
27
+ private var marketBadgeImages: [UIView] { badgeSlots.map { $0.view } }
28
+ private let leadingActionButton = UIButton(type: .system)
29
+ private var leadingWidth: NSLayoutConstraint!
30
+ private var leadingHeight: NSLayoutConstraint!
31
+ private var selectorConstraints: [NSLayoutConstraint] = []
32
+ private var accessorySizeConstraints: [NSLayoutConstraint] = []
33
+ private var leadingActionKey: String?
34
+ private var marketBadgeActionKeys: [String?] = []
35
+ private var imageSignature = ""
36
+ override var defaultCornerRadius: CGFloat { isHighlighted ? 12 : 0 }
37
+ override var assetFields: [String] { ["leading", "badges"] }
38
+ override init(frame: CGRect) {
39
+ super.init(frame: frame)
40
+ root.axis = .horizontal
41
+ root.alignment = .center
42
+ mainStack.axis = .vertical
43
+ mainStack.alignment = .fill
44
+ mainStack.setContentHuggingPriority(UILayoutPriority(1), for: .horizontal)
45
+ mainStack.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
46
+ titleRowStack.axis = .horizontal
47
+ titleRowStack.alignment = .center
48
+ titleRowStack.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
49
+ titleLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
50
+ titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
51
+ titleRowStack.addArrangedSubview(titleLabel)
52
+ for (index, button) in marketBadgeButtons.enumerated() {
53
+ button.tag = index
54
+ button.layer.cornerRadius = 4
55
+ button.clipsToBounds = true
56
+ button.addTarget(self, action: #selector(badgePressed(_:)), for: .touchUpInside)
57
+ let image = badgeSlots[index].view
58
+ image.isUserInteractionEnabled = false
59
+ image.translatesAutoresizingMaskIntoConstraints = false
60
+ button.addSubview(image)
61
+ NSLayoutConstraint.activate([
62
+ image.centerYAnchor.constraint(equalTo: button.centerYAnchor),
63
+ image.widthAnchor.constraint(equalToConstant: 14),
64
+ image.heightAnchor.constraint(equalToConstant: 14),
65
+ ])
66
+ titleRowStack.addArrangedSubview(button)
67
+ }
68
+ trailingStack.setContentHuggingPriority(.required, for: .horizontal)
69
+ trailingStack.setContentCompressionResistancePriority(.required, for: .horizontal)
70
+ accessoryButtons.forEach(trailingStack.addArrangedSubview)
71
+ visual.translatesAutoresizingMaskIntoConstraints = false
72
+ leadingWidth = visual.widthAnchor.constraint(equalToConstant: 32)
73
+ leadingHeight = visual.heightAnchor.constraint(equalToConstant: 32)
74
+ leadingActionButton.translatesAutoresizingMaskIntoConstraints = false
75
+ NSLayoutConstraint.activate([
76
+ leadingWidth, leadingHeight, leadingActionButton.widthAnchor.constraint(equalToConstant: 36),
77
+ leadingActionButton.heightAnchor.constraint(equalToConstant: 36),
78
+ ])
79
+ leadingActionButton.adjustsImageWhenDisabled = false
80
+ leadingActionButton.tintAdjustmentMode = .normal
81
+ leadingActionButton.addTarget(self, action: #selector(leadingPressed), for: .touchUpInside)
82
+ }
83
+ required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
84
+ override func bindContent(
85
+ _ item: NativeListItem, theme: [String: Any]?, layout: String,
86
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
87
+ ) {
88
+ NSLayoutConstraint.deactivate(selectorConstraints + accessorySizeConstraints)
89
+ selectorConstraints = []
90
+ accessorySizeConstraints = []
91
+ root.arrangedSubviews.forEach {
92
+ root.removeArrangedSubview($0)
93
+ $0.removeFromSuperview()
94
+ }
95
+ mainStack.arrangedSubviews.forEach {
96
+ mainStack.removeArrangedSubview($0)
97
+ $0.removeFromSuperview()
98
+ }
99
+ marketSubtitleStack.arrangedSubviews.forEach {
100
+ marketSubtitleStack.removeArrangedSubview($0)
101
+ $0.removeFromSuperview()
102
+ }
103
+ mainStack.alignment = .fill
104
+ titleRowStack.setContentHuggingPriority(.defaultLow, for: .horizontal)
105
+ for label in [titleLabel, subtitleLabel, tertiaryLabel] {
106
+ label.attributedText = nil
107
+ label.text = nil
108
+ label.isHidden = true
109
+ label.numberOfLines = 1
110
+ label.lineBreakMode = .byTruncatingTail
111
+ label.textAlignment = .natural
112
+ label.rowOffsetY = 0
113
+ label.rowVerticalAlignment = nil
114
+ label.setContentHuggingPriority(.defaultLow, for: .horizontal)
115
+ label.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
116
+ }
117
+ titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
118
+ [titleRowStack, subtitleLabel, tertiaryLabel].forEach(mainStack.addArrangedSubview)
119
+ leadingActionKey = nil
120
+ leadingActionButton.setImage(nil, for: .normal)
121
+ for (index, button) in marketBadgeButtons.enumerated() {
122
+ resetButton(button)
123
+ button.isHidden = true
124
+ marketBadgeImages[index].isHidden = true
125
+ if index >= item.data.dictionaries("badges").count
126
+ || item.data.dictionaries("badges")[index].dictionary("icon") == nil
127
+ {
128
+ badgeSlots[index].recycle()
129
+ }
130
+ }
131
+ accessoryButtons.forEach(resetButton)
132
+ bindMarket(item, theme: theme)
133
+ let style = item.data.dictionary("style")
134
+ for (label, descriptor) in [
135
+ (titleLabel, style?.dictionary("title")), (subtitleLabel, style?.dictionary("subtitle")),
136
+ (tertiaryLabel, item.data.dictionary("subtitlePrefix")?.dictionary("style")),
137
+ ] {
138
+ if let descriptor { NativeListTextStyles.applyStyledText(label, textLayoutStyle(descriptor)) }
139
+ }
140
+ root.alignment =
141
+ style?.dictionary("container")?.string("contentVerticalAlignment") == "top"
142
+ ? .top
143
+ : style?.dictionary("container")?.string("contentVerticalAlignment") == "bottom"
144
+ ? .bottom : .center
145
+ }
146
+ private func resetButton(_ button: NativeListAccessoryButton) {
147
+ button.setTitle(nil, for: .normal)
148
+ button.setAttributedTitle(nil, for: .normal)
149
+ button.setImage(nil, for: .normal)
150
+ button.marketLineHeight = nil
151
+ button.selectorSummaryLineHeight = nil
152
+ button.rowTextOffsetY = 0
153
+ button.contentVerticalAlignment = .center
154
+ button.contentHorizontalAlignment = .center
155
+ button.titleLabel?.numberOfLines = 1
156
+ button.titleLabel?.lineBreakMode = .byTruncatingTail
157
+ button.titleLabel?.textAlignment = .natural
158
+ button.contentEdgeInsets = .zero
159
+ button.imageEdgeInsets = .zero
160
+ button.titleEdgeInsets = .zero
161
+ }
162
+ private func textLayoutStyle(_ style: [String: Any]) -> [String: Any] {
163
+ style.filter { ["lines", "truncate", "verticalAlignment", "offsetY"].contains($0.key) }
164
+ }
165
+ override func accessibilityText(_ item: NativeListItem) -> String {
166
+ item.data.string(
167
+ "accessibilityLabel",
168
+ default: [
169
+ item.data.string("title"), item.data.string("subtitle"), item.data.string("price"),
170
+ item.data.dictionary("change")?.string("text") ?? "",
171
+ ].filter { !$0.isEmpty }.joined(separator: ", "))
172
+ }
173
+ override func updateMarketQuote(_ item: NativeListItem, theme: [String: Any]?) {
174
+ guard retainBoundItem(item) else { return }
175
+ bindQuote(item, theme: theme)
176
+ }
177
+ override func recycleContent() {
178
+ visual.recycle()
179
+ badgeSlots.forEach { $0.recycle() }
180
+ imageSignature = ""
181
+ }
182
+ @objc private func leadingPressed() {
183
+ if let key = leadingActionKey {
184
+ emitAction(key, from: leadingActionButton, source: "leadingAction")
185
+ }
186
+ }
187
+ @objc private func badgePressed(_ button: UIButton) {
188
+ if marketBadgeActionKeys.indices.contains(button.tag),
189
+ let key = marketBadgeActionKeys[button.tag]
190
+ {
191
+ emitAction(key, from: button, source: "marketBadge", slot: button.tag)
192
+ }
193
+ }
194
+ private func show(_ label: UILabel, _ value: String, lines: Int) {
195
+ label.text = value
196
+ label.numberOfLines = lines
197
+ label.isHidden = value.isEmpty
198
+ }
199
+ private func setLineHeight(_ label: UILabel, text: String, lineHeight: CGFloat) {
200
+ let paragraph = NSMutableParagraphStyle()
201
+ paragraph.minimumLineHeight = lineHeight
202
+ paragraph.maximumLineHeight = lineHeight
203
+ paragraph.alignment = label.textAlignment
204
+ paragraph.lineBreakMode = label.lineBreakMode
205
+ let baseline = max(0, (lineHeight - label.font.lineHeight) / 2)
206
+ let scale = window?.screen.scale ?? traitCollection.displayScale
207
+ label.attributedText = NSAttributedString(
208
+ string: text,
209
+ attributes: [
210
+ .font: label.font as Any, .foregroundColor: label.textColor as Any,
211
+ .paragraphStyle: paragraph, .kern: 0,
212
+ .baselineOffset: lineHeight == 20 && label.font.pointSize == 14 && scale > 0
213
+ ? ceil(baseline * scale) / scale : baseline,
214
+ ])
215
+ }
216
+ private func setButtonLine(
217
+ _ button: UIButton, text: String, font: UIFont, color: UIColor, lineHeight: CGFloat
218
+ ) {
219
+ guard !text.isEmpty else { return }
220
+ let paragraph = NSMutableParagraphStyle()
221
+ paragraph.minimumLineHeight = lineHeight
222
+ paragraph.maximumLineHeight = lineHeight
223
+ paragraph.alignment = .natural
224
+ (button as? NativeListAccessoryButton)?.marketLineHeight = lineHeight
225
+ let baseline = max(0, (lineHeight - font.lineHeight) / 2)
226
+ let scale = window?.screen.scale ?? traitCollection.displayScale
227
+ button.titleLabel?.font = font
228
+ button.setAttributedTitle(
229
+ NSAttributedString(
230
+ string: text,
231
+ attributes: [
232
+ .font: font, .foregroundColor: color, .paragraphStyle: paragraph, .kern: 0,
233
+ .baselineOffset: lineHeight == 20 && font.pointSize == 14 && scale > 0
234
+ ? ceil(baseline * scale) / scale : baseline,
235
+ ]), for: .normal)
236
+ }
237
+ private func marketFontWeight(_ value: String, fallback: NativeListFontWeight)
238
+ -> NativeListFontWeight
239
+ {
240
+ switch value {
241
+ case "regular": return .regular
242
+ case "semibold": return .semibold
243
+ case "bold": return .bold
244
+ case "medium": return .medium
245
+ default: return fallback
246
+ }
247
+ }
248
+ private func marketTextAlignment(_ value: String) -> NSTextAlignment {
249
+ switch value {
250
+ case "center": return .center
251
+ case "start": return effectiveUserInterfaceLayoutDirection == .rightToLeft ? .right : .left
252
+ case "end": return effectiveUserInterfaceLayoutDirection == .rightToLeft ? .left : .right
253
+ default: return .natural
254
+ }
255
+ }
256
+ private func applyMarketTextStyle(
257
+ _ label: UILabel,
258
+ data: [String: Any]?,
259
+ theme: [String: Any]?,
260
+ defaultSize: CGFloat,
261
+ defaultLineHeight: CGFloat,
262
+ defaultWeight: NativeListFontWeight,
263
+ defaultColor: UIColor,
264
+ defaultAlignment: NSTextAlignment = .natural
265
+ ) {
266
+ let size = CGFloat(
267
+ data?.double("fontSize", default: Double(defaultSize)) ?? Double(defaultSize))
268
+ let lineHeight = CGFloat(
269
+ data?.double("lineHeight", default: Double(defaultLineHeight)) ?? Double(defaultLineHeight))
270
+ label.font = nativeListTabularFont(
271
+ ofSize: size,
272
+ weight: marketFontWeight(data?.string("fontWeight") ?? "", fallback: defaultWeight))
273
+ label.textColor =
274
+ data?["color"].flatMap { $0 as? String }.map {
275
+ UIColor(nativeListHex: $0, fallback: defaultColor)
276
+ } ?? defaultColor
277
+ label.textAlignment =
278
+ data?["alignment"] == nil
279
+ ? defaultAlignment
280
+ : marketTextAlignment(data?.string("alignment") ?? "")
281
+ label.numberOfLines = min(3, max(1, data?.int("lines", default: 1) ?? 1))
282
+ setLineHeight(label, text: label.text ?? "", lineHeight: lineHeight)
283
+ }
284
+ private func applyMarketButtonStyle(
285
+ _ button: UIButton,
286
+ data: [String: Any]?,
287
+ defaultSize: CGFloat,
288
+ defaultLineHeight: CGFloat,
289
+ defaultWeight: NativeListFontWeight,
290
+ color: UIColor,
291
+ defaultAlignment: UIControl.ContentHorizontalAlignment
292
+ ) {
293
+ let size = CGFloat(
294
+ data?.double("fontSize", default: Double(defaultSize)) ?? Double(defaultSize))
295
+ let lineHeight = CGFloat(
296
+ data?.double("lineHeight", default: Double(defaultLineHeight)) ?? Double(defaultLineHeight))
297
+ setButtonLine(
298
+ button,
299
+ text: button.title(for: .normal) ?? "",
300
+ font: nativeListTabularFont(
301
+ ofSize: size,
302
+ weight: marketFontWeight(data?.string("fontWeight") ?? "", fallback: defaultWeight)),
303
+ color: data?["color"].flatMap { $0 as? String }.map {
304
+ UIColor(nativeListHex: $0, fallback: color)
305
+ } ?? color,
306
+ lineHeight: lineHeight
307
+ )
308
+ if data?["alignment"] == nil {
309
+ button.contentHorizontalAlignment = defaultAlignment
310
+ } else {
311
+ button.contentHorizontalAlignment =
312
+ data?.string("alignment") == "start"
313
+ ? .leading
314
+ : data?.string("alignment") == "end" ? .trailing : .center
315
+ }
316
+ button.titleLabel?.numberOfLines = min(3, max(1, data?.int("lines", default: 1) ?? 1))
317
+ }
318
+ private func marketAttributedText(
319
+ _ text: String,
320
+ segments: [[String: Any]],
321
+ style: [String: Any]?,
322
+ defaultSize: CGFloat,
323
+ defaultLineHeight: CGFloat,
324
+ defaultWeight: NativeListFontWeight,
325
+ color: UIColor,
326
+ defaultAlignment: NSTextAlignment
327
+ ) -> NSAttributedString {
328
+ let size = CGFloat(
329
+ style?.double("fontSize", default: Double(defaultSize)) ?? Double(defaultSize))
330
+ let lineHeight = CGFloat(
331
+ style?.double("lineHeight", default: Double(defaultLineHeight)) ?? Double(defaultLineHeight))
332
+ let paragraph = NSMutableParagraphStyle()
333
+ paragraph.minimumLineHeight = lineHeight
334
+ paragraph.maximumLineHeight = lineHeight
335
+ paragraph.alignment =
336
+ style?["alignment"] == nil
337
+ ? defaultAlignment
338
+ : marketTextAlignment(style?.string("alignment") ?? "")
339
+ let weight = marketFontWeight(style?.string("fontWeight") ?? "", fallback: defaultWeight)
340
+ let resolvedColor =
341
+ style?["color"].flatMap { $0 as? String }.map { UIColor(nativeListHex: $0, fallback: color) }
342
+ ?? color
343
+ let result = NSMutableAttributedString(string: "")
344
+ let source = segments.isEmpty ? [["text": text]] : segments
345
+ for segment in source {
346
+ // Legacy parity: subscript runs always use ceil(0.6 x fontSize), styled or not.
347
+ let segmentSize = segment.string("style") == "subscript" ? ceil(size * 0.6) : size
348
+ result.append(
349
+ NSAttributedString(
350
+ string: segment.string("text"),
351
+ attributes: [
352
+ .font: nativeListTabularFont(ofSize: segmentSize, weight: weight),
353
+ .foregroundColor: resolvedColor,
354
+ .kern: 0,
355
+ .paragraphStyle: paragraph,
356
+ .baselineOffset: max(
357
+ 0, (lineHeight - nativeListTabularFont(ofSize: size, weight: weight).lineHeight) / 2),
358
+ ]))
359
+ }
360
+ return result
361
+ }
362
+ private func marketLeading(_ item: NativeListItem, style: [String: Any]?) -> [String: Any]? {
363
+ guard var visual = item.data.dictionary("leading") else { return nil }
364
+ guard let imageStyle = style?.dictionary("image") else { return visual }
365
+ if let shape = imageStyle["shape"] as? String { visual["shape"] = shape }
366
+ if let contentFit = imageStyle["contentFit"] as? String, var image = visual.dictionary("image")
367
+ {
368
+ image["contentFit"] = contentFit
369
+ visual["image"] = image
370
+ }
371
+ return visual
372
+ }
373
+ private func bindMarket(_ item: NativeListItem, theme: [String: Any]?) {
374
+ let variant = item.data.string("variant")
375
+ let style = item.data.dictionary("style")
376
+ let imageStyle = style?.dictionary("image")
377
+ let imageWidth = CGFloat(
378
+ imageStyle?.double("width", default: variant == "stock" ? 40 : 32)
379
+ ?? (variant == "stock" ? 40 : 32))
380
+ let imageHeight = CGFloat(
381
+ imageStyle?.double("height", default: variant == "stock" ? 40 : 32)
382
+ ?? (variant == "stock" ? 40 : 32))
383
+ let horizontalPadding = CGFloat(
384
+ style?.double("horizontalPadding", default: variant == "perp" ? 16 : 20)
385
+ ?? (variant == "perp" ? 16 : 20))
386
+ let verticalPadding = CGFloat(style?.double("verticalPadding", default: 12) ?? 12)
387
+ contentInsets = UIEdgeInsets(
388
+ top: verticalPadding, left: horizontalPadding, bottom: verticalPadding,
389
+ right: horizontalPadding)
390
+ root.spacing = CGFloat(
391
+ style?.double("leadingGap", default: variant == "perp" ? 8 : 14)
392
+ ?? (variant == "perp" ? 8 : 14))
393
+ if let leadingAction = item.data.dictionary("leadingAction") {
394
+ leadingActionButton.isHidden = false
395
+ let tintColor = UIColor(
396
+ nativeListHex: leadingAction.string("tintColor", default: "#646464"),
397
+ fallback: .darkGray
398
+ )
399
+ leadingActionButton.tintColor = tintColor
400
+ if let image = nativeListIcon(named: leadingAction.string("name")) {
401
+ leadingActionButton.setImage(image, for: .normal)
402
+ leadingActionButton.setImage(
403
+ image.withTintColor(tintColor, renderingMode: .alwaysOriginal),
404
+ for: .disabled
405
+ )
406
+ }
407
+ leadingActionButton.isEnabled = !leadingAction.bool("disabled")
408
+ leadingActionButton.alpha = leadingActionButton.isEnabled ? 1 : 0.4
409
+ leadingActionButton.accessibilityIdentifier = leadingAction["testID"] as? String
410
+ leadingActionButton.accessibilityLabel = leadingAction["accessibilityLabel"] as? String
411
+ leadingActionKey = leadingAction.string("actionKey")
412
+ root.addArrangedSubview(leadingActionButton)
413
+ root.setCustomSpacing(5, after: leadingActionButton)
414
+ }
415
+ leadingWidth.constant = imageWidth
416
+ leadingHeight.constant = imageHeight
417
+ if let descriptor = marketLeading(item, style: style) {
418
+ var resolved = imageStyle ?? [:]
419
+ resolved["cornerRadius"] =
420
+ imageStyle?.double(
421
+ "cornerRadius",
422
+ default: imageStyle?.string("shape") == "square"
423
+ ? 0
424
+ : imageStyle?.string("shape") == "rounded"
425
+ ? 8 : Double(min(imageWidth, imageHeight) / 2))
426
+ ?? Double(min(imageWidth, imageHeight) / 2)
427
+ visual.bitmapBorderWidth = descriptor.string("borderColor").isEmpty ? 0 : 1
428
+ visual.bind(descriptor, style: resolved, key: item.key, theme: theme, isUnread: false)
429
+ root.addArrangedSubview(visual)
430
+ let signature = NativeListImageSlot.signature([
431
+ "key": item.key, "visual": descriptor, "fit": resolved["contentFit"] ?? NSNull(),
432
+ ])
433
+ if signature != imageSignature,
434
+ let diagnostic = item.data.dictionary("diagnostics")?.string("imageBindActionKey"),
435
+ !diagnostic.isEmpty,
436
+ descriptor.dictionary("image") != nil || descriptor.dictionary("networkImage") != nil
437
+ {
438
+ onAction?(item, diagnostic, nil, nil)
439
+ }
440
+ imageSignature = signature
441
+ } else {
442
+ visual.recycle()
443
+ imageSignature = ""
444
+ }
445
+ root.addArrangedSubview(mainStack)
446
+ // OneKey patch: opt in to the source Market row's content gap.
447
+ // root.setCustomSpacing(0, after: mainStack)
448
+ root.setCustomSpacing(
449
+ CGFloat(style?.double("contentTrailingGap", default: 0) ?? 0), after: mainStack)
450
+ mainStack.spacing = CGFloat(style?.double("lineGap", default: 0) ?? 0)
451
+ titleRowStack.spacing = CGFloat(style?.double("titleBadgeGap", default: 4) ?? 4)
452
+ // OneKey patch: opt in without changing the other row templates' filled layout.
453
+ if style?.string("titleBadgeLayout") == "inline" {
454
+ mainStack.alignment = .leading
455
+ titleRowStack.setContentHuggingPriority(.required, for: .horizontal)
456
+ }
457
+ show(
458
+ titleLabel, item.data.string("title"),
459
+ lines: style?.dictionary("title")?.int("lines", default: 1) ?? 1)
460
+ applyMarketTextStyle(
461
+ titleLabel, data: style?.dictionary("title"), theme: theme, defaultSize: 16,
462
+ defaultLineHeight: 24, defaultWeight: .medium,
463
+ defaultColor: nativeListColor(theme, "primaryText", "#202020"))
464
+ let badges = Array(item.data.dictionaries("badges").prefix(marketBadgeButtons.count))
465
+ marketBadgeActionKeys = badges.map { $0["actionKey"] as? String }
466
+ for (index, badge) in badges.enumerated() {
467
+ let button = marketBadgeButtons[index]
468
+ let badgeStyle = badge.dictionary("style")
469
+ let badgeFontSize = CGFloat(badgeStyle?.double("fontSize", default: 11) ?? 11)
470
+ let badgeFontWeight = marketFontWeight(
471
+ badgeStyle?.string("fontWeight") ?? "", fallback: .medium)
472
+ // OneKey patch: SizableText supplies tabular numerals for explicit Market metrics.
473
+ let badgeFont =
474
+ badgeStyle == nil
475
+ ? nativeListFont(ofSize: badgeFontSize, weight: badgeFontWeight)
476
+ : nativeListTabularFont(ofSize: badgeFontSize, weight: badgeFontWeight)
477
+ let hasBuiltInIcon = badge.string("iconName") == "verified"
478
+ let hasRemoteIcon = badge.dictionary("icon") != nil
479
+ let hasIcon = hasBuiltInIcon || hasRemoteIcon
480
+ let text = badge.string("text")
481
+ let toneColor: UIColor
482
+ switch badge.string("tone") {
483
+ case "success": toneColor = nativeListColor(theme, "positive", "#218358")
484
+ case "danger": toneColor = nativeListColor(theme, "negative", "#CE2C31")
485
+ case "info": toneColor = nativeListColor(theme, "info", "#0D74CE")
486
+ case "warning": toneColor = nativeListColor(theme, "primaryText", "#202020")
487
+ default: toneColor = nativeListColor(theme, "secondaryText", "#646464")
488
+ }
489
+ let foreground = UIColor(
490
+ nativeListHex: badge.string("textColor", default: ""),
491
+ fallback: toneColor
492
+ )
493
+ button.isHidden = false
494
+ button.isEnabled = true
495
+ button.isUserInteractionEnabled = !badge.string("actionKey").isEmpty
496
+ button.accessibilityTraits = button.isUserInteractionEnabled ? .button : .staticText
497
+ button.setTitle(text, for: .normal)
498
+ button.setTitleColor(foreground, for: .normal)
499
+ button.titleLabel?.font = badgeFont
500
+ if let lineHeight = badgeStyle?["lineHeight"] as? Double {
501
+ setButtonLine(
502
+ button, text: text, font: badgeFont, color: foreground, lineHeight: CGFloat(lineHeight))
503
+ // The explicit text-only line box must not cover an adjacent icon.
504
+ if hasIcon { button.marketLineHeight = nil }
505
+ }
506
+ button.tintColor = foreground
507
+ button.backgroundColor = UIColor(
508
+ nativeListHex: badge.string("backgroundColor", default: ""),
509
+ fallback: hasIcon && text.isEmpty
510
+ ? .clear
511
+ : nativeListColor(theme, "strongBackground", "#0000000F")
512
+ )
513
+ let iconOnly = hasIcon && text.isEmpty
514
+ let iconSize: CGFloat = hasBuiltInIcon ? 16 : 14
515
+ // OneKey patch: preserve the native defaults unless the caller supplies padding.
516
+ let padding = CGFloat(badgeStyle?.double("horizontalPadding", default: 5) ?? 5)
517
+ let hasCustomPadding = badgeStyle?["horizontalPadding"] != nil
518
+ let leftPadding =
519
+ hasCustomPadding
520
+ ? padding + (hasRemoteIcon ? iconSize + 2 : 0) : hasRemoteIcon ? 20 : hasIcon ? 3 : 5
521
+ // button.contentEdgeInsets = UIEdgeInsets(top: 0, left: iconOnly ? 0 : hasRemoteIcon ? 20 : hasIcon ? 3 : 5, bottom: 0, right: iconOnly ? 0 : 5)
522
+ button.contentEdgeInsets = UIEdgeInsets(
523
+ top: 0, left: iconOnly ? 0 : leftPadding, bottom: 0, right: iconOnly ? 0 : padding)
524
+ button.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: text.isEmpty ? 0 : 3)
525
+ button.titleEdgeInsets = .zero
526
+ button.imageView?.contentMode = .scaleAspectFit
527
+ if hasBuiltInIcon {
528
+ button.setImage(
529
+ nativeListIcon(
530
+ named: "BadgeVerifiedSolid", size: CGSize(width: iconSize, height: iconSize)),
531
+ for: .normal
532
+ )
533
+ }
534
+ // OneKey patch: match source badge metrics at physical-pixel precision.
535
+ // let height = button.heightAnchor.constraint(equalToConstant: 18)
536
+ let height = button.heightAnchor.constraint(
537
+ equalToConstant: CGFloat(badgeStyle?.double("height", default: 18) ?? 18))
538
+ let textWidth = (text as NSString).size(withAttributes: [.font: badgeFont]).width
539
+ let scale = max(1, traitCollection.displayScale)
540
+ let roundedTextWidth = badgeStyle == nil ? ceil(textWidth) : ceil(textWidth * scale) / scale
541
+ let extraWidth =
542
+ hasCustomPadding ? padding * 2 + (hasIcon ? iconSize + 2 : 0) : hasIcon ? iconSize + 11 : 10
543
+ // let width = button.widthAnchor.constraint(equalToConstant: iconOnly ? iconSize : ceil(textWidth) + (hasIcon ? iconSize + 11 : 10))
544
+ let width = button.widthAnchor.constraint(
545
+ equalToConstant: iconOnly ? iconSize : roundedTextWidth + extraWidth)
546
+ NSLayoutConstraint.activate([width, height])
547
+ selectorConstraints.append(contentsOf: [width, height])
548
+ if let icon = badge.dictionary("icon") {
549
+ marketBadgeImages[index].isHidden = false
550
+ marketBadgeImages[index].layer.cornerRadius = 7
551
+ marketBadgeImages[index].clipsToBounds = true
552
+ let imageLeading = marketBadgeImages[index].leadingAnchor.constraint(
553
+ equalTo: button.leadingAnchor, constant: text.isEmpty ? 0 : 4)
554
+ imageLeading.isActive = true
555
+ selectorConstraints.append(imageLeading)
556
+ badgeSlots[index].bind(icon, key: "\(item.key):badge:\(index)")
557
+ }
558
+ button.accessibilityLabel = badge.string("accessibilityLabel", default: badge.string("text"))
559
+ }
560
+ if !item.data.string("subtitle").isEmpty || !item.data.dictionaries("subtitleSegments").isEmpty
561
+ {
562
+ show(
563
+ subtitleLabel, item.data.string("subtitle"),
564
+ lines: style?.dictionary("subtitle")?.int("lines", default: 1) ?? 1)
565
+ applyMarketTextStyle(
566
+ subtitleLabel, data: style?.dictionary("subtitle"), theme: theme, defaultSize: 14,
567
+ defaultLineHeight: 20, defaultWeight: .regular,
568
+ defaultColor: nativeListColor(theme, "secondaryText", "#646464"))
569
+ if !item.data.dictionaries("subtitleSegments").isEmpty {
570
+ subtitleLabel.attributedText = marketAttributedText(
571
+ item.data.string("subtitle"), segments: item.data.dictionaries("subtitleSegments"),
572
+ style: style?.dictionary("subtitle"), defaultSize: 14, defaultLineHeight: 20,
573
+ defaultWeight: .regular, color: nativeListColor(theme, "secondaryText", "#646464"),
574
+ defaultAlignment: .natural)
575
+ }
576
+ }
577
+ // OneKey patch: preserve volume width while the localized name truncates.
578
+ let subtitlePrefix = item.data.dictionary("subtitlePrefix")
579
+ let subtitlePadding = CGFloat(style?.double("subtitleTrailingPadding", default: 0) ?? 0)
580
+ if subtitlePrefix != nil || subtitlePadding > 0 {
581
+ mainStack.removeArrangedSubview(subtitleLabel)
582
+ subtitleLabel.removeFromSuperview()
583
+ mainStack.removeArrangedSubview(tertiaryLabel)
584
+ tertiaryLabel.removeFromSuperview()
585
+ marketSubtitleStack.axis = .horizontal
586
+ marketSubtitleStack.alignment = .center
587
+ marketSubtitleStack.spacing = 0
588
+ marketSubtitleStack.clipsToBounds = true
589
+ show(tertiaryLabel, subtitlePrefix?.string("text") ?? "", lines: 1)
590
+ applyMarketTextStyle(
591
+ tertiaryLabel, data: subtitlePrefix?.dictionary("style"), theme: theme, defaultSize: 12,
592
+ defaultLineHeight: 16, defaultWeight: .regular,
593
+ defaultColor: nativeListColor(theme, "secondaryText", "#646464"))
594
+ tertiaryLabel.setContentHuggingPriority(.required, for: .horizontal)
595
+ tertiaryLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
596
+ subtitleLabel.setContentHuggingPriority(.required, for: .horizontal)
597
+ subtitleLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
598
+ marketSubtitleStack.addArrangedSubview(tertiaryLabel)
599
+ marketSubtitleStack.addArrangedSubview(subtitleLabel)
600
+ marketSubtitleStack.addArrangedSubview(marketSubtitleSpacer)
601
+ if !tertiaryLabel.isHidden && !subtitleLabel.isHidden {
602
+ marketSubtitleStack.setCustomSpacing(
603
+ CGFloat(subtitlePrefix?.double("gap", default: 4) ?? 4), after: tertiaryLabel)
604
+ }
605
+ mainStack.insertArrangedSubview(marketSubtitleStack, at: 1)
606
+ let width = marketSubtitleStack.widthAnchor.constraint(
607
+ equalTo: mainStack.widthAnchor, constant: -subtitlePadding)
608
+ width.isActive = true
609
+ selectorConstraints.append(width)
610
+ if let maxWidth = subtitlePrefix?["maxWidth"] as? Double {
611
+ let limit = tertiaryLabel.widthAnchor.constraint(
612
+ lessThanOrEqualToConstant: CGFloat(maxWidth))
613
+ limit.isActive = true
614
+ selectorConstraints.append(limit)
615
+ }
616
+ marketSubtitleStack.isHidden = tertiaryLabel.isHidden && subtitleLabel.isHidden
617
+ }
618
+ root.addArrangedSubview(trailingStack)
619
+ trailingStack.axis = .horizontal
620
+ trailingStack.alignment = .center
621
+ trailingStack.spacing = CGFloat(style?.double("trailingGap", default: 8) ?? 8)
622
+ bindQuote(item, theme: theme)
623
+ }
624
+ private func bindQuote(_ item: NativeListItem, theme: [String: Any]?) {
625
+ NSLayoutConstraint.deactivate(accessorySizeConstraints)
626
+ accessorySizeConstraints.removeAll()
627
+ let style = item.data.dictionary("style")
628
+ let price = accessoryButtons[0]
629
+ price.isUserInteractionEnabled = false
630
+ price.isHidden = false
631
+ price.setAttributedTitle(nil, for: .normal)
632
+ price.setTitle(item.data.string("price"), for: .normal)
633
+ applyMarketButtonStyle(
634
+ price, data: style?.dictionary("price"), defaultSize: 16, defaultLineHeight: 24,
635
+ defaultWeight: .medium, color: nativeListColor(theme, "primaryText", "#202020"),
636
+ defaultAlignment: .trailing)
637
+ if !item.data.dictionaries("priceSegments").isEmpty {
638
+ price.setAttributedTitle(
639
+ marketAttributedText(
640
+ item.data.string("price"), segments: item.data.dictionaries("priceSegments"),
641
+ style: style?.dictionary("price"), defaultSize: 16, defaultLineHeight: 24,
642
+ defaultWeight: .medium, color: nativeListColor(theme, "primaryText", "#202020"),
643
+ defaultAlignment: marketTextAlignment("end")), for: .normal)
644
+ }
645
+ let changeData = item.data.dictionary("change") ?? [:]
646
+ let change = accessoryButtons[1]
647
+ change.isUserInteractionEnabled = false
648
+ change.isHidden = false
649
+ change.setAttributedTitle(nil, for: .normal)
650
+ change.setTitle(changeData.string("text"), for: .normal)
651
+ let defaultChangeColor = nativeListColor(theme, "inverseText", "#FFFFFF")
652
+ applyMarketButtonStyle(
653
+ change, data: style?.dictionary("change"), defaultSize: 14, defaultLineHeight: 20,
654
+ defaultWeight: .medium,
655
+ color: UIColor(
656
+ nativeListHex: changeData.string("textColor", default: "#FFFFFF"),
657
+ fallback: defaultChangeColor), defaultAlignment: .center)
658
+ if !changeData.dictionaries("textSegments").isEmpty {
659
+ let changeTextColor = UIColor(
660
+ nativeListHex: changeData.string("textColor", default: ""), fallback: defaultChangeColor)
661
+ change.setAttributedTitle(
662
+ marketAttributedText(
663
+ changeData.string("text"), segments: changeData.dictionaries("textSegments"),
664
+ style: style?.dictionary("change"), defaultSize: 14, defaultLineHeight: 20,
665
+ defaultWeight: .medium, color: changeTextColor, defaultAlignment: .center), for: .normal)
666
+ }
667
+ let toneKey =
668
+ changeData.string("tone") == "positive"
669
+ ? "positive" : changeData.string("tone") == "negative" ? "negative" : "secondaryText"
670
+ change.backgroundColor = UIColor(
671
+ nativeListHex: changeData.string("backgroundColor", default: ""),
672
+ fallback: nativeListColor(
673
+ theme, toneKey,
674
+ changeData.string("tone") == "positive"
675
+ ? "#218358" : changeData.string("tone") == "negative" ? "#CE2C31" : "#8D8D8D"))
676
+ change.layer.cornerRadius = CGFloat(style?.double("changeCornerRadius", default: 8) ?? 8)
677
+ change.clipsToBounds = true
678
+ let width = change.widthAnchor.constraint(
679
+ equalToConstant: CGFloat(style?.double("changeWidth", default: 80) ?? 80))
680
+ let height = change.heightAnchor.constraint(
681
+ equalToConstant: CGFloat(style?.double("changeHeight", default: 32) ?? 32))
682
+ width.isActive = true
683
+ height.isActive = true
684
+ accessorySizeConstraints.append(contentsOf: [width, height])
685
+ if let priceStyle = style?.dictionary("price"), !textLayoutStyle(priceStyle).isEmpty {
686
+ NativeListTextStyles.applyStyledButton(price, textLayoutStyle(priceStyle))
687
+ }
688
+ if let changeStyle = style?.dictionary("change"), !textLayoutStyle(changeStyle).isEmpty {
689
+ NativeListTextStyles.applyStyledButton(change, textLayoutStyle(changeStyle))
690
+ }
691
+ accessibilityLabel = item.data.string(
692
+ "accessibilityLabel",
693
+ default: [
694
+ item.data.string("title"), item.data.string("subtitle"), item.data.string("price"),
695
+ changeData.string("text"),
696
+ ].filter { !$0.isEmpty }.joined(separator: ", "))
697
+ }
698
+ }