@onekeyfe/react-native-native-list 6.7.0-alpha.378 → 6.7.0-alpha.383

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 (116) hide show
  1. package/android/src/main/java/com/onekey/nativelist/HybridNativeList.kt +14 -1
  2. package/android/src/main/java/com/onekey/nativelist/NativeListAccessoryStack.kt +240 -11
  3. package/android/src/main/java/com/onekey/nativelist/NativeListActivityDetailsView.kt +7 -4
  4. package/android/src/main/java/com/onekey/nativelist/NativeListActivityRenderer.kt +6 -3
  5. package/android/src/main/java/com/onekey/nativelist/NativeListButtonGroupRenderer.kt +102 -0
  6. package/android/src/main/java/com/onekey/nativelist/NativeListContainerSlotAdapter.kt +12 -0
  7. package/android/src/main/java/com/onekey/nativelist/NativeListDataRowRenderer.kt +87 -7
  8. package/android/src/main/java/com/onekey/nativelist/NativeListIdentityRenderer.kt +177 -1
  9. package/android/src/main/java/com/onekey/nativelist/NativeListLeadingVisual.kt +43 -14
  10. package/android/src/main/java/com/onekey/nativelist/NativeListMessageRenderer.kt +25 -0
  11. package/android/src/main/java/com/onekey/nativelist/NativeListModels.kt +33 -2
  12. package/android/src/main/java/com/onekey/nativelist/NativeListPrimitives.kt +22 -0
  13. package/android/src/main/java/com/onekey/nativelist/NativeListRendererRegistry.kt +2 -0
  14. package/android/src/main/java/com/onekey/nativelist/NativeListRendererRowView.kt +2 -0
  15. package/android/src/main/java/com/onekey/nativelist/NativeListResolvedText.kt +3 -2
  16. package/android/src/main/java/com/onekey/nativelist/NativeListSectionHeaderRenderer.kt +69 -0
  17. package/android/src/main/java/com/onekey/nativelist/NativeListTextView.kt +55 -1
  18. package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +1912 -107
  19. package/android/src/test/java/com/margelo/nitro/nativelist/NativeListPressPolicyTest.kt +1 -0
  20. package/docs/ROW_TEMPLATES.md +60 -9
  21. package/docs/SPEC.md +450 -5
  22. package/docs/STYLE_SPEC.md +6 -0
  23. package/ios/HybridNativeList.swift +21 -2
  24. package/ios/NativeListAccessoryStack.swift +227 -39
  25. package/ios/NativeListActivityDetailsView.swift +18 -8
  26. package/ios/NativeListActivityRenderer.swift +8 -2
  27. package/ios/NativeListButtonGroupRenderer.swift +89 -0
  28. package/ios/NativeListDataRowRenderer.swift +155 -27
  29. package/ios/NativeListDesignAssets.swift +3 -0
  30. package/ios/NativeListIdentityRenderer.swift +166 -30
  31. package/ios/NativeListLeadingVisual.swift +49 -14
  32. package/ios/NativeListMessageRenderer.swift +34 -1
  33. package/ios/NativeListModels.swift +42 -2
  34. package/ios/NativeListRendererRegistry.swift +1 -0
  35. package/ios/NativeListResolvedText.swift +2 -1
  36. package/ios/NativeListRowHost.swift +14 -0
  37. package/ios/NativeListSectionHeaderRenderer.swift +88 -0
  38. package/ios/RNCNativeListView.swift +1013 -55
  39. package/ios/Resources/NativeListIcons.xcassets/onekey_crypto_coin_outline.imageset/Contents.json +15 -0
  40. package/ios/Resources/NativeListIcons.xcassets/onekey_crypto_coin_outline.imageset/icon.svg +4 -0
  41. package/ios/Resources/NativeListIcons.xcassets/onekey_gas_solid.imageset/Contents.json +15 -0
  42. package/ios/Resources/NativeListIcons.xcassets/onekey_gas_solid.imageset/icon.svg +3 -0
  43. package/ios/Resources/NativeListIcons.xcassets/onekey_refresh_ccw_outline.imageset/Contents.json +15 -0
  44. package/ios/Resources/NativeListIcons.xcassets/onekey_refresh_ccw_outline.imageset/icon.svg +3 -0
  45. package/ios/tests/NativeListActivityPresentationTests.swift +56 -0
  46. package/ios/tests/NativeListLifecycleTests.swift +2720 -3
  47. package/lib/module/NativeList.js +175 -28
  48. package/lib/module/NativeList.web.js +129 -30
  49. package/lib/module/index.js +1 -1
  50. package/lib/module/models.js +1 -0
  51. package/lib/module/selection.js +1 -1
  52. package/lib/module/validation.js +421 -8
  53. package/lib/module/web/NativeListWebEngine.js +507 -19
  54. package/lib/module/web/templates/ActivityRowRenderer.js +8 -7
  55. package/lib/module/web/templates/ButtonGroupRowRenderer.js +47 -0
  56. package/lib/module/web/templates/DataRowRenderer.js +40 -5
  57. package/lib/module/web/templates/IdentityRowRenderer.js +90 -4
  58. package/lib/module/web/templates/MessageRowRenderer.js +18 -2
  59. package/lib/module/web/templates/RowContainerStyle.js +1 -0
  60. package/lib/module/web/templates/RowRendererRegistry.js +3 -0
  61. package/lib/module/web/templates/SectionHeaderRowRenderer.js +36 -2
  62. package/lib/nitrogen/generated/android/c++/JHybridNativeListSpec.cpp +26 -0
  63. package/lib/nitrogen/generated/android/c++/JHybridNativeListSpec.hpp +4 -0
  64. package/lib/nitrogen/generated/android/c++/views/JHybridNativeListStateUpdater.cpp +10 -0
  65. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/HybridNativeListSpec.kt +20 -0
  66. package/lib/nitrogen/generated/ios/c++/HybridNativeListSpecSwift.hpp +14 -0
  67. package/lib/nitrogen/generated/ios/c++/views/HybridNativeListComponent.mm +12 -0
  68. package/lib/nitrogen/generated/ios/swift/HybridNativeListSpec.swift +2 -0
  69. package/lib/nitrogen/generated/ios/swift/HybridNativeListSpec_cxx.swift +43 -0
  70. package/lib/nitrogen/generated/shared/c++/HybridNativeListSpec.cpp +4 -0
  71. package/lib/nitrogen/generated/shared/c++/HybridNativeListSpec.hpp +4 -0
  72. package/lib/nitrogen/generated/shared/c++/views/HybridNativeListComponent.cpp +4 -0
  73. package/lib/nitrogen/generated/shared/c++/views/HybridNativeListComponent.hpp +6 -0
  74. package/lib/nitrogen/generated/shared/json/NativeListConfig.json +2 -0
  75. package/lib/typescript/src/NativeList.d.ts +1 -1
  76. package/lib/typescript/src/NativeList.nitro.d.ts +2 -0
  77. package/lib/typescript/src/NativeList.types.d.ts +18 -0
  78. package/lib/typescript/src/index.d.ts +2 -2
  79. package/lib/typescript/src/models.d.ts +105 -9
  80. package/lib/typescript/src/validation.d.ts +12 -0
  81. package/lib/typescript/src/web/NativeListWebEngine.d.ts +32 -0
  82. package/lib/typescript/src/web/templates/ButtonGroupRowRenderer.d.ts +14 -0
  83. package/lib/typescript/src/web/templates/IdentityRowRenderer.d.ts +1 -1
  84. package/lib/typescript/src/web/templates/MessageRowRenderer.d.ts +9 -3
  85. package/nitrogen/generated/android/c++/JHybridNativeListSpec.cpp +26 -0
  86. package/nitrogen/generated/android/c++/JHybridNativeListSpec.hpp +4 -0
  87. package/nitrogen/generated/android/c++/views/JHybridNativeListStateUpdater.cpp +10 -0
  88. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/HybridNativeListSpec.kt +20 -0
  89. package/nitrogen/generated/ios/c++/HybridNativeListSpecSwift.hpp +14 -0
  90. package/nitrogen/generated/ios/c++/views/HybridNativeListComponent.mm +12 -0
  91. package/nitrogen/generated/ios/swift/HybridNativeListSpec.swift +2 -0
  92. package/nitrogen/generated/ios/swift/HybridNativeListSpec_cxx.swift +43 -0
  93. package/nitrogen/generated/shared/c++/HybridNativeListSpec.cpp +4 -0
  94. package/nitrogen/generated/shared/c++/HybridNativeListSpec.hpp +4 -0
  95. package/nitrogen/generated/shared/c++/views/HybridNativeListComponent.cpp +4 -0
  96. package/nitrogen/generated/shared/c++/views/HybridNativeListComponent.hpp +6 -0
  97. package/nitrogen/generated/shared/json/NativeListConfig.json +2 -0
  98. package/package.json +3 -3
  99. package/react-native-native-list.podspec +1 -0
  100. package/src/NativeList.nitro.ts +2 -0
  101. package/src/NativeList.tsx +313 -44
  102. package/src/NativeList.types.ts +22 -0
  103. package/src/NativeList.web.tsx +204 -48
  104. package/src/index.ts +2 -0
  105. package/src/models.ts +117 -3
  106. package/src/selection.ts +1 -0
  107. package/src/validation.ts +852 -6
  108. package/src/web/NativeListWebEngine.ts +803 -20
  109. package/src/web/templates/ActivityRowRenderer.ts +14 -6
  110. package/src/web/templates/ButtonGroupRowRenderer.ts +58 -0
  111. package/src/web/templates/DataRowRenderer.ts +93 -20
  112. package/src/web/templates/IdentityRowRenderer.ts +150 -5
  113. package/src/web/templates/MessageRowRenderer.ts +23 -7
  114. package/src/web/templates/RowContainerStyle.ts +2 -0
  115. package/src/web/templates/RowRendererRegistry.ts +11 -1
  116. package/src/web/templates/SectionHeaderRowRenderer.ts +47 -4
