@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,108 @@
1
+ import UIKit
2
+
3
+ final class NativeListActionCell: NativeListRendererCell {
4
+ override class func measure(
5
+ _ item: NativeListItem, width: CGFloat, theme: [String: Any]?, layout: String
6
+ ) -> CGFloat? {
7
+ return item.data.string("presentation") == "accountSelector"
8
+ ? 48 : item.data.dictionary("icon") == nil ? 44 : 60
9
+ }
10
+
11
+ private let visual = NativeListLeadingVisual(frame: .zero)
12
+ private let title = NativeListTextLabel()
13
+ private let accessories = NativeListAccessoryStack(frame: .zero)
14
+ private var visualWidth: NSLayoutConstraint!
15
+ private var visualHeight: NSLayoutConstraint!
16
+ override var assetFields: [String] { ["icon"] }
17
+
18
+ override init(frame: CGRect) {
19
+ super.init(frame: frame)
20
+ visual.glyphSize = 24
21
+ root.axis = .horizontal
22
+ root.alignment = .center
23
+ [visual, title, accessories].forEach(root.addArrangedSubview)
24
+ visual.translatesAutoresizingMaskIntoConstraints = false
25
+ visualWidth = visual.widthAnchor.constraint(equalToConstant: 40)
26
+ visualHeight = visual.heightAnchor.constraint(equalToConstant: 40)
27
+ NSLayoutConstraint.activate([visualWidth, visualHeight])
28
+ title.setContentHuggingPriority(UILayoutPriority(1), for: .horizontal)
29
+ title.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
30
+ accessories.onAction = { [weak self] key, view, slot, target in
31
+ self?.emitAction(
32
+ key, from: view, source: "trailingAccessory", slot: slot, target: target,
33
+ anchorInset: self?.accessories.anchorInset(for: view) ?? 0)
34
+ }
35
+ }
36
+ required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
37
+ override func bindContent(
38
+ _ item: NativeListItem, theme: [String: Any]?, layout: String,
39
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
40
+ ) {
41
+ let style = item.data.dictionary("style") ?? [:]
42
+ root.alignment =
43
+ style.dictionary("container")?.string("contentVerticalAlignment") == "top"
44
+ ? .top
45
+ : style.dictionary("container")?.string("contentVerticalAlignment") == "bottom"
46
+ ? .bottom : .center
47
+ let selector = item.data.string("presentation") == "accountSelector"
48
+ let icon = item.data.dictionary("icon")
49
+ let descriptors =
50
+ item.data.dictionary("checkbox").map { checkbox -> [[String: Any]] in
51
+ var checkbox = checkbox
52
+ if checkbox["kind"] == nil { checkbox["kind"] = "checkbox" }
53
+ return [checkbox]
54
+ } ?? item.data.dictionaries("trailing")
55
+ accessories.bind(
56
+ item, descriptors: descriptors, theme: theme, style: style, checkboxState: checkboxState)
57
+ let hp = CGFloat(style.double("horizontalPadding", default: layout == "table" ? 16 : 12))
58
+ let vp = CGFloat(style.double("verticalPadding", default: 8))
59
+ let end =
60
+ descriptors.count == 1 && descriptors[0].string("kind") == "chevron"
61
+ ? 6 : accessories.endInset ?? hp
62
+ contentInsets = UIEdgeInsets(
63
+ top: vp, left: hp, bottom: vp, right: style["horizontalPadding"] == nil ? end : hp)
64
+ root.spacing = 12
65
+ root.setCustomSpacing(CGFloat(style.double("leadingGap", default: 12)), after: visual)
66
+ root.setCustomSpacing(accessories.contentGap ?? 12, after: title)
67
+ let tone = item.data.string("tone")
68
+ let color =
69
+ selector
70
+ ? nativeListColor(
71
+ theme, tone == "primary" ? "primaryText" : "secondaryText",
72
+ tone == "primary" ? "#202020" : "#646464")
73
+ : nativeListColor(
74
+ theme, tone == "danger" ? "negative" : "primaryText",
75
+ tone == "danger" ? "#CE2C31" : "#202020")
76
+ NativeListResolvedText(
77
+ item.data.string("title"), style: style.dictionary("title"), size: 16,
78
+ weight: selector && icon == nil ? .regular : .medium, color: color, lineHeight: 24, lines: 1
79
+ ).bind(title)
80
+ visual.isHidden = icon == nil
81
+ if var icon {
82
+ // Legacy kept the icon hairline border unless the selector or a transparent default
83
+ // background (no explicit backgroundColor) removed it.
84
+ let keepsBorder = !selector && icon["backgroundColor"] != nil
85
+ if icon["backgroundColor"] == nil { icon["backgroundColor"] = "#00000000" }
86
+ var image = style.dictionary("image") ?? [:]
87
+ if selector && image["cornerRadius"] == nil && image["shape"] == nil {
88
+ image["cornerRadius"] = 8
89
+ }
90
+ visualWidth.constant = CGFloat(image.double("width", default: selector ? 32 : 40))
91
+ visualHeight.constant = CGFloat(image.double("height", default: selector ? 32 : 40))
92
+ visual.bind(icon, style: image, key: item.key, theme: theme, isUnread: false)
93
+ if !keepsBorder { visual.layer.borderWidth = 0 }
94
+ } else {
95
+ visual.recycle()
96
+ }
97
+ }
98
+ override func bindSelectionContent(
99
+ _ item: NativeListItem,
100
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
101
+ ) {
102
+ accessories.updateSelection(item, checkboxState: checkboxState)
103
+ }
104
+ override func recycleContent() {
105
+ visual.recycle()
106
+ accessories.reset()
107
+ }
108
+ }
@@ -0,0 +1,179 @@
1
+ import UIKit
2
+
3
+ final class NativeListActivityCell: NativeListRendererCell {
4
+ override class func measure(
5
+ _ item: NativeListItem, width: CGFloat, theme: [String: Any]?, layout: String
6
+ ) -> CGFloat? {
7
+ return item.data.dictionaries("footerActions").isEmpty ? 60 : 100
8
+ }
9
+
10
+ private let visual = NativeListLeadingVisual()
11
+ private let column = UIStackView()
12
+ private let titleLine = UIStackView()
13
+ private let title = NativeListTextLabel()
14
+ private let descriptionLabel = NativeListTextLabel()
15
+ private let status = NativeListTextLabel()
16
+ private let failure = NativeListTextLabel()
17
+ private let amounts = UIStackView()
18
+ private let amountLabels = [
19
+ NativeListAccessoryButton(type: .system), NativeListAccessoryButton(type: .system),
20
+ ]
21
+ private let actions = UIStackView()
22
+ private let buttons = (0..<3).map { _ in UIButton(type: .system) }
23
+ private var actionKeys: [String] = []
24
+ private var dimensions: [NSLayoutConstraint] = []
25
+ override var assetFields: [String] { ["leading", "secondaryLeading"] }
26
+
27
+ override init(frame: CGRect) {
28
+ super.init(frame: frame)
29
+ root.axis = .horizontal
30
+ column.axis = .vertical
31
+ column.alignment = .fill
32
+ column.setContentHuggingPriority(UILayoutPriority(1), for: .horizontal)
33
+ column.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
34
+ titleLine.axis = .horizontal
35
+ titleLine.alignment = .center
36
+ titleLine.spacing = 8
37
+ title.setContentCompressionResistancePriority(.required, for: .horizontal)
38
+ failure.setContentHuggingPriority(.required, for: .horizontal)
39
+ titleLine.addArrangedSubview(title)
40
+ titleLine.addArrangedSubview(failure)
41
+ [titleLine, descriptionLabel, status, actions].forEach(column.addArrangedSubview)
42
+ amounts.axis = .vertical
43
+ amounts.alignment = .trailing
44
+ amounts.setContentHuggingPriority(.required, for: .horizontal)
45
+ amounts.setContentCompressionResistancePriority(.required, for: .horizontal)
46
+ amountLabels.forEach {
47
+ amounts.addArrangedSubview($0)
48
+ // Legacy amounts keep their full width; the text column yields first.
49
+ $0.setContentHuggingPriority(.required, for: .horizontal)
50
+ $0.setContentCompressionResistancePriority(.required, for: .horizontal)
51
+ }
52
+ actions.axis = .horizontal
53
+ actions.alignment = .center
54
+ actions.spacing = 8
55
+ for (index, button) in buttons.enumerated() {
56
+ button.tag = index
57
+ button.titleLabel?.font = nativeListFont(ofSize: 12, weight: .medium)
58
+ button.layer.cornerRadius = 8
59
+ button.contentEdgeInsets = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8)
60
+ button.addTarget(self, action: #selector(pressed(_:)), for: .touchUpInside)
61
+ actions.addArrangedSubview(button)
62
+ }
63
+ [visual, column, amounts].forEach(root.addArrangedSubview)
64
+ }
65
+ required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
66
+ override func bindContent(
67
+ _ item: NativeListItem, theme: [String: Any]?, layout: String,
68
+ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
69
+ ) {
70
+ let data = item.data
71
+ let style = data.dictionary("style") ?? [:]
72
+ let image = style.dictionary("image") ?? [:]
73
+ root.spacing = 12
74
+ root.setCustomSpacing(CGFloat(style.double("leadingGap", default: 12)), after: visual)
75
+ switch style.dictionary("container")?.string("contentVerticalAlignment") {
76
+ case "top": root.alignment = .top
77
+ case "bottom": root.alignment = .bottom
78
+ default: root.alignment = .center
79
+ }
80
+ column.spacing = CGFloat(style.double("lineGap"))
81
+ titleLine.spacing = CGFloat(style.double("titleBadgeGap", default: 8))
82
+ amounts.spacing = CGFloat(style.double("trailingGap", default: 2))
83
+ contentInsets = UIEdgeInsets(
84
+ top: CGFloat(style.double("verticalPadding", default: 8)),
85
+ left: CGFloat(style.double("horizontalPadding", default: layout == "table" ? 16 : 12)),
86
+ bottom: CGFloat(style.double("verticalPadding", default: 8)),
87
+ right: CGFloat(style.double("horizontalPadding", default: layout == "table" ? 16 : 12)))
88
+ NSLayoutConstraint.deactivate(dimensions)
89
+ dimensions = [
90
+ visual.widthAnchor.constraint(equalToConstant: CGFloat(image.double("width", default: 40))),
91
+ visual.heightAnchor.constraint(equalToConstant: CGFloat(image.double("height", default: 40))),
92
+ ]
93
+ NSLayoutConstraint.activate(dimensions)
94
+ if let leading = data.dictionary("leading") {
95
+ visual.bind(
96
+ leading, style: image, key: item.key, theme: theme, isUnread: false,
97
+ secondaryVisual: data.dictionary("secondaryLeading"))
98
+ } else {
99
+ // Legacy kept an empty leading slot (no placeholder disc) when no visual was given.
100
+ visual.clear()
101
+ }
102
+ func bind(
103
+ _ view: NativeListTextLabel, _ value: String, _ slot: String, size: CGFloat,
104
+ weight: NativeListFontWeight = .regular, color: UIColor, line: CGFloat, lines: Int = 1
105
+ ) {
106
+ NativeListResolvedText(
107
+ value, style: style.dictionary(slot), size: size, weight: weight, color: color,
108
+ lineHeight: line, lines: lines
109
+ ).bind(view)
110
+ }
111
+ bind(
112
+ title, data.string("title"), "title", size: 16, weight: .medium,
113
+ color: nativeListColor(theme, "primaryText", "#202020"), line: 24)
114
+ bind(
115
+ descriptionLabel, data.string("description"), "description", size: 14,
116
+ color: nativeListColor(theme, "secondaryText", "#646464"), line: 20, lines: 2)
117
+ let failed = data.string("status") == "Failed"
118
+ bind(
119
+ status, failed ? "" : data.string("status"), "status", size: 12,
120
+ color: nativeListColor(theme, "secondaryText", "#646464"),
121
+ line: nativeListFont(ofSize: 12).lineHeight)
122
+ bind(
123
+ failure, failed ? " Failed " : "", "status", size: 12, weight: .medium,
124
+ color: nativeListColor(theme, "negative", "#CE2C31"),
125
+ line: nativeListFont(ofSize: 12, weight: .medium).lineHeight)
126
+ failure.backgroundColor = nativeListColor(theme, "criticalBackground", "#F3000D14")
127
+ failure.layer.cornerRadius = 4
128
+ failure.clipsToBounds = true
129
+ for (index, slot) in ["primaryAmount", "secondaryAmount"].enumerated() {
130
+ let button = amountLabels[index]
131
+ let value = data.string(slot)
132
+ let color =
133
+ index == 0
134
+ ? nativeListColor(
135
+ theme, value.hasPrefix("+") ? "positive" : "primaryText",
136
+ value.hasPrefix("+") ? "#218358" : "#202020")
137
+ : nativeListColor(theme, "secondaryText", "#646464")
138
+ button.titleLabel?.numberOfLines = 1
139
+ button.titleLabel?.lineBreakMode = .byTruncatingTail
140
+ button.contentHorizontalAlignment = .center
141
+ button.contentVerticalAlignment = .center
142
+ button.rowTextOffsetY = 0
143
+ let resolved = NativeListResolvedText(
144
+ value, style: nil, size: index == 0 ? 16 : 14, weight: index == 0 ? .medium : .regular,
145
+ color: color, lineHeight: index == 0 ? 24 : 20, lines: 1, tabular: true)
146
+ button.setAttributedTitle(
147
+ resolved.attributed(direction: button.effectiveUserInterfaceLayoutDirection), for: .normal)
148
+ button.titleLabel?.font = resolved.font
149
+ button.isHidden = value.isEmpty
150
+ if let text = style.dictionary(slot) { NativeListTextStyles.applyStyledButton(button, text) }
151
+ }
152
+ let descriptors = Array(data.dictionaries("footerActions").prefix(3))
153
+ actions.isHidden = descriptors.isEmpty
154
+ actionKeys = descriptors.map { $0.string("key") }
155
+ for (index, button) in buttons.enumerated() {
156
+ button.isHidden = index >= descriptors.count
157
+ guard index < descriptors.count else { continue }
158
+ let action = descriptors[index]
159
+ button.setTitle(action.string("label"), for: .normal)
160
+ // Legacy: only the action's own flag disables it; a disabled row is already dimmed
161
+ // and non-interactive as a whole.
162
+ button.isEnabled = !action.bool("disabled")
163
+ button.backgroundColor = nativeListColor(theme, "strongBackground", "#F0F0F0")
164
+ button.setTitleColor(
165
+ nativeListColor(
166
+ theme, action.string("tone") == "danger" ? "negative" : "primaryText",
167
+ action.string("tone") == "danger" ? "#CE2C31" : "#202020"), for: .normal)
168
+ }
169
+ }
170
+ @objc private func pressed(_ button: UIButton) {
171
+ guard button.tag < actionKeys.count else { return }
172
+ emitAction(actionKeys[button.tag], from: button, source: "footerAction", slot: button.tag)
173
+ }
174
+ override func recycleContent() {
175
+ visual.recycle()
176
+ actionKeys = []
177
+ buttons.forEach { $0.isHidden = true }
178
+ }
179
+ }