@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
@@ -7,7 +7,7 @@ final class NativeListView: UIView {
7
7
  private final class ActionAnchorRecord {
8
8
  let token: String
9
9
  weak var sourceView: UIView?
10
- weak var ownerCell: NativeListCell?
10
+ weak var ownerCell: NativeListRowHost?
11
11
  let bindingEpoch: Int
12
12
  var open = false
13
13
  var invalidatedReason: String?
@@ -62,7 +62,8 @@ final class NativeListView: UIView {
62
62
  private let flowLayout = NativeListFlowLayout()
63
63
  private lazy var collectionView = NativeListCollectionView(frame: .zero, collectionViewLayout: flowLayout)
64
64
  private let footerContainer = UIView()
65
- private let footerCell = NativeListCell(frame: .zero)
65
+ private var footerCell: NativeListRowHost = NativeListRendererRegistry.create(.action)
66
+ private var footerRendererKey = NativeListRendererKey.action
66
67
  private let sectionIndexView = NativeListSectionIndexView()
67
68
  private let sectionIndexPreview = NativeListSectionIndexPreviewView()
68
69
  private var sectionIndexLayoutConstraints: [NSLayoutConstraint] = []
@@ -102,13 +103,13 @@ final class NativeListView: UIView {
102
103
  private var keyboardDismissMode: NativeListKeyboardDismissMode = .none
103
104
  private var keyboardShouldPersistTaps: NativeListKeyboardShouldPersistTaps = .never
104
105
  private var interactiveReorderSource: (key: String, index: Int)?
105
- private weak var interactiveReorderCell: NativeListCell?
106
+ private weak var interactiveReorderCell: NativeListRowHost?
106
107
  private var interactiveReorderCompactKey: String?
107
108
  private var interactiveReorderUsesAtomicTargeting = false
108
109
  private var interactiveReorderTargetIndex: Int?
109
110
  private var interactiveReorderTargetKey: String?
110
111
  private var interactiveReorderLockedCrossAxisPosition: CGFloat?
111
- private var interactiveReorderTransformedCells: [NativeListCell] = []
112
+ private var interactiveReorderTransformedCells: [NativeListRowHost] = []
112
113
  private var deferredReorderReconfigureKeys = Set<String>()
113
114
  private let interactiveReorderPlaceholder = UIView()
114
115
  private var interactiveReorderAnimator: UIViewPropertyAnimator?
@@ -129,7 +130,7 @@ final class NativeListView: UIView {
129
130
 
130
131
  override init(frame: CGRect) {
131
132
  super.init(frame: frame)
132
- collectionView.register(NativeListCell.self, forCellWithReuseIdentifier: NativeListCell.reuseIdentifier)
133
+ NativeListRendererRegistry.register(in: collectionView)
133
134
  collectionView.backgroundColor = .clear
134
135
  collectionView.delegate = self
135
136
  collectionView.dragDelegate = self
@@ -228,9 +229,9 @@ final class NativeListView: UIView {
228
229
  guard let self,
229
230
  let item = self.itemsByKey[key],
230
231
  let cell = collectionView.dequeueReusableCell(
231
- withReuseIdentifier: NativeListCell.reuseIdentifier,
232
+ withReuseIdentifier: NativeListRendererRegistry.reuseIdentifier(for: item.rendererKey),
232
233
  for: indexPath
233
- ) as? NativeListCell else { return nil }
234
+ ) as? NativeListRowHost else { return nil }
234
235
  cell.onAction = { [weak self] item, action, target, origin in
235
236
  self?.handleAction(item: item, actionKey: action, target: target, origin: origin)
236
237
  }
@@ -329,10 +330,38 @@ final class NativeListView: UIView {
329
330
  lastLayoutSize = bounds.size
330
331
  if lastLayoutDirection != direction, let config {
331
332
  configureLayout(config)
333
+ rebindRowsForLayoutDirection(config)
332
334
  }
333
335
  performPendingScrollIfNeeded()
334
336
  }
335
337
 
338
+ // A layout-direction change may arrive without a layout pass (semantic attribute or trait).
339
+ override var semanticContentAttribute: UISemanticContentAttribute {
340
+ didSet { if semanticContentAttribute != oldValue { setNeedsLayout() } }
341
+ }
342
+
343
+ override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
344
+ super.traitCollectionDidChange(previousTraitCollection)
345
+ if previousTraitCollection?.layoutDirection != traitCollection.layoutDirection {
346
+ setNeedsLayout()
347
+ }
348
+ }
349
+
350
+ /// Renderers resolve start/end alignment at bind time, and the layout direction is one of a
351
+ /// cell's bound inputs; rebinding visible rows and the footer re-resolves it. A cell whose
352
+ /// bound direction already matches only refreshes selection and appearance. Offscreen rows
353
+ /// rebind when they are next dequeued.
354
+ private func rebindRowsForLayoutDirection(_ config: NativeListConfig) {
355
+ for case let cell as NativeListRowHost in collectionView.visibleCells {
356
+ guard let indexPath = collectionView.indexPath(for: cell),
357
+ let item = item(at: indexPath) else { continue }
358
+ bind(cell: cell, item: item, itemIndex: indexPath.item)
359
+ }
360
+ if let footer = config.fixedFooter, !footerCell.isHidden {
361
+ bind(cell: footerCell, item: footer, itemIndex: nil)
362
+ }
363
+ }
364
+
336
365
  func applySnapshotJson(_ json: String) {
337
366
  guard let next = try? NativeListConfig.parse(json: json) else { return }
338
367
  invalidateActionAnchor(reason: "snapshot")
@@ -352,7 +381,10 @@ final class NativeListView: UIView {
352
381
  cancelInteractiveReorderForStructuralUpdate()
353
382
  }
354
383
  let oldItems = itemsByKey
384
+ // Chrome lives outside the row payload, so a changed list style must rebind
385
+ // rows whose own content is unchanged.
355
386
  let themeChanged = !dictionariesEqual(config?.theme, next.theme)
387
+ || !dictionariesEqual(config?.listStyle, next.listStyle)
356
388
  if config?.generation != next.generation { endReachedGeneration = nil }
357
389
  config = next
358
390
  itemsByKey = Dictionary(uniqueKeysWithValues: next.items.map { ($0.key, $0) })
@@ -371,7 +403,13 @@ final class NativeListView: UIView {
371
403
  })
372
404
  changedKeys.formUnion(deferredReorderReconfigureKeys)
373
405
  deferredReorderReconfigureKeys.removeAll()
374
- snapshot.reconfigureItems(changedKeys.filter { itemsByKey[$0] != nil })
406
+ let retainedKeys = Set(dataSource.snapshot().itemIdentifiers).intersection(keys)
407
+ let changedRetainedKeys = changedKeys.filter { retainedKeys.contains($0) }
408
+ // Reconfigure must keep its existing reuse identifier. A same-key renderer
409
+ // change needs reload so UIKit dequeues from the new structural pool.
410
+ let replacedKeys = changedRetainedKeys.filter { oldItems[$0]?.rendererKey != itemsByKey[$0]?.rendererKey }
411
+ snapshot.reloadItems(Array(replacedKeys))
412
+ snapshot.reconfigureItems(Array(changedRetainedKeys.subtracting(replacedKeys)))
375
413
  dataSource.apply(snapshot, animatingDifferences: false) { [weak self] in
376
414
  guard let self else { return }
377
415
  self.collectionView.layoutIfNeeded()
@@ -444,7 +482,7 @@ final class NativeListView: UIView {
444
482
  for indexPath in collectionView.indexPathsForVisibleItems {
445
483
  guard let item = item(at: indexPath),
446
484
  marketQuoteKeys.contains(item.key),
447
- let cell = collectionView.cellForItem(at: indexPath) as? NativeListCell else { continue }
485
+ let cell = collectionView.cellForItem(at: indexPath) as? NativeListRowHost else { continue }
448
486
  cell.updateMarketQuote(item, theme: current.theme)
449
487
  }
450
488
  if changedKeys.isEmpty {
@@ -685,7 +723,7 @@ final class NativeListView: UIView {
685
723
  let indexPath = collectionView.indexPathForItem(at: point),
686
724
  let item = item(at: indexPath),
687
725
  item.isReorderable,
688
- let cell = collectionView.cellForItem(at: indexPath) as? NativeListCell,
726
+ let cell = collectionView.cellForItem(at: indexPath) as? NativeListRowHost,
689
727
  cell.canStartWalletGroupReorder(at: gesture.location(in: cell)) else {
690
728
  interactiveReorderSource = nil
691
729
  interactiveReorderCell = nil
@@ -981,9 +1019,9 @@ final class NativeListView: UIView {
981
1019
  animator.startAnimation()
982
1020
  }
983
1021
 
984
- private func walletGroupCell(for key: String) -> NativeListCell? {
1022
+ private func walletGroupCell(for key: String) -> NativeListRowHost? {
985
1023
  guard let index = dataSource.snapshot().indexOfItem(key) else { return nil }
986
- return collectionView.cellForItem(at: IndexPath(item: index, section: 0)) as? NativeListCell
1024
+ return collectionView.cellForItem(at: IndexPath(item: index, section: 0)) as? NativeListRowHost
987
1025
  }
988
1026
 
989
1027
  // OneKey patch: flow layout sizes an interactive move in UIKit's in-flight order,
@@ -1017,7 +1055,7 @@ final class NativeListView: UIView {
1017
1055
  let distance = CGFloat(68) + flowLayout.minimumLineSpacing
1018
1056
  let updates = { [weak self] in
1019
1057
  guard let self else { return }
1020
- for case let cell as NativeListCell in self.collectionView.visibleCells {
1058
+ for case let cell as NativeListRowHost in self.collectionView.visibleCells {
1021
1059
  guard let indexPath = self.collectionView.indexPath(for: cell),
1022
1060
  indexPath.item != source.index else { continue }
1023
1061
  let offset: CGFloat
@@ -1286,8 +1324,14 @@ final class NativeListView: UIView {
1286
1324
 
1287
1325
  private func syncSectionIndexToVisibleRows() {
1288
1326
  guard !sectionIndexScrubbing, !sectionIndexEntries.isEmpty else { return }
1289
- let firstVisible = collectionView.indexPathsForVisibleItems.map(\.item).min() ?? 0
1290
- let index = sectionIndexEntries.lastIndex { $0.position <= firstVisible }
1327
+ let offset = collectionView.contentOffset.y + collectionView.adjustedContentInset.top
1328
+ // Legacy semantics: a header at the first position is active even while overscrolled;
1329
+ // no entry is active while rows above the first header are still visible.
1330
+ let index = sectionIndexEntries.lastIndex {
1331
+ if $0.position == 0 { return true }
1332
+ guard let attributes = flowLayout.layoutAttributesForItem(at: IndexPath(item: $0.position, section: 0)) else { return false }
1333
+ return attributes.frame.minY <= offset + 1
1334
+ }
1291
1335
  sectionIndexView.setActiveIndex(index)
1292
1336
  }
1293
1337
 
@@ -1311,13 +1355,27 @@ final class NativeListView: UIView {
1311
1355
  footerCell.isHidden = true
1312
1356
  return
1313
1357
  }
1358
+ if footer.rendererKey != footerRendererKey {
1359
+ let previous = footerCell
1360
+ previous.prepareForReuse()
1361
+ footerCell = NativeListRendererRegistry.create(footer.rendererKey)
1362
+ footerCell.onAction = previous.onAction
1363
+ footerCell.onBindingInvalidated = previous.onBindingInvalidated
1364
+ for recognizer in previous.gestureRecognizers ?? [] { footerCell.addGestureRecognizer(recognizer) }
1365
+ previous.removeFromSuperview()
1366
+ footerContainer.addSubview(footerCell)
1367
+ footerCell.frame = footerContainer.bounds
1368
+ footerCell.autoresizingMask = [.flexibleWidth, .flexibleHeight]
1369
+ footerRendererKey = footer.rendererKey
1370
+ }
1314
1371
  footerCell.isHidden = false
1315
1372
  footerHeightConstraint.constant = rowHeight(footer)
1316
1373
  bind(cell: footerCell, item: footer, itemIndex: nil)
1317
1374
  }
1318
1375
 
1319
- private func bind(cell: NativeListCell, item: NativeListItem, itemIndex: Int? = nil) {
1376
+ private func bind(cell: NativeListRowHost, item: NativeListItem, itemIndex: Int? = nil) {
1320
1377
  guard let config else { return }
1378
+ cell.listStyle = config.listStyle
1321
1379
  cell.bind(
1322
1380
  item: item,
1323
1381
  theme: config.theme,
@@ -1360,7 +1418,7 @@ final class NativeListView: UIView {
1360
1418
  item.isRowPressEnabled else { return }
1361
1419
  let actionKey = item.data.string("longPressActionKey")
1362
1420
  guard !actionKey.isEmpty else { return }
1363
- let origin = (collectionView.cellForItem(at: indexPath) as? NativeListCell)?.rowActionOrigin()
1421
+ let origin = (collectionView.cellForItem(at: indexPath) as? NativeListRowHost)?.rowActionOrigin()
1364
1422
  origin?.windowPoint = gesture.location(in: window)
1365
1423
  handleAction(item: item, actionKey: actionKey, target: nil, origin: origin)
1366
1424
  }
@@ -1442,7 +1500,7 @@ final class NativeListView: UIView {
1442
1500
  }
1443
1501
  for indexPath in collectionView.indexPathsForVisibleItems {
1444
1502
  guard let item = item(at: indexPath),
1445
- let cell = collectionView.cellForItem(at: indexPath) as? NativeListCell else { continue }
1503
+ let cell = collectionView.cellForItem(at: indexPath) as? NativeListRowHost else { continue }
1446
1504
  if changedSummaryKeys.contains(item.key) {
1447
1505
  bind(cell: cell, item: item, itemIndex: indexPath.item)
1448
1506
  continue
@@ -1470,6 +1528,7 @@ final class NativeListView: UIView {
1470
1528
  current.layout == next.layout,
1471
1529
  current.orientation == next.orientation,
1472
1530
  current.gridColumns == next.gridColumns,
1531
+ dictionariesEqual(current.listStyle, next.listStyle),
1473
1532
  current.stickyHeaders == next.stickyHeaders,
1474
1533
  current.contentPadding == next.contentPadding,
1475
1534
  current.contentPaddingHorizontal == next.contentPaddingHorizontal,
@@ -1517,6 +1576,7 @@ final class NativeListView: UIView {
1517
1576
  guard current.layout == next.layout,
1518
1577
  current.orientation == next.orientation,
1519
1578
  current.gridColumns == next.gridColumns,
1579
+ dictionariesEqual(current.listStyle, next.listStyle),
1520
1580
  current.stickyHeaders == next.stickyHeaders,
1521
1581
  current.contentPadding == next.contentPadding,
1522
1582
  current.contentPaddingHorizontal == next.contentPaddingHorizontal,
@@ -1670,149 +1730,29 @@ final class NativeListView: UIView {
1670
1730
 
1671
1731
  private func rowHeight(
1672
1732
  _ item: NativeListItem,
1673
- usingCompactReorderHeight: Bool = true
1733
+ usingCompactReorderHeight: Bool = true,
1734
+ allocatedWidth: CGFloat? = nil
1674
1735
  ) -> CGFloat {
1675
1736
  // OneKey patch: honor selector baseline geometry; keep compact drag sizing.
1676
- if item.type != "walletGroup", item.data["height"] != nil { return CGFloat(item.data.double("height")) }
1677
- if item.type == "system", item.data.string("variant") == "spacer" {
1678
- return CGFloat(item.data.int("height"))
1679
- }
1680
- // OneKey patch: warning height follows the current native font and available width.
1681
- if item.type == "system", item.data.string("variant") == "warning" {
1682
- let textWidth = max(1, collectionView.bounds.width - (config?.contentPaddingHorizontal ?? 0) * 2 - 24)
1683
- func textHeight(_ key: String, weight: NativeListFontWeight) -> CGFloat {
1684
- let paragraph = NSMutableParagraphStyle()
1685
- paragraph.minimumLineHeight = 20
1686
- paragraph.maximumLineHeight = 20
1687
- return ceil((item.data.string(key) as NSString).boundingRect(with: CGSize(width: textWidth, height: .greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [.font: nativeListFont(ofSize: 14, weight: weight), .paragraphStyle: paragraph], context: nil).height / 20) * 20
1688
- }
1689
- return 32 + textHeight("title", weight: .medium) + textHeight("message", weight: .regular)
1690
- }
1691
- if item.type == "walletGroup" {
1692
- if usingCompactReorderHeight, item.key == interactiveReorderCompactKey { return 68 }
1693
- let childCount = item.data.dictionaries("children").count
1694
- // OneKey patch: wallet badges contribute their own member heights.
1695
- // return CGFloat((childCount + 1) * 68 + childCount * 12)
1696
- let members = [item.data.dictionary("parent")].compactMap { $0 } + item.data.dictionaries("children")
1697
- return members.reduce(CGFloat(childCount * 12 + (members.first?["height"] != nil ? 2 : 0))) { total, data in
1698
- total + CGFloat(data.double("height", default: data.dictionaries("badges").isEmpty ? 68 : 92))
1699
- }
1737
+ if item.type == "walletGroup", usingCompactReorderHeight, item.key == interactiveReorderCompactKey { return 68 }
1738
+ if let height = item.styledHeight { return height }
1739
+ // Legacy WalletGroup ignored `row.height` (its height comes from its members);
1740
+ // `style.container.height` above still wins.
1741
+ if item.type != "walletGroup", item.data["height"] != nil {
1742
+ return CGFloat(item.data.double("height"))
1700
1743
  }
1701
- if item.type == "identity", item.data.string("presentation") == "walletSidebar" {
1702
- // OneKey patch: default sidebar badge geometry is 24 points taller.
1703
- // return 68
1704
- return item.data.dictionaries("badges").isEmpty ? 68 : 92
1705
- }
1706
- if item.type == "identity", item.data.string("presentation") == "networkSelector" {
1707
- return 47
1708
- }
1709
- let base: CGFloat
1710
- switch item.type {
1711
- case "rail": base = 40
1712
- case "activity": base = item.data.dictionaries("footerActions").isEmpty ? 60 : 100
1713
- case "message": base = messageHeight(item)
1714
- case "mediaTile": base = 244
1715
- case "metricCard":
1716
- base = item.data.string("variant") == "activity"
1717
- ? 160 + 1 / UIScreen.main.scale
1718
- : item.data.string("variant") == "performance" ? 178 : 132
1719
- case "sectionHeader":
1720
- let variant = item.data.string("variant")
1721
- let isNetworkSelector = item.data.string("presentation") == "networkSelector"
1722
- let isHistory = variant == "history" ||
1723
- item.key.hasPrefix("history-") ||
1724
- (item.sectionKey?.hasPrefix("history-") ?? false)
1725
- base = config?.layout == "table"
1726
- ? 28
1727
- : isNetworkSelector
1728
- ? 47
1729
- : isHistory
1730
- ? 16
1731
- : variant == "summary"
1732
- ? 68
1733
- : variant == "gallery"
1734
- ? 32
1735
- : item.data.dictionary("checkbox") != nil
1736
- ? 56
1737
- : config?.layout == "linear" ? 30 : 36
1738
- case "system":
1739
- if item.data.string("variant") == "loading" && item.data.string("loadingStyle") == "skeleton" {
1740
- base = 56
1741
- } else if item.data.string("variant") == "loading" && item.data.string("loadingStyle") == "spinner" {
1742
- base = 52
1743
- } else if item.data.string("presentation") == "market" {
1744
- base = item.data.string("variant") == "loading" ? 68 : 44
1745
- } else {
1746
- switch item.data.string("variant") {
1747
- case "noMatch", "end": base = 36
1748
- case "retry": base = 44
1749
- default: base = 56
1750
- }
1751
- }
1752
- case "action":
1753
- base = item.data.string("presentation") == "accountSelector"
1754
- ? 48
1755
- : item.data.dictionary("icon") == nil ? 44 : 60
1756
- case "dataRow":
1757
- base = item.data.dictionaries("columns").contains {
1758
- !$0.string("secondaryText").isEmpty
1759
- } ? 60 : 56
1760
- case "market":
1761
- let style = item.data.dictionary("style")
1762
- let imageHeight = CGFloat(style?.dictionary("image")?.double(
1763
- "height",
1764
- default: item.data.string("variant") == "stock" ? 40 : 32
1765
- ) ?? (item.data.string("variant") == "stock" ? 40 : 32))
1766
- let verticalPadding = CGFloat(style?.double("verticalPadding", default: 12) ?? 12)
1767
- base = max(item.data.string("variant") == "stock" ? 72 : 68, imageHeight + verticalPadding * 2)
1768
- default:
1769
- if item.type == "identity", !item.data.string("tertiary").isEmpty {
1770
- base = 72
1771
- } else if item.type == "identity", !item.data.string("subtitle").isEmpty {
1772
- base = 60
1773
- } else {
1774
- base = 56
1775
- }
1744
+ let availableWidth = max(0, collectionView.bounds.width - flowLayout.sectionInset.left - flowLayout.sectionInset.right)
1745
+ let columnWidth = config?.layout == "grid"
1746
+ ? floor((availableWidth - CGFloat((config?.gridColumns ?? 2) - 1) * (config?.itemSpacing ?? 0)) / CGFloat(config?.gridColumns ?? 2))
1747
+ : availableWidth
1748
+ let rowWidth = allocatedWidth ?? (config?.orientation == "horizontal" ? 280 : columnWidth)
1749
+ if let height = NativeListRendererRegistry.measure(item, width: rowWidth, theme: config?.theme, layout: config?.layout ?? "linear") {
1750
+ return max(0, height + (NativeListRendererRegistry.appliesSizePreset(item) ? (item.data.string("size") == "small" ? -8 : item.data.string("size") == "large" ? 12 : 0) : 0))
1776
1751
  }
1777
- let hasFixedHeaderHeight = item.type == "sectionHeader" &&
1778
- ["summary", "gallery"].contains(item.data.string("variant"))
1779
- let modifier: CGFloat = hasFixedHeaderHeight
1780
- ? 0
1781
- : item.data.string("size", default: "medium") == "small"
1782
- ? -8
1783
- : item.data.string("size") == "large" ? 12 : 0
1784
- let sectionSpacing: CGFloat = 0
1785
- let tableAdjustment: CGFloat = config?.layout == "table" &&
1786
- item.type == "dataRow" &&
1787
- !item.data.dictionaries("columns").contains(where: { !$0.string("secondaryText").isEmpty })
1788
- ? -8
1789
- : 0
1790
- return max(0, base + modifier + sectionSpacing + tableAdjustment)
1791
- }
1792
-
1793
- private func messageHeight(_ item: NativeListItem) -> CGFloat {
1794
- let maximumBodyLines = min(3, max(1, item.data.int("bodyLines", default: 3)))
1795
- let horizontalInsets = (config?.contentPaddingHorizontal ?? 0) * 2 + 40
1796
- let leadingWidth: CGFloat = item.data.dictionary("leading") == nil ? 0 : 40
1797
- let thumbnailWidth: CGFloat = item.data.dictionary("thumbnail") == nil ? 0 : 76
1798
- let textWidth = max(1, collectionView.bounds.width - horizontalInsets - leadingWidth - thumbnailWidth)
1799
- let bodyBounds = (item.data.string("body") as NSString).boundingRect(
1800
- with: CGSize(width: textWidth, height: .greatestFiniteMagnitude),
1801
- options: [.usesLineFragmentOrigin, .usesFontLeading],
1802
- attributes: [.font: nativeListFont(ofSize: 14)],
1803
- context: nil
1804
- )
1805
- let titleBounds = (item.data.string("title") as NSString).boundingRect(
1806
- with: CGSize(width: textWidth, height: .greatestFiniteMagnitude),
1807
- options: [.usesLineFragmentOrigin, .usesFontLeading],
1808
- attributes: [.font: nativeListFont(ofSize: 14, weight: .semibold)],
1809
- context: nil
1810
- )
1811
- let bodyLines = min(maximumBodyLines, max(1, Int(ceil(bodyBounds.height / 20))))
1812
- let titleLines = min(2, max(1, Int(ceil(titleBounds.height / 20))))
1813
- return 32 + CGFloat(titleLines * 20 + bodyLines * 20) + 22
1752
+ return 56
1814
1753
  }
1815
1754
 
1755
+
1816
1756
  private func emit(_ block: ((String) -> Void)?, _ value: [String: Any]) {
1817
1757
  guard JSONSerialization.isValidJSONObject(value),
1818
1758
  let data = try? JSONSerialization.data(withJSONObject: value),
@@ -1894,7 +1834,7 @@ final class NativeListView: UIView {
1894
1834
  reorderLongPress.delegate = nil
1895
1835
  marketLongPress.delegate = nil
1896
1836
 
1897
- for case let cell as NativeListCell in collectionView.visibleCells {
1837
+ for case let cell as NativeListRowHost in collectionView.visibleCells {
1898
1838
  cell.onAction = nil
1899
1839
  cell.onBindingInvalidated = nil
1900
1840
  cell.prepareForReuse()
@@ -1961,7 +1901,7 @@ final class NativeListView: UIView {
1961
1901
  && (sourceView === ownerCell.contentView || sourceView.isDescendant(of: ownerCell.contentView))
1962
1902
  }
1963
1903
 
1964
- private func handleBindingInvalidated(cell: NativeListCell, epoch: Int) {
1904
+ private func handleBindingInvalidated(cell: NativeListRowHost, epoch: Int) {
1965
1905
  guard let anchor = actionAnchor,
1966
1906
  anchor.ownerCell === cell,
1967
1907
  anchor.bindingEpoch == epoch else { return }
@@ -2158,7 +2098,8 @@ extension NativeListView: UICollectionViewDelegateFlowLayout {
2158
2098
  if config.orientation == "horizontal" {
2159
2099
  let width: CGFloat = item.type == "rail" ? railWidth(item) : item.type == "mediaTile" ? 200 : 280
2160
2100
  let availableHeight = max(0, collectionView.bounds.height - insets.top - insets.bottom)
2161
- let height = item.type == "rail" ? min(rowHeight(item), availableHeight) : availableHeight
2101
+ let height = item.type == "walletGroup" && item.key == interactiveReorderCompactKey ? 68
2102
+ : item.styledHeight ?? (item.data["height"] != nil ? rowHeight(item) : item.type == "rail" ? min(rowHeight(item), availableHeight) : availableHeight)
2162
2103
  return CGSize(width: width, height: height)
2163
2104
  }
2164
2105
  let available = max(0, collectionView.bounds.width - insets.left - insets.right)
@@ -2166,34 +2107,20 @@ extension NativeListView: UICollectionViewDelegateFlowLayout {
2166
2107
  if config.layout == "grid", !structural {
2167
2108
  let spacing = CGFloat(config.gridColumns - 1) * config.itemSpacing
2168
2109
  let width = floor((available - spacing) / CGFloat(config.gridColumns))
2169
- let height = item.type == "mediaTile" ? width + 48 : rowHeight(item)
2110
+ let height = item.styledHeight ?? (item.type == "mediaTile" && item.data["height"] == nil ? width + 48 : rowHeight(item, allocatedWidth: width))
2170
2111
  return CGSize(width: width, height: height)
2171
2112
  }
2172
- return CGSize(width: available, height: rowHeight(item))
2113
+ return CGSize(width: available, height: rowHeight(item, allocatedWidth: available))
2173
2114
  }
2174
2115
 
2175
2116
  private func railWidth(_ item: NativeListItem) -> CGFloat {
2176
- let titleWidth = (item.data.string("title") as NSString).size(
2177
- withAttributes: [.font: nativeListFont(ofSize: 12, weight: .medium)]
2178
- ).width
2179
- let badge = item.data.dictionary("badge")?.string("text") ?? ""
2180
- let badgeWidth = (badge as NSString).size(
2181
- withAttributes: [.font: nativeListTabularFont(ofSize: 12, weight: .medium)]
2182
- ).width
2183
- let status = item.data.string("status")
2184
- let statusWidth = status.isEmpty || status == "none" ? 0 : (status as NSString).size(
2185
- withAttributes: [.font: nativeListTabularFont(ofSize: 12)]
2186
- ).width
2187
- let visibleTextCount = 1 + (badgeWidth > 0 ? 1 : 0) + (statusWidth > 0 ? 1 : 0)
2188
- let width = 4 + 20 + 6 + titleWidth + badgeWidth + statusWidth
2189
- + CGFloat(max(0, visibleTextCount - 1)) * 6 + 4
2190
- return min(288, max(72, ceil(width + 8)))
2117
+ NativeListRailRenderer.Resolved(item, theme: config?.theme).horizontalWidth
2191
2118
  }
2192
2119
 
2193
2120
  func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
2194
2121
  guard let item = config?.items[safe: indexPath.item] else { return }
2195
2122
  if item.type == "walletGroup" { return }
2196
- let origin = (collectionView.cellForItem(at: indexPath) as? NativeListCell)?.rowActionOrigin()
2123
+ let origin = (collectionView.cellForItem(at: indexPath) as? NativeListRowHost)?.rowActionOrigin()
2197
2124
  handleRowPress(item, origin: origin)
2198
2125
  }
2199
2126
 
@@ -2206,7 +2133,7 @@ extension NativeListView: UICollectionViewDelegateFlowLayout {
2206
2133
  guard let item = config?.items[safe: indexPath.item], item.type == "market" else { return }
2207
2134
  let actionKey = item.data.string("pressInActionKey")
2208
2135
  guard !actionKey.isEmpty else { return }
2209
- let origin = (collectionView.cellForItem(at: indexPath) as? NativeListCell)?.rowActionOrigin()
2136
+ let origin = (collectionView.cellForItem(at: indexPath) as? NativeListRowHost)?.rowActionOrigin()
2210
2137
  handleAction(item: item, actionKey: actionKey, target: nil, origin: origin)
2211
2138
  }
2212
2139
 
@@ -2365,11 +2292,15 @@ final class NativeListFlowLayout: UICollectionViewFlowLayout {
2365
2292
  !stickyItemIndexes.isEmpty,
2366
2293
  let collectionView else { return super.layoutAttributesForElements(in: rect) }
2367
2294
  let base = super.layoutAttributesForElements(in: rect)?.compactMap { $0.copy() as? UICollectionViewLayoutAttributes } ?? []
2368
- let firstVisible = collectionView.indexPathsForVisibleItems.map(\.item).min() ?? 0
2369
- guard let stickyIndex = stickyItemIndexes.filter({ $0 <= firstVisible }).max(),
2295
+ let pinY = collectionView.contentOffset.y + collectionView.adjustedContentInset.top
2296
+ // Visible cells can still describe the old viewport during an imperative
2297
+ // scroll. Resolve the pinned header from unmodified layout positions.
2298
+ guard let stickyIndex = stickyItemIndexes.filter({
2299
+ guard let attributes = super.layoutAttributesForItem(at: IndexPath(item: $0, section: 0)) else { return false }
2300
+ return attributes.frame.minY <= pinY
2301
+ }).max(),
2370
2302
  let original = super.layoutAttributesForItem(at: IndexPath(item: stickyIndex, section: 0)),
2371
2303
  let sticky = original.copy() as? UICollectionViewLayoutAttributes else { return base }
2372
- let pinY = collectionView.contentOffset.y + collectionView.adjustedContentInset.top
2373
2304
  let nextHeaderY = stickyItemIndexes
2374
2305
  .filter { $0 > stickyIndex }
2375
2306
  .min()
@@ -1,2 +1,56 @@
1
1
  "use strict";
2
+
3
+ /**
4
+ * Named typography steps, shared with the application's design scale. Resolved
5
+ * to fontSize/lineHeight/fontWeight in JavaScript before the snapshot is
6
+ * serialized, so no native renderer needs to know the vocabulary.
7
+ * See docs/STYLE_SPEC.md §3.2.
8
+ */
9
+
10
+ /** Retained alias: Market shipped this name before the style surface was shared. */
11
+
12
+ /** Local box vocabulary. Each template exposes only parameters for its own slots. */
13
+
14
+ /** Template-owned slots determine which local box parameters exist, on every platform. */
15
+ export const ROW_BOX_STYLE_KEYS_BY_TYPE = {
16
+ identity: ['horizontalPadding', 'verticalPadding', 'leadingGap', 'lineGap', 'titleBadgeGap', 'trailingGap', 'image'],
17
+ walletGroup: ['horizontalPadding', 'verticalPadding'],
18
+ rail: ['horizontalPadding', 'verticalPadding', 'leadingGap', 'titleBadgeGap', 'trailingGap', 'image'],
19
+ activity: ['horizontalPadding', 'verticalPadding', 'leadingGap', 'lineGap', 'trailingGap', 'image'],
20
+ message: ['horizontalPadding', 'verticalPadding', 'leadingGap', 'lineGap', 'image'],
21
+ dataRow: ['horizontalPadding', 'verticalPadding', 'leadingGap', 'lineGap', 'titleBadgeGap', 'image'],
22
+ market: ['horizontalPadding', 'verticalPadding', 'leadingGap', 'lineGap', 'titleBadgeGap', 'trailingGap', 'image'],
23
+ mediaTile: ['horizontalPadding', 'verticalPadding', 'leadingGap', 'lineGap', 'image'],
24
+ metricCard: ['horizontalPadding', 'verticalPadding', 'leadingGap', 'lineGap', 'image'],
25
+ sectionHeader: ['horizontalPadding', 'verticalPadding', 'lineGap', 'trailingGap'],
26
+ action: ['horizontalPadding', 'verticalPadding', 'leadingGap', 'trailingGap', 'image'],
27
+ system: ['horizontalPadding', 'verticalPadding', 'lineGap']
28
+ };
29
+
30
+ /**
31
+ * A style key names the model field it modifies, never the view that carries
32
+ * it. Each template renderer owns its views and maps the key to whichever view
33
+ * renders that field (for example metricCard `value` is its large number), so
34
+ * the same key name keeps one meaning across platforms. See docs/STYLE_SPEC.md §4.
35
+ */
36
+
37
+ // OneKey patch: keep selector decoration and text data serializable.
38
+
39
+ // OneKey patch: preserve compact very-small-balance digit runs.
40
+
41
+ /** Native Market quote row. All values are already localized/formatted strings. */
42
+
43
+ /** Compact KPI card for dashboards and grids. */
44
+
45
+ /**
46
+ * List chrome that every platform can honour. Pull to refresh, the section index
47
+ * rail and the reorder preview are deliberately absent: the first two are system
48
+ * controls or three independent constant sets, and the third is drawn with
49
+ * platform-specific primitives. See docs/STYLE_SPEC.md §5.
50
+ */
51
+
52
+ // OneKey patch: include selector-only mutable presentation fields.
53
+ // type CommonPatchFields = 'revision' | 'disabled' | 'selected' | 'separator';
54
+ // OneKey patch: balance patches also refresh the existing row's spoken content.
55
+ // type CommonPatchFields = 'revision' | 'disabled' | 'selected' | 'separator' | 'height' | 'heightRounding' | 'opacity' | 'pressDisabled';
2
56
  //# sourceMappingURL=models.js.map