@@ -37,6 +37,8 @@ import {
37
37
  canonicalNativeListAvatarUri,
38
38
  } from './web/NativeListWebAvatarCache';
39
39
 
40
+ const collapsedExpandedContentStyle = { display: 'none' } as const;
41
+
40
42
  export type { NativeListProps, NativeListRef } from './NativeList.types';
41
43
  export type {
42
44
  ActionAnchorState,
@@ -93,6 +95,10 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
93
95
  listHeader,
94
96
  listEmpty,
95
97
  listFooter,
98
+ listExpandedContent,
99
+ listStructureToggle,
100
+ listSidecar,
101
+ sidecarLayout,
96
102
  topOverlay: _topOverlay,
97
103
  topScrollEdgeEffect: _topScrollEdgeEffect,
98
104
  webScrollHost,
@@ -109,6 +115,7 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
109
115
  onReorder,
110
116
  onEndReached,
111
117
  onVisibleRangeChanged,
118
+ onStructuralTransitionSettled: _onStructuralTransitionSettled,
112
119
  onRefresh,
113
120
  onScrollToIndexFailed,
114
121
  initialScrollIndex,
@@ -132,10 +139,16 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
132
139
  }) ?? 0,
133
140
  [inputStickyInset]
134
141
  );
135
- const preparedSnapshot = useMemo(
136
- () => containNativeListInput(() => validateSnapshot(inputSnapshot)),
137
- [inputSnapshot]
138
- );
142
+ // Freshness is limited to this component invocation; later renders revalidate.
143
+ let freshlyValidatedSnapshot: NativeListSnapshot | undefined;
144
+ const preparedSnapshot = useMemo(() => {
145
+ const prepared = containNativeListInput(() =>
146
+ validateSnapshot(inputSnapshot)
147
+ );
148
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- The value is intentionally render-local.
149
+ freshlyValidatedSnapshot = prepared;
150
+ return prepared;
151
+ }, [inputSnapshot]);
139
152
  const snapshot = preparedSnapshot ?? acceptedSnapshotRef.current;
