@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,311 @@
1
+ import UIKit
2
+
3
+ // Shared chrome and binding lifetime; the subclass owns its template views.
4
+ enum NativeListRendererUpdate { case unchanged, content, assets, replace }
5
+
6
+ class NativeListRendererCell: NativeListRowHost {
7
+ let root = UIStackView()
8
+ var contentInsets = UIEdgeInsets.zero
9
+ var usesIntrinsicContentHeight: Bool { false }
10
+ var defaultBorderWidth: CGFloat { 0 }
11
+ func defaultBorderColor(_ theme: [String: Any]?) -> UIColor { .clear }
12
+ var defaultCornerRadius: CGFloat { 0 }
13
+ /// Legacy pressed rows round all corners (12pt) while highlighted; nil opts out.
14
+ var pressedCornerRadius: CGFloat? { pressChangesBackground ? 12 : nil }
15
+ /// Radius for `groupPosition` rows when listStyle does not set one (legacy 12).
16
+ var groupCornerRadius: CGFloat { 12 }
17
+ var defaultCornerCurve: CALayerCornerCurve { .circular }
18
+ var pressChangesBackground: Bool { true }
19
+ func pressedBackground(_ theme: [String: Any]?) -> UIColor {
20
+ nativeListColor(theme, "rowPressedBackground", "#E8E8E8")
21
+ }
22
+ var showsSelection: Bool { true }
23
+ /// Whether the legacy row-level `backgroundColor` paints this row (resting and full-width
24
+ /// fills). `style.container.backgroundColor` always applies.
25
+ var honorsRowBackgroundColor: Bool { true }
26
+ var defaultVerticalAlignment: String? { nil }
27
+ var defaultSeparatorInset: CGFloat { 12 }
28
+ func bindContent(
29
+ _ item: NativeListItem, theme: [String: Any]?, layout: String,
30
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
31
+ ) {}
32
+ func recycleContent() {}
33
+ func bindSelectionContent(
34
+ _ item: NativeListItem,
35
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
36
+ ) {}
37
+ var assetFields: [String] { [] }
38
+ private func update(from old: NativeListItem?, to new: NativeListItem) -> NativeListRendererUpdate
39
+ {
40
+ guard let old, old.key == new.key, old.rendererKey == new.rendererKey else { return .replace }
41
+ if old.content == new.content { return .unchanged }
42
+ for field in assetFields where !nativeListValuesEqual(old.data[field], new.data[field]) {
43
+ return .assets
44
+ }
45
+ return .content
46
+ }
47
+ private let separator = UIView()
48
+ private let fullWidthBackground = CALayer()
49
+ private var contentPosition: NSLayoutConstraint?
50
+ private var rootConstraints: [NSLayoutConstraint] = []
51
+ private var item: NativeListItem?
52
+ private var theme: [String: Any]?
53
+ private var layout = "linear"
54
+ private var itemIndex: Int?
55
+ func unselectedBackground(
56
+ _ item: NativeListItem, theme: [String: Any]?, layout: String, itemIndex: Int?
57
+ ) -> UIColor { nativeListColor(theme, "rowBackground", "#FFFFFF") }
58
+ private(set) var selectedState = false
59
+ // Last applied list-level inputs; compared structurally (no serialization/hashing per bind).
60
+ // The layout direction is an input because renderers resolve start/end alignment at bind.
61
+ private var boundInputs:
62
+ (
63
+ theme: NSDictionary?, layout: String, listStyle: NSDictionary?,
64
+ direction: UIUserInterfaceLayoutDirection
65
+ )?
66
+
67
+ override init(frame: CGRect) {
68
+ super.init(frame: frame)
69
+ root.translatesAutoresizingMaskIntoConstraints = false
70
+ contentView.addSubview(root)
71
+ contentView.addSubview(separator)
72
+ rootConstraints = [
73
+ root.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
74
+ root.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
75
+ root.topAnchor.constraint(equalTo: contentView.topAnchor),
76
+ root.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
77
+ ]
78
+ NSLayoutConstraint.activate(rootConstraints)
79
+ }
80
+ required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
81
+
82
+ func accessibilityText(_ item: NativeListItem) -> String {
83
+ item.data.string("accessibilityLabel", default: item.data.string("title"))
84
+ }
85
+ @discardableResult func retainBoundItem(_ item: NativeListItem) -> Bool {
86
+ guard self.item?.key == item.key else { return false }
87
+ self.item = item
88
+ return true
89
+ }
90
+
91
+ override func bind(
92
+ item: NativeListItem, theme: [String: Any]?, layout: String, itemIndex: Int? = nil,
93
+ selected: Bool, checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
94
+ ) {
95
+ let update = update(from: self.item, to: item)
96
+ let inputs = (
97
+ theme: theme.map { $0 as NSDictionary }, layout: layout,
98
+ listStyle: listStyle.map { $0 as NSDictionary },
99
+ direction: effectiveUserInterfaceLayoutDirection
100
+ )
101
+ let inputsChanged =
102
+ boundInputs.map {
103
+ // A missing theme/listStyle is the empty object, as in the former `?? [:]` JSON digest.
104
+ $0.layout != inputs.layout || $0.direction != inputs.direction
105
+ || !nativeListValuesEqual($0.theme ?? [:], inputs.theme ?? [:])
106
+ || !nativeListValuesEqual($0.listStyle ?? [:], inputs.listStyle ?? [:])
107
+ } ?? true
108
+ if update != .unchanged || inputsChanged {
109
+ if self.item != nil { onBindingInvalidated?(self, bindingEpoch) }
110
+ bindingEpoch &+= 1
111
+ if update == .replace {
112
+ recycleContent()
113
+ isHighlighted = false
114
+ }
115
+ contentPosition?.isActive = false
116
+ contentPosition = nil
117
+ NSLayoutConstraint.activate(Array(rootConstraints.suffix(2)))
118
+ bindContent(item, theme: theme, layout: layout, checkboxState: checkboxState)
119
+ rootConstraints[0].constant = contentInsets.left
120
+ rootConstraints[1].constant = -contentInsets.right
121
+ rootConstraints[2].constant = contentInsets.top
122
+ rootConstraints[3].constant = -contentInsets.bottom
123
+ if root.axis == .vertical,
124
+ let alignment = item.data.dictionary("style")?.dictionary("container")?[
125
+ "contentVerticalAlignment"] as? String
126
+ ?? (item.styledHeight != nil || usesIntrinsicContentHeight
127
+ ? (defaultVerticalAlignment ?? "top") : nil)
128
+ {
129
+ NSLayoutConstraint.deactivate(Array(rootConstraints.suffix(2)))
130
+ contentPosition =
131
+ alignment == "bottom"
132
+ ? root.bottomAnchor.constraint(
133
+ equalTo: contentView.bottomAnchor, constant: -contentInsets.bottom)
134
+ : alignment == "center"
135
+ ? root.centerYAnchor.constraint(
136
+ equalTo: contentView.centerYAnchor,
137
+ constant: (contentInsets.top - contentInsets.bottom) / 2)
138
+ : root.topAnchor.constraint(equalTo: contentView.topAnchor, constant: contentInsets.top)
139
+ contentPosition?.isActive = true
140
+ }
141
+ boundInputs = inputs
142
+ }
143
+ self.item = item
144
+ self.theme = theme
145
+ self.layout = layout
146
+ self.itemIndex = itemIndex
147
+ self.selectedState = selected
148
+ bindSelectionContent(item, checkboxState: checkboxState)
149
+ accessibilityLabel = accessibilityText(item)
150
+ accessibilityIdentifier = item.data["testID"] as? String
151
+ isUserInteractionEnabled = !item.data.bool("disabled")
152
+ if !isUserInteractionEnabled { isHighlighted = false }
153
+ applyAppearance()
154
+ setNeedsLayout()
155
+ }
156
+
157
+ override func updateSelection(
158
+ item: NativeListItem, selected: Bool,
159
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
160
+ ) {
161
+ guard self.item?.key == item.key else { return }
162
+ self.item = item
163
+ self.selectedState = selected
164
+ bindSelectionContent(item, checkboxState: checkboxState)
165
+ applyAppearance()
166
+ }
167
+ override var isHighlighted: Bool { didSet { applyAppearance() } }
168
+
169
+ func emitAction(
170
+ _ key: String, from view: UIView, source: String, slot: Int? = nil,
171
+ target: NativeSelectionTarget? = nil, anchorInset: CGFloat = 0
172
+ ) {
173
+ guard let item, !key.isEmpty, !item.data.bool("disabled") else { return }
174
+ onAction?(
175
+ item, key, target,
176
+ NativeListActionOrigin(
177
+ sourceView: view, ownerCell: self,
178
+ bindingEpoch: bindingEpoch, source: source, slot: slot, anchorInset: anchorInset))
179
+ }
180
+
181
+ func applyAppearance() {
182
+ guard let item else { return }
183
+ let container = item.data.dictionary("style")?.dictionary("container") ?? [:]
184
+ let showSelection =
185
+ showsSelection && selectedState && (layout != "sectioned" || item.data.bool("selected"))
186
+ var background =
187
+ showSelection
188
+ ? nativeListColor(theme, "rowSelectedBackground", "#F0F0F0")
189
+ : unselectedBackground(item, theme: theme, layout: layout, itemIndex: itemIndex)
190
+ let rowFill =
191
+ container["backgroundColor"]
192
+ ?? (honorsRowBackgroundColor ? item.data["backgroundColor"] : nil)
193
+ if let color = rowFill as? String {
194
+ background = UIColor(nativeListHex: color, fallback: background)
195
+ }
196
+ contentView.backgroundColor =
197
+ isHighlighted && pressChangesBackground
198
+ ? pressedBackground(theme) : background
199
+ contentView.alpha =
200
+ CGFloat(container.double("opacity", default: item.data.double("opacity", default: 1)))
201
+ * (item.data.bool("disabled") ? 0.5 : 1)
202
+ let position = item.data.string("groupPosition")
203
+ let grouped = ["first", "last", "single"].contains(position)
204
+ let pressedRadius =
205
+ isHighlighted && isUserInteractionEnabled ? pressedCornerRadius.map(Double.init) : nil
206
+ let fallbackRadius =
207
+ defaultCornerRadius > 0
208
+ ? Double(defaultCornerRadius)
209
+ : grouped
210
+ ? listStyle?.double("groupCornerRadius", default: Double(groupCornerRadius))
211
+ ?? Double(groupCornerRadius) : 0
212
+ let radius = CGFloat(
213
+ container.double(
214
+ "cornerRadius", default: pressedRadius ?? fallbackRadius))
215
+ let top: CACornerMask = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
216
+ let bottom: CACornerMask = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
217
+ layer.maskedCorners =
218
+ container["cornerRadius"] != nil || position == "single" || defaultCornerRadius > 0
219
+ || pressedRadius != nil
220
+ ? top.union(bottom) : position == "first" ? top : position == "last" ? bottom : []
221
+ layer.cornerCurve = defaultCornerCurve
222
+ contentView.layer.cornerCurve = defaultCornerCurve
223
+ layer.cornerRadius = radius
224
+ layer.masksToBounds = radius > 0
225
+ contentView.layer.cornerRadius = radius
226
+ contentView.layer.maskedCorners = layer.maskedCorners
227
+ contentView.layer.borderWidth = CGFloat(
228
+ container.double("borderWidth", default: Double(defaultBorderWidth)))
229
+ contentView.layer.borderColor =
230
+ UIColor(
231
+ nativeListHex: container.string("borderColor"), fallback: defaultBorderColor(theme)
232
+ ).cgColor
233
+ if item.data.bool("backgroundFullWidth"),
234
+ let fill = rowFill as? String
235
+ {
236
+ fullWidthBackground.backgroundColor = UIColor(nativeListHex: fill, fallback: .clear).cgColor
237
+ contentView.layer.insertSublayer(fullWidthBackground, at: 0)
238
+ clipsToBounds = false
239
+ } else {
240
+ fullWidthBackground.removeFromSuperlayer()
241
+ }
242
+ separator.isHidden = !item.data.bool("separator")
243
+ separator.backgroundColor = UIColor(
244
+ nativeListHex: listStyle?.dictionary("separator")?.string(
245
+ "color", default: theme?.string("separator", default: "#E0E0E0") ?? "#E0E0E0") ?? theme?
246
+ .string("separator", default: "#E0E0E0") ?? "#E0E0E0", fallback: .lightGray)
247
+ }
248
+ override func layoutSubviews() {
249
+ super.layoutSubviews()
250
+ fullWidthBackground.frame = CGRect(
251
+ x: -frame.minX, y: 0, width: superview?.bounds.width ?? bounds.width, height: bounds.height)
252
+ let inset = CGFloat(
253
+ listStyle?.dictionary("separator")?.double("inset", default: Double(defaultSeparatorInset))
254
+ ?? Double(defaultSeparatorInset))
255
+ separator.frame = CGRect(
256
+ x: effectiveUserInterfaceLayoutDirection == .rightToLeft ? 0 : inset,
257
+ y: contentView.bounds.height - 1 / UIScreen.main.scale,
258
+ width: max(0, contentView.bounds.width - inset), height: 1 / UIScreen.main.scale)
259
+ }
260
+ override func prepareForReuse() {
261
+ super.prepareForReuse()
262
+ if item != nil { onBindingInvalidated?(self, bindingEpoch) }
263
+ bindingEpoch &+= 1
264
+ item = nil
265
+ boundInputs = nil
266
+ recycleContent()
267
+ fullWidthBackground.removeFromSuperlayer()
268
+ isHighlighted = false
269
+ }
270
+ }
271
+
272
+ /// Type-aware structural equality for bridged JSON values, matching the former
273
+ /// sorted-keys JSON digest without serializing: `true` != `1` (CFBoolean vs NSNumber),
274
+ /// numbers compare by value (`1` == `1.0`), strings by UTF-16 code units, and inside objects
275
+ /// an explicit `null` differs from a missing key. At the top level a missing value equals
276
+ /// `NSNull` (the digest used `value ?? NSNull()`).
277
+ func nativeListValuesEqual(_ lhs: Any?, _ rhs: Any?) -> Bool {
278
+ switch (lhs.flatMap { $0 is NSNull ? nil : $0 }, rhs.flatMap { $0 is NSNull ? nil : $0 }) {
279
+ case (nil, nil): return true
280
+ case let (lhs?, rhs?): return nativeListJSONEqual(lhs as AnyObject, rhs as AnyObject)
281
+ default: return false
282
+ }
283
+ }
284
+
285
+ private func nativeListJSONEqual(_ lhs: AnyObject, _ rhs: AnyObject) -> Bool {
286
+ if lhs === rhs { return true }
287
+ if let lhs = lhs as? NSNumber, let rhs = rhs as? NSNumber {
288
+ let boolean = CFBooleanGetTypeID()
289
+ return (CFGetTypeID(lhs) == boolean) == (CFGetTypeID(rhs) == boolean) && lhs.isEqual(to: rhs)
290
+ }
291
+ if let lhs = lhs as? NSString, let rhs = rhs as? NSString { return lhs.isEqual(to: rhs as String) }
292
+ if let lhs = lhs as? NSDictionary, let rhs = rhs as? NSDictionary {
293
+ guard lhs.count == rhs.count else { return false }
294
+ for (key, value) in lhs {
295
+ guard let other = rhs.object(forKey: key),
296
+ nativeListJSONEqual(value as AnyObject, other as AnyObject)
297
+ else { return false }
298
+ }
299
+ return true
300
+ }
301
+ if let lhs = lhs as? NSArray, let rhs = rhs as? NSArray {
302
+ guard lhs.count == rhs.count else { return false }
303
+ for index in 0..<lhs.count
304
+ where !nativeListJSONEqual(lhs[index] as AnyObject, rhs[index] as AnyObject) {
305
+ return false
306
+ }
307
+ return true
308
+ }
309
+ if lhs is NSNull || rhs is NSNull { return lhs is NSNull && rhs is NSNull }
310
+ return lhs.isEqual(rhs)
311
+ }
@@ -0,0 +1,50 @@
1
+ import UIKit
2
+
3
+ // Closed internal registry; no public plugin or bridge surface.
4
+ enum NativeListRendererRegistry {
5
+ private static let hosts:
6
+ [NativeListRendererKey: (type: NativeListRowHost.Type, create: () -> NativeListRowHost)] = [
7
+ .sectionHeader: (
8
+ NativeListSectionHeaderCell.self, { NativeListSectionHeaderCell(frame: .zero) }
9
+ ),
10
+ .walletGroup: (NativeListWalletGroupCell.self, { NativeListWalletGroupCell(frame: .zero) }),
11
+ .identity: (NativeListIdentityCell.self, { NativeListIdentityCell(frame: .zero) }),
12
+ .market: (NativeListMarketCell.self, { NativeListMarketCell(frame: .zero) }),
13
+ .message: (NativeListMessageCell.self, { NativeListMessageCell(frame: .zero) }),
14
+ .rail: (NativeListRailCell.self, { NativeListRailCell(frame: .zero) }),
15
+ .mediaTile: (NativeListMediaTileCell.self, { NativeListMediaTileCell(frame: .zero) }),
16
+ .metricCard: (NativeListMetricCardCell.self, { NativeListMetricCardCell(frame: .zero) }),
17
+ .dataRow: (NativeListDataRowCell.self, { NativeListDataRowCell(frame: .zero) }),
18
+ .activity: (NativeListActivityCell.self, { NativeListActivityCell(frame: .zero) }),
19
+ .system: (NativeListSystemCell.self, { NativeListSystemCell(frame: .zero) }),
20
+ .action: (NativeListActionCell.self, { NativeListActionCell(frame: .zero) }),
21
+ .unsupported: (
22
+ NativeListUnsupportedCell.self, { NativeListUnsupportedCell(frame: .zero) }
23
+ ),
24
+ ]
25
+ static func create(_ key: NativeListRendererKey) -> NativeListRowHost { hosts[key]!.create() }
26
+ static func reuseIdentifier(for key: NativeListRendererKey) -> String {
27
+ "NativeList.\(key.rawValue)"
28
+ }
29
+ static func register(in collectionView: UICollectionView) {
30
+ for (key, host) in hosts {
31
+ collectionView.register(host.type, forCellWithReuseIdentifier: reuseIdentifier(for: key))
32
+ }
33
+ }
34
+ static func appliesSizePreset(_ item: NativeListItem) -> Bool {
35
+ hosts[item.rendererKey]!.type.appliesSizePreset(item)
36
+ }
37
+ static func measure(_ item: NativeListItem, width: CGFloat, theme: [String: Any]?, layout: String)
38
+ -> CGFloat?
39
+ {
40
+ hosts[item.rendererKey]!.type.measure(item, width: width, theme: theme, layout: layout)
41
+ }
42
+ }
43
+
44
+ /// Legacy fallback for unknown row types: shared chrome only, 56-point default height
45
+ /// (plus the size preset), no template content.
46
+ final class NativeListUnsupportedCell: NativeListRendererCell {
47
+ override class func measure(
48
+ _ item: NativeListItem, width: CGFloat, theme: [String: Any]?, layout: String
49
+ ) -> CGFloat? { 56 }
50
+ }
@@ -0,0 +1,122 @@
1
+ import UIKit
2
+
3
+ struct NativeListResolvedText {
4
+ let text: String
5
+ let font: UIFont
6
+ let color: UIColor
7
+ let lineHeight: CGFloat
8
+ let lines: Int
9
+ /// Semantic `start | center | end`; nil keeps the legacy `.natural` default.
10
+ let alignmentName: String?
11
+ let breakMode: NSLineBreakMode
12
+ let verticalAlignment: String?
13
+ let offsetY: CGFloat
14
+ let explicitLineHeight: Bool
15
+ let explicitTruncation: Bool
16
+ let tabular: Bool
17
+ /// Keep the label's break mode in the attributed paragraph (legacy plain-text labels kept
18
+ /// their tail ellipsis; attributed paragraphs otherwise default to word wrapping).
19
+ let preservesBreakMode: Bool
20
+
21
+ init(
22
+ _ value: String, style: [String: Any]?, size: CGFloat, weight: NativeListFontWeight = .regular,
23
+ color: UIColor, lineHeight: CGFloat, lines: Int, breakMode: NSLineBreakMode = .byTruncatingTail,
24
+ tabular: Bool = false, preservesBreakMode: Bool = false
25
+ ) {
26
+ let style = style ?? [:]
27
+ self.tabular = tabular
28
+ self.preservesBreakMode = preservesBreakMode
29
+ self.lines = min(3, max(1, style.int("lines", default: lines)))
30
+ explicitTruncation = style["lines"] != nil || style["truncate"] != nil
31
+ text =
32
+ self.lines == 1 && explicitTruncation
33
+ ? value.replacingOccurrences(of: "\r\n|[\r\n]", with: " ", options: .regularExpression)
34
+ : value
35
+ let resolvedWeight: NativeListFontWeight
36
+ switch style.string("fontWeight") {
37
+ case "regular": resolvedWeight = .regular
38
+ case "medium": resolvedWeight = .medium
39
+ case "semibold": resolvedWeight = .semibold
40
+ case "bold": resolvedWeight = .bold
41
+ default: resolvedWeight = weight
42
+ }
43
+ let resolvedSize = CGFloat(style.double("fontSize", default: Double(size)))
44
+ font =
45
+ tabular
46
+ ? nativeListTabularFont(ofSize: resolvedSize, weight: resolvedWeight)
47
+ : nativeListFont(ofSize: resolvedSize, weight: resolvedWeight)
48
+ self.color =
49
+ (style["color"] as? String).map { UIColor(nativeListHex: $0, fallback: color) } ?? color
50
+ self.lineHeight = CGFloat(style.double("lineHeight", default: Double(lineHeight)))
51
+ explicitLineHeight = style["lineHeight"] != nil
52
+ alignmentName = style["alignment"] as? String
53
+ self.breakMode =
54
+ explicitTruncation
55
+ ? (style.string("truncate", default: "tail") == "clip"
56
+ ? (self.lines == 1 ? .byClipping : .byWordWrapping) : .byTruncatingTail) : breakMode
57
+ verticalAlignment = style["verticalAlignment"] as? String
58
+ offsetY = CGFloat(style.double("offsetY"))
59
+ }
60
+
61
+ /// Start/end follow the host view's layout direction, as legacy `marketTextAlignment` did.
62
+ func alignment(for direction: UIUserInterfaceLayoutDirection) -> NSTextAlignment {
63
+ guard let alignmentName else { return .natural }
64
+ return NativeListTextStyles.marketTextAlignment(alignmentName, direction: direction)
65
+ }
66
+
67
+ func attributed(direction: UIUserInterfaceLayoutDirection) -> NSAttributedString {
68
+ let paragraph = NSMutableParagraphStyle()
69
+ paragraph.minimumLineHeight = lineHeight
70
+ paragraph.maximumLineHeight = lineHeight
71
+ paragraph.alignment = alignment(for: direction)
72
+ if explicitTruncation || preservesBreakMode { paragraph.lineBreakMode = breakMode }
73
+ var attributes: [NSAttributedString.Key: Any] = [
74
+ .font: font, .foregroundColor: color, .paragraphStyle: paragraph,
75
+ ]
76
+ if explicitLineHeight {
77
+ attributes[.baselineOffset] = max(0, (lineHeight - font.lineHeight) / 2)
78
+ }
79
+ return NSAttributedString(string: text, attributes: attributes)
80
+ }
81
+
82
+ /// Resolves start/end once from the label's current direction; the list rebinds bound rows
83
+ /// when its effective layout direction changes (NativeListRendererCell bound inputs).
84
+ func bind(_ label: NativeListTextLabel) {
85
+ let direction = label.effectiveUserInterfaceLayoutDirection
86
+ label.font = font
87
+ label.textColor = color
88
+ label.numberOfLines = lines
89
+ label.textAlignment = alignment(for: direction)
90
+ label.lineBreakMode = breakMode
91
+ label.rowVerticalAlignment = verticalAlignment
92
+ label.rowOffsetY = offsetY
93
+ label.attributedText = attributed(direction: direction)
94
+ label.isHidden = text.isEmpty
95
+ }
96
+
97
+ // Measurement runs for every row on every snapshot. It reuses one sizing label (the same
98
+ // UILabel metrics, line cap and truncation as the bound view) and memoizes by the inputs
99
+ // that affect height; alignment and color do not.
100
+ private static let sizingLabel = NativeListTextLabel()
101
+ private static let heightCache: NSCache<NSString, NSNumber> = {
102
+ let cache = NSCache<NSString, NSNumber>()
103
+ cache.countLimit = 4096
104
+ return cache
105
+ }()
106
+
107
+ func measure(width: CGFloat) -> CGFloat {
108
+ guard !text.isEmpty else { return 0 }
109
+ let width = max(1, width)
110
+ let key =
111
+ "\(width)|\(lines)|\(lineHeight)|\(explicitLineHeight)|\(explicitTruncation)|\(breakMode.rawValue)|\(preservesBreakMode)|\(font.fontName)|\(font.pointSize)|\(tabular)|\(text)"
112
+ as NSString
113
+ if let cached = Self.heightCache.object(forKey: key) { return CGFloat(cached.doubleValue) }
114
+ let label = Thread.isMainThread ? Self.sizingLabel : NativeListTextLabel()
115
+ bind(label)
116
+ let height = ceil(
117
+ label.sizeThatFits(CGSize(width: width, height: .greatestFiniteMagnitude)).height)
118
+ if label === Self.sizingLabel { label.attributedText = nil }
119
+ Self.heightCache.setObject(NSNumber(value: Double(height)), forKey: key)
120
+ return height
121
+ }
122
+ }
@@ -0,0 +1,63 @@
1
+ import UIKit
2
+
3
+ final class NativeListActionOrigin {
4
+ weak var sourceView: UIView?
5
+ weak var ownerCell: NativeListRowHost?
6
+ let bindingEpoch: Int
7
+ let source: String
8
+ let slot: Int?
9
+ // OneKey patch: expose the layout slot while preserving the larger hit target.
10
+ let anchorInset: CGFloat
11
+ var windowPoint: CGPoint?
12
+
13
+ init(
14
+ sourceView: UIView,
15
+ ownerCell: NativeListRowHost,
16
+ bindingEpoch: Int,
17
+ source: String,
18
+ slot: Int? = nil,
19
+ anchorInset: CGFloat = 0
20
+ ) {
21
+ self.sourceView = sourceView
22
+ self.ownerCell = ownerCell
23
+ self.bindingEpoch = bindingEpoch
24
+ self.source = source
25
+ self.slot = slot
26
+ self.anchorInset = anchorInset
27
+ }
28
+ }
29
+
30
+ // Common list-facing lifecycle; each renderer owns its view allocation and reset.
31
+ class NativeListRowHost: UICollectionViewCell {
32
+ class func measure(_ item: NativeListItem, width: CGFloat, theme: [String: Any]?, layout: String)
33
+ -> CGFloat?
34
+ { nil }
35
+ class func appliesSizePreset(_ item: NativeListItem) -> Bool { true }
36
+
37
+ var bindingEpoch = 0
38
+ var listStyle: [String: Any]?
39
+ var onAction: ((NativeListItem, String, NativeSelectionTarget?, NativeListActionOrigin?) -> Void)?
40
+ var onBindingInvalidated: ((NativeListRowHost, Int) -> Void)?
41
+
42
+ func bind(
43
+ item: NativeListItem, theme: [String: Any]?, layout: String, itemIndex: Int? = nil,
44
+ selected: Bool, checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
45
+ ) {
46
+ preconditionFailure("A row host must implement binding")
47
+ }
48
+ func updateSelection(
49
+ item: NativeListItem, selected: Bool,
50
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
51
+ ) {}
52
+ func updateMarketQuote(_ item: NativeListItem, theme: [String: Any]?) {}
53
+ func setPressed(_ pressed: Bool) { isHighlighted = pressed && isUserInteractionEnabled }
54
+ func canStartWalletGroupReorder(at point: CGPoint) -> Bool { true }
55
+ func setWalletGroupReorderCompact(_ compact: Bool) {}
56
+ func prepareWalletGroupReorderExpansion() {}
57
+ func animateWalletGroupReorderExpansion() {}
58
+ func finishWalletGroupReorderExpansion() {}
59
+ func rowActionOrigin() -> NativeListActionOrigin {
60
+ NativeListActionOrigin(
61
+ sourceView: contentView, ownerCell: self, bindingEpoch: bindingEpoch, source: "row")
62
+ }
63
+ }