@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,236 @@
1
+ import CoreText
2
+ import UIKit
3
+
4
+ // Apply semantic overrides after a renderer has restored its resolved defaults.
5
+ enum NativeListTextStyles {
6
+ static func applyStyledText(_ label: UILabel, _ style: [String: Any]) {
7
+ let text = label.attributedText?.string ?? label.text ?? ""
8
+ guard !text.isEmpty, !style.isEmpty else { return }
9
+ let originalText = label.attributedText
10
+ let textLabel = label as? NativeListTextLabel
11
+ let baseFont =
12
+ (originalText?.attribute(.font, at: 0, effectiveRange: nil) as? UIFont) ?? label.font
13
+ ?? nativeListFont(ofSize: 14)
14
+ let size = CGFloat(style.double("fontSize", default: Double(baseFont.pointSize)))
15
+ let font: UIFont
16
+ if let weightName = style["fontWeight"] as? String {
17
+ font = nativeListFont(
18
+ ofSize: size,
19
+ weight: marketFontWeight(weightName, fallback: .regular)
20
+ )
21
+ } else {
22
+ font = baseFont.withSize(size)
23
+ }
24
+ let color =
25
+ (style["color"] as? String)
26
+ .map { UIColor(nativeListHex: $0, fallback: label.textColor ?? .black) }
27
+ ?? label.textColor ?? .black
28
+ if style["lines"] != nil {
29
+ label.numberOfLines = min(3, max(1, style.int("lines", default: 1)))
30
+ label.lineBreakMode = .byTruncatingTail
31
+ }
32
+ if let alignmentName = style["alignment"] as? String {
33
+ label.textAlignment = marketTextAlignment(
34
+ alignmentName, direction: label.effectiveUserInterfaceLayoutDirection)
35
+ }
36
+ if style["truncate"] != nil || style["lines"] != nil {
37
+ label.lineBreakMode = styledLineBreakMode(style, lines: label.numberOfLines)
38
+ }
39
+ if let alignment = style["verticalAlignment"] as? String {
40
+ textLabel?.rowVerticalAlignment = alignment
41
+ }
42
+ if style["offsetY"] != nil { textLabel?.rowOffsetY = CGFloat(style.double("offsetY")) }
43
+ label.font = font
44
+ label.textColor = color
45
+ let attributed =
46
+ originalText.map { NSMutableAttributedString(attributedString: $0) }
47
+ ?? NSMutableAttributedString(string: text)
48
+ let paragraph =
49
+ (attributed.attribute(.paragraphStyle, at: 0, effectiveRange: nil)
50
+ as? NSParagraphStyle)?.mutableCopy() as? NSMutableParagraphStyle ?? NSMutableParagraphStyle()
51
+ if style["alignment"] != nil { paragraph.alignment = label.textAlignment }
52
+ if style["lines"] != nil || style["truncate"] != nil {
53
+ paragraph.lineBreakMode = label.lineBreakMode
54
+ }
55
+ var attributes: [NSAttributedString.Key: Any] = [:]
56
+ applyStyledFont(attributed, style, fallback: baseFont)
57
+ if style["color"] != nil { attributes[.foregroundColor] = color }
58
+ if style["alignment"] != nil || style["lines"] != nil || style["truncate"] != nil
59
+ || style["lineHeight"] != nil
60
+ {
61
+ attributes[.paragraphStyle] = paragraph
62
+ }
63
+ if style["lineHeight"] != nil {
64
+ let box = CGFloat(style.double("lineHeight"))
65
+ paragraph.minimumLineHeight = box
66
+ paragraph.maximumLineHeight = box
67
+ // React Native centers font metrics inside an explicit line height.
68
+ attributes[.baselineOffset] = max(0, (box - font.lineHeight) / 2)
69
+ }
70
+ attributed.addAttributes(attributes, range: NSRange(location: 0, length: attributed.length))
71
+ if label.numberOfLines == 1 && (style["lines"] != nil || style["truncate"] != nil) {
72
+ normalizeSingleLine(attributed)
73
+ }
74
+ label.attributedText = attributed
75
+ }
76
+
77
+ static func applyStyledButton(_ button: UIButton, _ style: [String: Any]) {
78
+ let text =
79
+ button.attributedTitle(for: .normal)?.string
80
+ ?? button.title(for: .normal)
81
+ ?? ""
82
+ guard !text.isEmpty, !style.isEmpty else { return }
83
+ let originalText = button.attributedTitle(for: .normal)
84
+ let accessory = button as? NativeListAccessoryButton
85
+ let baseFont =
86
+ (originalText?.attribute(.font, at: 0, effectiveRange: nil) as? UIFont) ?? button.titleLabel?
87
+ .font ?? nativeListFont(ofSize: 14)
88
+ let size = CGFloat(style.double("fontSize", default: Double(baseFont.pointSize)))
89
+ let font: UIFont
90
+ if let weightName = style["fontWeight"] as? String {
91
+ font = nativeListFont(
92
+ ofSize: size,
93
+ weight: marketFontWeight(weightName, fallback: .regular)
94
+ )
95
+ } else {
96
+ font = baseFont.withSize(size)
97
+ }
98
+ let fallbackColor = button.titleColor(for: .normal) ?? .black
99
+ let color =
100
+ (style["color"] as? String)
101
+ .map { UIColor(nativeListHex: $0, fallback: fallbackColor) }
102
+ ?? fallbackColor
103
+ let attributed =
104
+ button.attributedTitle(for: .normal)
105
+ .map { NSMutableAttributedString(attributedString: $0) }
106
+ ?? NSMutableAttributedString(string: text)
107
+ let paragraph =
108
+ (attributed.attribute(.paragraphStyle, at: 0, effectiveRange: nil)
109
+ as? NSParagraphStyle)?.mutableCopy() as? NSMutableParagraphStyle ?? NSMutableParagraphStyle()
110
+ if let alignmentName = style["alignment"] as? String {
111
+ paragraph.alignment = marketTextAlignment(
112
+ alignmentName, direction: button.effectiveUserInterfaceLayoutDirection)
113
+ button.contentHorizontalAlignment =
114
+ alignmentName == "start"
115
+ ? .leading
116
+ : alignmentName == "end" ? .trailing : .center
117
+ }
118
+ var attributes: [NSAttributedString.Key: Any] = [:]
119
+ applyStyledFont(attributed, style, fallback: baseFont)
120
+ if style["color"] != nil { attributes[.foregroundColor] = color }
121
+ if style["alignment"] != nil || style["lineHeight"] != nil || style["lines"] != nil
122
+ || style["truncate"] != nil
123
+ {
124
+ attributes[.paragraphStyle] = paragraph
125
+ }
126
+ if style["lineHeight"] != nil {
127
+ let box = CGFloat(style.double("lineHeight"))
128
+ paragraph.minimumLineHeight = box
129
+ paragraph.maximumLineHeight = box
130
+ attributes[.baselineOffset] = max(0, (box - font.lineHeight) / 2)
131
+ if accessory?.marketLineHeight != nil { accessory?.marketLineHeight = box }
132
+ if accessory?.selectorSummaryLineHeight != nil { accessory?.selectorSummaryLineHeight = box }
133
+ }
134
+ if style["lines"] != nil {
135
+ button.titleLabel?.numberOfLines = min(3, max(1, style.int("lines", default: 1)))
136
+ }
137
+ if style["lines"] != nil || style["truncate"] != nil {
138
+ let mode = styledLineBreakMode(style, lines: button.titleLabel?.numberOfLines ?? 1)
139
+ button.titleLabel?.lineBreakMode = mode
140
+ paragraph.lineBreakMode = mode
141
+ if button.titleLabel?.numberOfLines == 1 { normalizeSingleLine(attributed) }
142
+ }
143
+ if let alignment = style["verticalAlignment"] as? String {
144
+ button.contentVerticalAlignment =
145
+ alignment == "top" ? .top : alignment == "bottom" ? .bottom : .center
146
+ }
147
+ if style["offsetY"] != nil {
148
+ (button as? NativeListAccessoryButton)?.rowTextOffsetY = CGFloat(style.double("offsetY"))
149
+ }
150
+ button.titleLabel?.font = font
151
+ attributed.addAttributes(attributes, range: NSRange(location: 0, length: attributed.length))
152
+ button.setAttributedTitle(attributed, for: .normal)
153
+ }
154
+
155
+ static func applyStyledFont(
156
+ _ text: NSMutableAttributedString, _ style: [String: Any], fallback: UIFont
157
+ ) {
158
+ guard style["fontSize"] != nil || style["fontWeight"] != nil else { return }
159
+ text.enumerateAttribute(.font, in: NSRange(location: 0, length: text.length)) {
160
+ value, range, _ in
161
+ let original = (value as? UIFont) ?? fallback
162
+ let size = CGFloat(style.double("fontSize", default: Double(original.pointSize)))
163
+ let font =
164
+ style["fontWeight"] == nil
165
+ ? original.withSize(size)
166
+ : nativeListFont(
167
+ ofSize: size, weight: marketFontWeight(style.string("fontWeight"), fallback: .regular))
168
+ text.addAttribute(.font, value: font, range: range)
169
+ }
170
+ }
171
+
172
+ static func normalizeSingleLine(_ text: NSMutableAttributedString) {
173
+ let pattern = try! NSRegularExpression(pattern: "\\r\\n|[\\r\\n]")
174
+ for match in pattern.matches(in: text.string, range: NSRange(location: 0, length: text.length))
175
+ .reversed()
176
+ {
177
+ text.replaceCharacters(in: match.range, with: " ")
178
+ }
179
+ }
180
+
181
+ static func styledLineBreakMode(_ style: [String: Any], lines: Int) -> NSLineBreakMode {
182
+ if style.string("truncate", default: "tail") != "clip" { return .byTruncatingTail }
183
+ // Clipping disables wrapping; multi-line clip must wrap without adding an ellipsis.
184
+ return lines == 1 ? .byClipping : .byWordWrapping
185
+ }
186
+
187
+ static func marketFontWeight(_ value: String, fallback: NativeListFontWeight)
188
+ -> NativeListFontWeight
189
+ {
190
+ switch value {
191
+ case "regular": return .regular
192
+ case "semibold": return .semibold
193
+ case "bold": return .bold
194
+ case "medium": return .medium
195
+ default: return fallback
196
+ }
197
+ }
198
+
199
+ /// Legacy selector/warning typography: monospaced digits and zero kerning on the label.
200
+ static func applyTabularDigits(_ label: UILabel) {
201
+ guard let font = label.font else { return }
202
+ label.font = tabularDigitsFont(font)
203
+ guard let original = label.attributedText else { return }
204
+ let result = NSMutableAttributedString(attributedString: original)
205
+ let range = NSRange(location: 0, length: result.length)
206
+ result.addAttribute(.kern, value: 0, range: range)
207
+ original.enumerateAttribute(.font, in: range) { value, range, _ in
208
+ if let font = value as? UIFont {
209
+ result.addAttribute(.font, value: tabularDigitsFont(font), range: range)
210
+ }
211
+ }
212
+ label.attributedText = result
213
+ }
214
+
215
+ static func tabularDigitsFont(_ font: UIFont) -> UIFont {
216
+ var settings =
217
+ font.fontDescriptor.fontAttributes[.featureSettings] as? [[UIFontDescriptor.FeatureKey: Int]]
218
+ ?? []
219
+ settings.removeAll { $0[.type] == kNumberSpacingType }
220
+ settings.append([.type: kNumberSpacingType, .selector: kMonospacedNumbersSelector])
221
+ return UIFont(
222
+ descriptor: font.fontDescriptor.addingAttributes([.featureSettings: settings]),
223
+ size: font.pointSize)
224
+ }
225
+
226
+ static func marketTextAlignment(_ value: String, direction: UIUserInterfaceLayoutDirection)
227
+ -> NSTextAlignment
228
+ {
229
+ switch value {
230
+ case "center": return .center
231
+ case "start": return direction == .rightToLeft ? .right : .left
232
+ case "end": return direction == .rightToLeft ? .left : .right
233
+ default: return .natural
234
+ }
235
+ }
236
+ }
@@ -0,0 +1,255 @@
1
+ import UIKit
2
+
3
+ /// Composite only: member content, assets and local styles belong to Identity.
4
+ final class NativeListWalletGroupCell: NativeListRendererCell, UIGestureRecognizerDelegate {
5
+ private struct MemberView {
6
+ let cell: NativeListIdentityCell
7
+ let height: NSLayoutConstraint
8
+ }
9
+ private var members: [NativeListItem] = []
10
+ private var memberViews: [String: MemberView] = [:]
11
+ private var compactCell: NativeListIdentityCell?
12
+ private let compactContainer = UIView()
13
+ private let dragBadge = NativeListInsetLabel()
14
+ private var compact = false
15
+ private lazy var memberTap = UITapGestureRecognizer(
16
+ target: self, action: #selector(memberPressed(_:)))
17
+ override var usesIntrinsicContentHeight: Bool { true }
18
+ override var defaultCornerRadius: CGFloat { 20 }
19
+ override var defaultCornerCurve: CALayerCornerCurve { .continuous }
20
+ override var defaultBorderWidth: CGFloat { 1 }
21
+ override var pressChangesBackground: Bool { false }
22
+ override var showsSelection: Bool { false }
23
+ // Legacy fill is `subduedBackground`; the group's row-level `backgroundColor` is ignored.
24
+ override var honorsRowBackgroundColor: Bool { false }
25
+ override func defaultBorderColor(_ theme: [String: Any]?) -> UIColor {
26
+ nativeListColor(theme, "separator", "#E0E0E0")
27
+ }
28
+ override func unselectedBackground(
29
+ _ item: NativeListItem, theme: [String: Any]?, layout: String, itemIndex: Int?
30
+ ) -> UIColor {
31
+ nativeListColor(theme, "subduedBackground", "#F9F9F9")
32
+ }
33
+ private static func memberItems(_ item: NativeListItem) -> [NativeListItem] {
34
+ ([item.data.dictionary("parent")].compactMap { $0 } + item.data.dictionaries("children"))
35
+ .compactMap { try? NativeListItem(data: $0) }
36
+ }
37
+ private static func memberHeight(_ item: NativeListItem) -> CGFloat {
38
+ item.styledHeight
39
+ ?? (item.data["height"] != nil
40
+ ? CGFloat(item.data.double("height"))
41
+ // Legacy member default is independent of the member's presentation.
42
+ : item.data.dictionaries("badges").isEmpty ? 68 : 92)
43
+ }
44
+ /// Legacy default: a one-point inset (source group border) when the parent carries an
45
+ /// explicit height, otherwise none. `style.*Padding` overrides it; bind and measure share it.
46
+ private static func padding(_ item: NativeListItem, members: [NativeListItem]) -> (
47
+ horizontal: CGFloat, vertical: CGFloat
48
+ ) {
49
+ let style = item.data.dictionary("style") ?? [:]
50
+ let inset: Double = members.first?.data["height"] != nil ? 1 : 0
51
+ return (
52
+ CGFloat(style.double("horizontalPadding", default: inset)),
53
+ CGFloat(style.double("verticalPadding", default: inset))
54
+ )
55
+ }
56
+ override class func appliesSizePreset(_ item: NativeListItem) -> Bool { false }
57
+ override class func measure(
58
+ _ item: NativeListItem, width: CGFloat, theme: [String: Any]?, layout: String
59
+ ) -> CGFloat? {
60
+ let members = memberItems(item)
61
+ return members.reduce(
62
+ CGFloat(max(0, members.count - 1) * 12) + padding(item, members: members).vertical * 2
63
+ ) { $0 + memberHeight($1) }
64
+ }
65
+ override init(frame: CGRect) {
66
+ super.init(frame: frame)
67
+ root.axis = .vertical
68
+ root.alignment = .fill
69
+ root.spacing = 12
70
+ memberTap.delegate = self
71
+ root.addGestureRecognizer(memberTap)
72
+ compactContainer.translatesAutoresizingMaskIntoConstraints = false
73
+ compactContainer.isUserInteractionEnabled = false
74
+ compactContainer.isHidden = true
75
+ contentView.addSubview(compactContainer)
76
+ dragBadge.translatesAutoresizingMaskIntoConstraints = false
77
+ dragBadge.horizontalInset = 6
78
+ dragBadge.textAlignment = .center
79
+ dragBadge.font = nativeListTabularFont(ofSize: 12, weight: .semibold)
80
+ dragBadge.layer.cornerRadius = 12
81
+ dragBadge.layer.masksToBounds = true
82
+ dragBadge.layer.borderWidth = 1
83
+ compactContainer.addSubview(dragBadge)
84
+ NSLayoutConstraint.activate([
85
+ compactContainer.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
86
+ compactContainer.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
87
+ compactContainer.topAnchor.constraint(equalTo: contentView.topAnchor),
88
+ compactContainer.heightAnchor.constraint(equalToConstant: 68),
89
+ dragBadge.trailingAnchor.constraint(equalTo: compactContainer.trailingAnchor, constant: -4),
90
+ dragBadge.bottomAnchor.constraint(equalTo: compactContainer.bottomAnchor, constant: -4),
91
+ dragBadge.heightAnchor.constraint(equalToConstant: 24),
92
+ dragBadge.widthAnchor.constraint(greaterThanOrEqualToConstant: 24),
93
+ ])
94
+ }
95
+ required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
96
+ override func accessibilityText(_ item: NativeListItem) -> String {
97
+ item.data.string(
98
+ "accessibilityLabel", default: item.data.dictionary("parent")?.string("title") ?? "")
99
+ }
100
+ override func bindContent(
101
+ _ item: NativeListItem, theme: [String: Any]?, layout: String,
102
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
103
+ ) {
104
+ members = Self.memberItems(item)
105
+ let keys = Set(members.map(\.key))
106
+ for key in Array(memberViews.keys) where !keys.contains(key) {
107
+ let view = memberViews.removeValue(forKey: key)!
108
+ view.cell.prepareForReuse()
109
+ root.removeArrangedSubview(view.cell)
110
+ view.cell.removeFromSuperview()
111
+ }
112
+ let (horizontal, vertical) = Self.padding(item, members: members)
113
+ contentInsets = UIEdgeInsets(
114
+ top: vertical, left: horizontal, bottom: vertical, right: horizontal)
115
+ for (index, member) in members.enumerated() {
116
+ let view: MemberView
117
+ if let existing = memberViews[member.key] {
118
+ view = existing
119
+ } else {
120
+ let cell = NativeListIdentityCell(frame: .zero)
121
+ cell.translatesAutoresizingMaskIntoConstraints = false
122
+ let height = cell.heightAnchor.constraint(equalToConstant: Self.memberHeight(member))
123
+ height.isActive = true
124
+ view = MemberView(cell: cell, height: height)
125
+ memberViews[member.key] = view
126
+ cell.onAction = { [weak self] item, action, target, origin in
127
+ self?.onAction?(item, action, target, origin)
128
+ }
129
+ cell.onBindingInvalidated = { [weak self] cell, epoch in
130
+ self?.onBindingInvalidated?(cell, epoch)
131
+ }
132
+ }
133
+ view.height.constant = Self.memberHeight(member)
134
+ view.cell.listStyle = listStyle
135
+ view.cell.bind(
136
+ item: member, theme: theme, layout: layout, selected: member.data.bool("selected"),
137
+ checkboxState: checkboxState)
138
+ root.insertArrangedSubview(view.cell, at: index)
139
+ }
140
+ if let parent = members.first {
141
+ if compactCell == nil {
142
+ let cell = NativeListIdentityCell(frame: .zero)
143
+ cell.translatesAutoresizingMaskIntoConstraints = false
144
+ compactContainer.insertSubview(cell, at: 0)
145
+ NSLayoutConstraint.activate([
146
+ cell.leadingAnchor.constraint(equalTo: compactContainer.leadingAnchor),
147
+ cell.trailingAnchor.constraint(equalTo: compactContainer.trailingAnchor),
148
+ cell.topAnchor.constraint(equalTo: compactContainer.topAnchor),
149
+ cell.bottomAnchor.constraint(equalTo: compactContainer.bottomAnchor),
150
+ ])
151
+ compactCell = cell
152
+ }
153
+ compactCell?.listStyle = listStyle
154
+ compactCell?.bind(
155
+ item: parent, theme: theme, layout: layout, selected: parent.data.bool("selected"),
156
+ checkboxState: checkboxState)
157
+ }
158
+ let count = members.dropFirst().filter { ($0.data["draggable"] as? Bool) != false }.count
159
+ dragBadge.text = count > 0 ? "+\(count)" : nil
160
+ dragBadge.isHidden = count == 0
161
+ dragBadge.backgroundColor = nativeListColor(theme, "inverseBackground", "#202020")
162
+ dragBadge.textColor = nativeListColor(theme, "inverseText", "#FCFCFC")
163
+ dragBadge.layer.borderColor = nativeListColor(theme, "rowBackground", "#FFFFFF").cgColor
164
+ }
165
+ override func bindSelectionContent(
166
+ _ item: NativeListItem,
167
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
168
+ ) {
169
+ members = Self.memberItems(item)
170
+ for member in members {
171
+ memberViews[member.key]?.cell.updateSelection(
172
+ item: member, selected: member.data.bool("selected"), checkboxState: checkboxState)
173
+ }
174
+ if let parent = members.first {
175
+ compactCell?.updateSelection(
176
+ item: parent, selected: parent.data.bool("selected"), checkboxState: checkboxState)
177
+ }
178
+ }
179
+ func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch)
180
+ -> Bool
181
+ {
182
+ var view = touch.view
183
+ while let current = view, current !== root {
184
+ if current is UIControl { return false }
185
+ view = current.superview
186
+ }
187
+ return true
188
+ }
189
+ @objc private func memberPressed(_ gesture: UITapGestureRecognizer) {
190
+ let point = gesture.location(in: root)
191
+ for member in members {
192
+ guard let cell = memberViews[member.key]?.cell, cell.frame.contains(point) else { continue }
193
+ if !member.data.bool("disabled") && !member.data.bool("pressDisabled") {
194
+ onAction?(member, "press", nil, cell.rowActionOrigin())
195
+ }
196
+ return
197
+ }
198
+ }
199
+ override func canStartWalletGroupReorder(at point: CGPoint) -> Bool {
200
+ let point = root.convert(point, from: self)
201
+ for member in members where memberViews[member.key]?.cell.frame.contains(point) == true {
202
+ // Legacy: only `draggable: false` excludes a member; disabled members may start a drag.
203
+ return (member.data["draggable"] as? Bool) != false
204
+ }
205
+ return true
206
+ }
207
+ override func applyAppearance() {
208
+ super.applyAppearance()
209
+ if compact {
210
+ contentView.backgroundColor = .clear
211
+ contentView.layer.borderWidth = 0
212
+ }
213
+ }
214
+ override func setPressed(_ pressed: Bool) {
215
+ if compact { compactCell?.setPressed(pressed) } else { super.setPressed(pressed) }
216
+ }
217
+ override func setWalletGroupReorderCompact(_ compact: Bool) {
218
+ self.compact = compact
219
+ root.isHidden = compact
220
+ root.alpha = compact ? 0 : 1
221
+ compactContainer.isHidden = !compact
222
+ compactContainer.alpha = compact ? 1 : 0
223
+ applyAppearance()
224
+ }
225
+ override func prepareWalletGroupReorderExpansion() {
226
+ compact = false
227
+ root.isHidden = false
228
+ root.alpha = 0
229
+ compactContainer.isHidden = false
230
+ compactContainer.alpha = 1
231
+ applyAppearance()
232
+ }
233
+ override func animateWalletGroupReorderExpansion() {
234
+ root.alpha = 1
235
+ compactContainer.alpha = 0
236
+ }
237
+ override func finishWalletGroupReorderExpansion() { setWalletGroupReorderCompact(false) }
238
+ override func recycleContent() {
239
+ for view in memberViews.values {
240
+ view.cell.prepareForReuse()
241
+ view.cell.removeFromSuperview()
242
+ }
243
+ memberViews.removeAll()
244
+ members.removeAll()
245
+ compactCell?.prepareForReuse()
246
+ compactCell?.removeFromSuperview()
247
+ compactCell = nil
248
+ compact = false
249
+ root.isHidden = false
250
+ root.alpha = 1
251
+ compactContainer.isHidden = true
252
+ compactContainer.alpha = 1
253
+ dragBadge.text = nil
254
+ }
255
+ }