140
153
  const fontFamiliesJson = useNativeListFontFamiliesJson();
141
154
  const fontFamiliesJsonRef = useRef(fontFamiliesJson);
@@ -143,49 +156,28 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
143
156
  const hasEmptySlot = listEmpty !== undefined;
144
157
  const hasEmptySlotRef = useRef(hasEmptySlot);
145
158
  hasEmptySlotRef.current = hasEmptySlot;
146
- const [slotState, setSlotState] = useState(() => ({
147
- hasRows: snapshot.rows.length > 0,
148
- emptyBottomPadding:
149
- !snapshot.rows.length && hasEmptySlot
150
- ? snapshot.layout.contentPaddingBottom ??
151
- snapshot.layout.contentPadding ??
152
- 0
153
- : 0,
154
- padding:
155
- snapshot.layout.contentPaddingHorizontal ??
156
- snapshot.layout.contentPadding ??
157
- 0,
158
- }));
159
- const synchronizeSlots = (next: NativeListSnapshot) => {
160
- const hasRows = next.rows.length > 0;
161
- const padding =
162
- next.layout.contentPaddingHorizontal ?? next.layout.contentPadding ?? 0;
163
- const emptyBottomPadding =
164
- !hasRows && hasEmptySlotRef.current
165
- ? next.layout.contentPaddingBottom ?? next.layout.contentPadding ?? 0
166
- : 0;
167
- setSlotState((previous) =>
168
- previous.hasRows === hasRows &&
169
- previous.padding === padding &&
170
- previous.emptyBottomPadding === emptyBottomPadding
171
- ? previous
172
- : { hasRows, padding, emptyBottomPadding }
173
- );
174
- };
159
+ const [viewportWidth, setViewportWidth] = useState(0);
160
+ const [sidecarHeight, setSidecarHeight] = useState(0);
175
161
  const normalizeWebSnapshot = (
176
162
  next: NativeListSnapshot,
177
163
  hasReactEmpty = hasEmptySlotRef.current,
178
- hasSlotLayout = useSlotLayoutRef.current
164
+ hasSlotLayout = useSlotLayoutRef.current,
165
+ validatedInThisRender = false
179
166
  ) => {
180
167
  if (hasSlotLayout && next.layout.orientation === 'horizontal')
181
168
  throw new Error(
182
169
  'Web parent scrolling and container slots support vertical lists only'
183
170
  );
184
171
  const emptySlot = hasReactEmpty && next.rows.length === 0;
185
- const validated = validateSnapshot(
186
- emptySlot ? { ...next, emptyState: undefined } : next
187
- );
188
- return emptySlot
172
+ const normalized =
173
+ emptySlot && next.emptyState !== undefined
174
+ ? { ...next, emptyState: undefined }
175
+ : next;
176
+ const validated = validatedInThisRender
177
+ ? normalized
178
+ : validateSnapshot(normalized);
179
+ // Preserve an already normalized fallback's identity across rerenders.
180
+ return emptySlot && validated.layout.contentPaddingBottom !== 0
189
181
  ? {
190
182
  ...validated,
191
183
  layout: { ...validated.layout, contentPaddingBottom: 0 },
@@ -205,6 +197,9 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
205
197
  listHeader !== undefined ||
206
198
  listEmpty !== undefined ||
207
199
  listFooter !== undefined;
200
+ useSlotLayoutRef.current ||= listSidecar !== undefined;
201
+ useSlotLayoutRef.current ||=
202
+ listExpandedContent !== undefined || listStructureToggle !== undefined;
208
203
  const useSlotLayout = useSlotLayoutRef.current;
209
204
  const resolvedScrollHost =
210
205
  webScrollHost === undefined
@@ -217,11 +212,86 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
217
212
  webVirtualizationEnabledRef.current = webVirtualizationEnabled;
218
213
  const validatedSnapshot = useMemo(
219
214
  () =>
220
- containNativeListInput(() =>
221
- normalizeWebSnapshot(snapshot, hasEmptySlot, useSlotLayout)
222
- ) ?? acceptedSnapshotRef.current,
223
- [snapshot, hasEmptySlot, useSlotLayout]
215
+ containNativeListInput(() => {
216
+ if ((listSidecar !== undefined) !== (sidecarLayout !== undefined))
217
+ throw new Error(
218
+ 'listSidecar and sidecarLayout must be supplied together'
219
+ );
220
+ if (sidecarLayout) {
221
+ const { width, gap, maxContentWidth } = sidecarLayout;
222
+ if (
223
+ snapshot.layout.kind !== 'linear' ||
224
+ !Number.isFinite(width) ||
225
+ width <= 0 ||
226
+ !Number.isFinite(gap) ||
227
+ gap < 0 ||
228
+ !Number.isFinite(maxContentWidth) ||
229
+ maxContentWidth <= width + gap
230
+ )
231
+ throw new Error('Invalid NativeList sidecar layout');
232
+ }
233
+ return normalizeWebSnapshot(
234
+ snapshot,
235
+ hasEmptySlot,
236
+ useSlotLayout,
237
+ preparedSnapshot !== undefined &&
238
+ preparedSnapshot === freshlyValidatedSnapshot
239
+ );
240
+ }) ?? acceptedSnapshotRef.current,
241
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- Preserve the existing memo dependencies; freshness is render-local.
242
+ [snapshot, hasEmptySlot, useSlotLayout, listSidecar, sidecarLayout]
224
243
  );
244
+ const [slotState, setSlotState] = useState(() => ({
245
+ hasRows: validatedSnapshot.rows.length > 0,
246
+ emptyBottomPadding:
247
+ !validatedSnapshot.rows.length && hasEmptySlot
248
+ ? validatedSnapshot.layout.contentPaddingBottom ??
249
+ validatedSnapshot.layout.contentPadding ??
250
+ 0
251
+ : 0,
252
+ padding:
253
+ validatedSnapshot.layout.contentPaddingHorizontal ??
254
+ validatedSnapshot.layout.contentPadding ??
255
+ 0,
256
+ disclosurePresent: validatedSnapshot.disclosure !== undefined,
257
+ disclosureExpanded: validatedSnapshot.disclosure?.expanded === true,
258
+ hasExpandedContent: validatedSnapshot.disclosure?.expanded === true,
259
+ expandedTrailingInset:
260
+ validatedSnapshot.disclosure?.expandedTrailingInset ?? 0,
261
+ }));
262
+ const synchronizeSlots = (next: NativeListSnapshot) => {
263
+ const hasRows = next.rows.length > 0;
264
+ const padding =
265
+ next.layout.contentPaddingHorizontal ?? next.layout.contentPadding ?? 0;
266
+ const emptyBottomPadding =
267
+ !hasRows && hasEmptySlotRef.current
268
+ ? next.layout.contentPaddingBottom ?? next.layout.contentPadding ?? 0
269
+ : 0;
270
+ const disclosurePresent = next.disclosure !== undefined;
271
+ const disclosureExpanded = next.disclosure?.expanded === true;
272
+ const expandedTrailingInset = next.disclosure?.expandedTrailingInset ?? 0;
273
+ setSlotState((previous) => {
274
+ const hasExpandedContent =
275
+ previous.hasExpandedContent || disclosureExpanded;
276
+ return previous.hasRows === hasRows &&
277
+ previous.padding === padding &&
278
+ previous.emptyBottomPadding === emptyBottomPadding &&
279
+ previous.disclosurePresent === disclosurePresent &&
280
+ previous.disclosureExpanded === disclosureExpanded &&
281
+ previous.hasExpandedContent === hasExpandedContent &&
282
+ previous.expandedTrailingInset === expandedTrailingInset
283
+ ? previous
284
+ : {
285
+ hasRows,
286
+ padding,
287
+ emptyBottomPadding,
288
+ disclosurePresent,
289
+ disclosureExpanded,
290
+ hasExpandedContent,
291
+ expandedTrailingInset,
292
+ };
293
+ });
294
+ };
225
295
  const snapshotRef = useRef(validatedSnapshot);
226
296
  const snapshotPropRef = useRef(validatedSnapshot);
227
297
  // An imperative snapshot survives host mounting until the snapshot prop changes.
@@ -229,7 +299,7 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
229
299
  snapshotPropRef.current = validatedSnapshot;
230
300
  snapshotRef.current = validatedSnapshot;
231
301
  acceptedSnapshotRef.current = validatedSnapshot;
232
- synchronizeSlots(snapshot);
302
+ synchronizeSlots(validatedSnapshot);
233
303
  }
234
304
  if (!initializedSnapshot.current) {
235
305
  acceptedSnapshotRef.current = validatedSnapshot;
@@ -498,9 +568,19 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
498
568
  );
499
569
  if (!useSlotLayout) return <View {...viewProps} ref={setHostRef} />;
500
570
  const padding = slotState.padding;
571
+ const footerBandStyle = { paddingHorizontal: padding };
572
+ const trailingInsetStyle = {
573
+ height: slotState.disclosureExpanded
574
+ ? slotState.expandedTrailingInset
575
+ : 0,
576
+ };
577
+ const hasFooterBand =
578
+ listFooter !== undefined ||
579
+ listExpandedContent !== undefined ||
580
+ listStructureToggle !== undefined;
501
581
  const content = (
502
582
  <View
503
- ref={setBoundsRef}
583
+ ref={sidecarLayout ? undefined : setBoundsRef}
504
584
  style={{ position: 'relative', flexShrink: 0, minWidth: 0 }}
505
585
  >
506
586
  {listHeader !== undefined ? (
@@ -517,14 +597,90 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
517
597
  {listEmpty}
518
598
  </View>
519
599
  ) : null}
520
- {listFooter !== undefined ? (
521
- <View style={{ paddingHorizontal: padding }}>{listFooter}</View>
600
+ {hasFooterBand ? (
601
+ <View testID="native-list-footer-slot" style={footerBandStyle}>
602
+ {listExpandedContent !== undefined &&
603
+ slotState.hasExpandedContent ? (
604
+ <View
605
+ testID="native-list-expanded-content-slot"
606
+ aria-hidden={!slotState.disclosureExpanded}
607
+ pointerEvents={slotState.disclosureExpanded ? 'auto' : 'none'}
608
+ style={
609
+ slotState.disclosureExpanded
610
+ ? undefined
611
+ : collapsedExpandedContentStyle
612
+ }
613
+ >
614
+ {listExpandedContent}
615
+ </View>
616
+ ) : null}
617
+ {listStructureToggle !== undefined ? (
618
+ <View
619
+ testID="native-list-structure-toggle-slot"
620
+ aria-hidden={!slotState.disclosurePresent}
621
+ pointerEvents={slotState.disclosurePresent ? 'auto' : 'none'}
622
+ style={
623
+ slotState.disclosurePresent
624
+ ? undefined
625
+ : collapsedExpandedContentStyle
626
+ }
627
+ >
628
+ {listStructureToggle}
629
+ </View>
630
+ ) : null}
631
+ {slotState.expandedTrailingInset > 0 &&
632
+ (listExpandedContent !== undefined ||
633
+ listStructureToggle !== undefined) ? (
634
+ <View
635
+ testID="native-list-trailing-inset-slot"
636
+ style={trailingInsetStyle}
637
+ />
638
+ ) : null}
639
+ {listFooter}
640
+ </View>
522
641
  ) : null}
523
642
  </View>
524
643
  );
644
+ const bandWidth = sidecarLayout
645
+ ? Math.min(viewportWidth, sidecarLayout.maxContentWidth)
646
+ : 0;
647
+ const sidecarMargin = Math.max(0, (viewportWidth - bandWidth) / 2);
648
+ const leftWidth = Math.max(
649
+ 0,
650
+ bandWidth - (sidecarLayout?.width ?? 0) - (sidecarLayout?.gap ?? 0)
651
+ );
652
+ const scrollContent = sidecarLayout ? (
653
+ <View
654
+ ref={setBoundsRef}
655
+ style={{ position: 'relative', minHeight: sidecarHeight }}
656
+ >
657
+ <View style={{ marginStart: sidecarMargin, width: leftWidth }}>
658
+ {content}
659
+ </View>
660
+ <View
661
+ style={{
662
+ position: 'absolute',
663
+ end: sidecarMargin,
664
+ top: 0,
665
+ width: sidecarLayout.width,
666
+ }}
667
+ onLayout={(event) =>
668
+ setSidecarHeight(event.nativeEvent.layout.height)
669
+ }
670
+ >
671
+ {listSidecar}
672
+ </View>
673
+ </View>
674
+ ) : (
675
+ content
676
+ );
525
677
  return (
526
678
  <View
527
679
  {...viewProps}
680
+ onLayout={(event) => {
681
+ setViewportWidth(event.nativeEvent.layout.width);
682
+ viewProps.onLayout?.(event);
683
+ }}
528
684
  style={[viewProps.style, { position: 'relative', minHeight: 0 }]}
529
685
  >
530
686
  {webScrollHost === undefined ? (
@@ -532,10 +688,10 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
532
688
  ref={setScrollHostRef}
533
689
  style={{ position: 'absolute', inset: 0, overflow: 'scroll' }}
534
690
  >
535
- {content}
691
+ {scrollContent}
536
692
  </View>
537
693
  ) : (
538
- content
694
+ scrollContent
539
695
  )}
540
696
  </View>
541
697
  );
package/src/index.ts CHANGED
@@ -14,10 +14,12 @@ export type {
14
14
  ScrollToKeyParams,
15
15
  ScrollToLocationParams,
16
16
  ScrollToOffsetParams,
17
+ StructuralTransitionSettledEvent,
17
18
  } from './NativeList';
18
19
  export type * from './models';
19
20
  export {
20
21
  applyRowPatches,
22
+ planNativeListPatches,
21
23
  serializePatches,
22
24
  serializeSnapshot,
23
25
  validatePatches,
package/src/models.ts CHANGED
@@ -185,6 +185,7 @@ export const ROW_BOX_STYLE_KEYS_BY_TYPE = {
185
185
  'trailingGap',
186
186
  'image',
187
187
  ],
188
+ buttonGroup: [],
188
189
  system: ['horizontalPadding', 'verticalPadding', 'lineGap'],
189
190
  } as const;
190
191
 
@@ -201,6 +202,7 @@ type TemplateBoxStyle<T extends keyof typeof ROW_BOX_STYLE_KEYS_BY_TYPE> = Pick<
201
202
  */
202
203
  export type IdentityRowStyle = TemplateBoxStyle<'identity'> &
203
204
  Readonly<{
205
+ leadingContentMaxWidthRatio?: number;
204
206
  title?: NativeListTextStyle;
205
207
  subtitle?: NativeListTextStyle;
206
208
  tertiary?: NativeListTextStyle;
@@ -279,8 +281,16 @@ export type SectionHeaderRowStyle = TemplateBoxStyle<'sectionHeader'> &
279
281
  title?: NativeListTextStyle;
280
282
  subtitle?: NativeListTextStyle;
281
283
  value?: NativeListTextStyle;
284
+ valuePlacement?: 'afterTitle';
285
+ titleDecoration?: 'none' | 'dashed';
282
286
  }>;
283
287
 
288
+ export type RowTextHelp = Readonly<{
289
+ actionKey: string;
290
+ presentation: 'tooltip' | 'popover';
291
+ underline?: 'dashed';
292
+ }>;
293
+
284
294
  export type ActionRowStyle = TemplateBoxStyle<'action'> &
285
295
  Readonly<{
286
296
  title?: NativeListTextStyle;
@@ -344,19 +354,27 @@ export type VisualOverlay = Readonly<{
344
354
  offsetX?: number;
345
355
  offsetY?: number;
346
356
  image?: ImageSource;
357
+ fallbackIcon?: Readonly<{ name: string; tintColor?: string }>;
347
358
  name?: string;
348
359
  text?: string;
360
+ fontWeight?: NativeListTextStyle['fontWeight'];
349
361
  tintColor?: string;
350
362
  backgroundColor?: string;
351
363
  }>;
352
364
 
353
365
  type VisualWithImage = Readonly<{
354
- fallbackIcon?: Readonly<{ name: string; tintColor?: string }>;
366
+ fallbackIcon?: Readonly<{
367
+ name: string;
368
+ tintColor?: string;
369
+ size?: number;
370
+ backgroundColor?: string;
371
+ }>;
355
372
  overlays?: readonly VisualOverlay[];
356
373
  borderStyle?: 'dashed';
357
374
  borderColor?: string;
358
375
  image?: ImageSource;
359
376
  fallbackText?: string;
377
+ fallbackTextColor?: string;
360
378
  backgroundColor?: string;
361
379
  shape?: 'circle' | 'rounded' | 'square';
362
380
  cornerIcon?: Readonly<{
@@ -408,9 +426,26 @@ export type TrailingAccessory =
408
426
  kind: 'valuePair';
409
427
  primary: string;
410
428
  secondary: string;
429
+ primaryFontWeight?: 'medium';
430
+ primaryHelp?: RowTextHelp;
431
+ primaryTextSegments?: readonly ValueTextSegment[];
432
+ secondaryTextSegments?: readonly ValueTextSegment[];
411
433
  primaryTone?: TextTone;
412
434
  secondaryTone?: TextTone;
413
435
  }>
436
+ | Readonly<{
437
+ kind: 'button';
438
+ label: string;
439
+ actionKey: string;
440
+ disabled?: boolean;
441
+ testID?: string;
442
+ accessibilityLabel?: string;
443
+ textColor?: string;
444
+ backgroundColor?: string;
445
+ pressedBackgroundColor?: string;
446
+ hoverBackgroundColor?: string;
447
+ focusRingColor?: string;
448
+ }>
414
449
  | Readonly<{
415
450
  kind: 'checkbox';
416
451
  state: CheckboxState;
@@ -438,6 +473,7 @@ export type TrailingAccessory =
438
473
  kind: 'icon';
439
474
  name: string;
440
475
  tintColor?: string;
476
+ buttonStyle?: 'tertiary';
441
477
  disabled?: boolean;
442
478
  actionKey?: string;
443
479
  testID?: string;
@@ -480,6 +516,10 @@ export type RowBase = Readonly<{
480
516
  export type IdentityRow = RowBase &
481
517
  Readonly<{
482
518
  type: 'identity';
519
+ /** Opt in to content-based height; explicit row/container height still wins. */
520
+ contentHeight?: 'auto';
521
+ /** Emits a row action instead of a press after a successful long press. */
522
+ longPressActionKey?: string;
483
523
  // OneKey patch: UTF-16 ranges match Fuse indices and native attributed strings.
484
524
  titleMatch?: readonly Readonly<{ start: number; end: number }>[];
485
525
  titleActionKey?: string;
@@ -489,11 +529,27 @@ export type IdentityRow = RowBase &
489
529
  leading: LeadingVisual;
490
530
  leadingAction?: Extract<TrailingAccessory, { kind: 'icon' }>;
491
531
  title: string;
532
+ /** Plain 12/16 labels beside the title, preserving independent tones. */
533
+ titleSuffixes?: readonly Readonly<{
534
+ text: string;
535
+ tone: 'secondary' | 'disabled';
536
+ maxWidth?: number;
537
+ }>[];
538
+ titleIcon?: Readonly<{
539
+ name: string;
540
+ tintColor?: string;
541
+ accessibilityLabel?: string;
542
+ /** Web-only anchored help from this icon; does not add a click action. */
543
+ hoverActionKey?: string;
544
+ }>;
545
+ /** Web-only additional help icon after titleIcon. */
546
+ secondaryTitleIcon?: IdentityRow['titleIcon'];
492
547
  subtitle?: string;
493
548
  tertiary?: string;
494
549
  tertiaryTone?: 'secondary' | 'info';
495
550
  titleLines?: 1 | 2;
496
- subtitleLines?: 1 | 2;
551
+ /** Zero displays the complete subtitle. */
552
+ subtitleLines?: 0 | 1 | 2;
497
553
  badges?: readonly BadgeModel[];
498
554
  trailing?: readonly TrailingAccessory[];
499
555
  draggable?: boolean;
@@ -534,6 +590,7 @@ export type ActivityRow = RowBase &
534
590
  Readonly<{
535
591
  type: 'activity';
536
592
  leading: LeadingVisual;
593
+ hideLeading?: boolean;
537
594
  secondaryLeading?: LeadingVisual;
538
595
  title: string;
539
596
  description?: string;
@@ -559,11 +616,14 @@ export type ActivityRow = RowBase &
559
616
  export type MessageRow = RowBase &
560
617
  Readonly<{
561
618
  type: 'message';
619
+ /** Draws only top and bottom container borders when a border is supplied. */
620
+ presentation?: 'inlineWarning';
562
621
  unread?: boolean;
563
622
  leading?: LeadingVisual;
564
623
  title: string;
565
624
  body: string;
566
- bodyLines?: 1 | 2 | 3;
625
+ /** Zero displays the complete body. */
626
+ bodyLines?: 0 | 1 | 2 | 3;
567
627
  time: string;
568
628
  thumbnail?: ImageSource;
569
629
  style?: MessageRowStyle;
@@ -572,8 +632,12 @@ export type MessageRow = RowBase &
572
632
  export type DataColumn = Readonly<{
573
633
  key: string;
574
634
  text: string;
635
+ textSegments?: readonly ValueTextSegment[];
636
+ titleIcon?: IdentityRow['titleIcon'];
637
+ secondaryTitleIcon?: IdentityRow['titleIcon'];
575
638
  secondaryLeadingText?: string;
576
639
  secondaryText?: string;
640
+ secondaryTextSegments?: readonly ValueTextSegment[];
577
641
  weight?: 1 | 2 | 3;
578
642
  alignment?: 'start' | 'center' | 'end';
579
643
  tone?: 'primary' | 'secondary' | 'positive' | 'negative';
@@ -585,6 +649,7 @@ export type DataRow = RowBase &
585
649
  type: 'dataRow';
586
650
  leading?: LeadingVisual;
587
651
  columns: readonly DataColumn[];
652
+ trailing?: readonly TrailingAccessory[];
588
653
  checkbox?: Extract<TrailingAccessory, { kind: 'checkbox' }>;
589
654
  index?: number;
590
655
  badge?: BadgeModel;
@@ -766,9 +831,11 @@ export type SectionHeaderRow = RowBase &
766
831
  indexTitle?: string;
767
832
  variant?: 'summary' | 'gallery' | 'history';
768
833
  title: string;
834
+ leadingBadge?: BadgeModel;
769
835
  titleLoading?: boolean;
770
836
  subtitle?: string;
771
837
  value?: string;
838
+ valueHelp?: RowTextHelp;
772
839
  valueActionKey?: string;
773
840
  titleIcon?: Extract<TrailingAccessory, { kind: 'icon' }>;
774
841
  valueIcon?: Extract<TrailingAccessory, { kind: 'icon' }>;
@@ -789,6 +856,21 @@ export type ActionRow = RowBase &
789
856
  style?: ActionRowStyle;
790
857
  }>;
791
858
 
859
+ export type ButtonGroupRow = RowBase &
860
+ Readonly<{
861
+ type: 'buttonGroup';
862
+ buttons: readonly Readonly<{
863
+ key: string;
864
+ label: string;
865
+ actionKey: string;
866
+ disabled?: boolean;
867
+ loading?: boolean;
868
+ testID?: string;
869
+ accessibilityLabel?: string;
870
+ }>[];
871
+ style?: TemplateBoxStyle<'buttonGroup'>;
872
+ }>;
873
+
792
874
  export type SystemRow = RowBase &
793
875
  Readonly<{ style?: SystemRowStyle }> &
794
876
  (
@@ -841,6 +923,7 @@ export type RowModel =
841
923
  | MetricCardRow
842
924
  | SectionHeaderRow
843
925
  | ActionRow
926
+ | ButtonGroupRow
844
927
  | SystemRow;
845
928
 
846
929
  export type NativeListTheme = Readonly<{
@@ -853,6 +936,8 @@ export type NativeListTheme = Readonly<{
853
936
  rowBackground: string;
854
937
  rowSelectedBackground: string;
855
938
  rowPressedBackground?: string;
939
+ /** Web-only enabled-row hover color; defaults to rowPressedBackground. */
940
+ rowHoverBackground?: string;
856
941
  subduedBackground?: string;
857
942
  strongBackground?: string;
858
943
  /** Web-only background for a displayed declared image fallback. */
@@ -865,6 +950,8 @@ export type NativeListTheme = Readonly<{
865
950
  separator: string;
866
951
  accent: string;
867
952
  positive: string;
953
+ /** Optional exact background for the success tone of section leading badges. */
954
+ successBackground?: string;
868
955
  negative: string;
869
956
  criticalBackground?: string;
870
957
  inverseBackground?: string;
@@ -911,6 +998,16 @@ export type NativeListListStyle = Readonly<{
911
998
  export type NativeListSnapshot = Readonly<{
912
999
  schemaVersion: 1;
913
1000
  generation: number;
1001
+ /** One-shot animation for a user-initiated row-key insertion or removal. */
1002
+ structuralTransition?: Readonly<{
1003
+ id: string;
1004
+ durationMs?: number;
1005
+ anchorKey?: string;
1006
+ }>;
1007
+ disclosure?: Readonly<{
1008
+ expanded: boolean;
1009
+ expandedTrailingInset?: number;
1010
+ }>;
914
1011
  layout: Readonly<{
915
1012
  kind: NativeListLayout;
916
1013
  orientation?: NativeListOrientation;
@@ -968,7 +1065,13 @@ export type RowPatch =
968
1065
  IdentityRow,
969
1066
  | CommonPatchFields
970
1067
  | 'title'
1068
+ | 'titleSuffixes'
1069
+ | 'titleIcon'
1070
+ | 'secondaryTitleIcon'
971
1071
  | 'subtitle'
1072
+ | 'subtitleLines'
1073
+ | 'contentHeight'
1074
+ | 'longPressActionKey'
972
1075
  | 'tertiary'
973
1076
  | 'tertiaryTone'
974
1077
  | 'badges'
@@ -1010,6 +1113,7 @@ export type RowPatch =
1010
1113
  | 'primaryAmount'
1011
1114
  | 'secondaryAmount'
1012
1115
  | 'leading'
1116
+ | 'hideLeading'
1013
1117
  | 'secondaryLeading'
1014
1118
  | 'footerActions'
1015
1119
  >
@@ -1026,6 +1130,7 @@ export type RowPatch =
1026
1130
  | 'title'
1027
1131
  | 'body'
1028
1132
  | 'bodyLines'
1133
+ | 'presentation'
1029
1134
  | 'time'
1030
1135
  | 'thumbnail'
1031
1136
  >
@@ -1040,6 +1145,7 @@ export type RowPatch =
1040
1145
  | CommonPatchFields
1041
1146
  | 'leading'
1042
1147
  | 'columns'
1148
+ | 'trailing'
1043
1149
  | 'checkbox'
1044
1150
  | 'index'
1045
1151
  | 'badge'
@@ -1124,6 +1230,8 @@ export type RowPatch =
1124
1230
  | 'title'
1125
1231
  | 'subtitle'
1126
1232
  | 'value'
1233
+ | 'valueHelp'
1234
+ | 'leadingBadge'
1127
1235
  | 'valueActionKey'
1128
1236
  | 'titleActionKey'
1129
1237
  | 'titleActionOnHover'
@@ -1148,6 +1256,11 @@ export type RowPatch =
1148
1256
  >
1149
1257
  >;
1150
1258
  }>
1259
+ | Readonly<{
1260
+ type: 'buttonGroup';
1261
+ key: string;
1262
+ changes: Partial<Pick<ButtonGroupRow, CommonPatchFields | 'buttons'>>;
1263
+ }>
1151
1264
  | Readonly<{
1152
1265
  type: 'system';
1153
1266
  key: string;
@@ -1164,6 +1277,7 @@ export type NativeListActionSource =
1164
1277
  | 'leadingAction'
1165
1278
  | 'trailingAccessory'
1166
1279
  | 'footerAction'
1280
+ | 'buttonGroup'
1167
1281
  | 'description'
1168
1282
  | 'mediaClose';
1169
1283
 
package/src/selection.ts CHANGED
@@ -39,6 +39,7 @@ export function isRowPressEnabled(row: RowModel): boolean {
39
39
  return (
40
40
  !row.disabled &&
41
41
  !row.pressDisabled &&
42
+ row.type !== 'buttonGroup' &&
42
43
  (row.type !== 'system' || row.variant === 'retry')
43
44
  );
44
45
  }