@onekeyfe/react-native-native-list 6.7.0-alpha.375 → 6.7.0-alpha.382

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
@@ -8,10 +8,16 @@ import { Platform, View } from 'react-native';
8
8
  import { callback, getHostComponent } from 'react-native-nitro-modules';
9
9
  import { isSelectableRow } from "./selection.js";
10
10
  import { normalizeIndexScroll, normalizeKeyScroll, normalizePositionScroll, resolveLocationIndex, scrollFailure, validateOffset } from "./scrolling.js";
11
- import { applyRowPatches, containNativeListInput, emptyNativeListSnapshot, serializePatches, validatePatches, validateSnapshot } from "./validation.js";
11
+ import { applyOwnedNativePatches, captureNativeListInput, containNativeListInput, emptyNativeListSnapshot, ownValidatedNativeSnapshot, validateSnapshot } from "./validation.js";
12
12
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
13
  const NativeListConfig = require('../nitrogen/generated/shared/json/NativeListConfig.json');
14
14
  const NativeListHost = getHostComponent('NativeList', () => NativeListConfig);
15
+ const clearedStructuralTransitionSettled = {
16
+ f: undefined
17
+ };
18
+ const disclosureSlotMeasurementStyle = {
19
+ alignSelf: 'stretch'
20
+ };
15
21
  export function preloadNativeListAvatarImages(sources) {
16
22
  return OneKeyImageCache.preload(sources.map(source => ({
17
23
  uri: source.uri,
@@ -31,6 +37,10 @@ export const NativeList = /*#__PURE__*/forwardRef(function NativeList({
31
37
  listHeader,
32
38
  listFooter,
33
39
  listEmpty,
40
+ listSidecar,
41
+ listExpandedContent,
42
+ listStructureToggle,
43
+ sidecarLayout,
34
44
  topOverlay,
35
45
  topScrollEdgeEffect,
36
46
  scrollPositionThresholds,
@@ -46,6 +56,7 @@ export const NativeList = /*#__PURE__*/forwardRef(function NativeList({
46
56
  onReorder,
47
57
  onEndReached,
48
58
  onVisibleRangeChanged,
59
+ onStructuralTransitionSettled,
49
60
  onScrollPositionThresholdChange,
50
61
  onRefresh,
51
62
  onScrollToIndexFailed,
@@ -56,21 +67,118 @@ export const NativeList = /*#__PURE__*/forwardRef(function NativeList({
56
67
  ...viewProps
57
68
  }, forwardedRef) {
58
69
  const snapshotRef = useRef(emptyNativeListSnapshot());
70
+ const renderedSnapshotRef = useRef(undefined);
59
71
  const initializedSnapshot = useRef(false);
60
- const hasContainerSlots = Boolean(listHeader || listEmpty || listFooter);
61
- const prepareSnapshot = useCallback(next => {
62
- const accepted = validateSnapshot(next);
72
+ const pendingDisclosureSnapshotRef = useRef(undefined);
73
+ const previousPropInputRef = useRef(inputSnapshot);
74
+ const [pendingDisclosureRevision, setPendingDisclosureRevision] = useState(0);
75
+ const disclosureSlotMounts = useRef({
76
+ expandedPresent: false,
77
+ togglePresent: false,
78
+ expandedEpoch: 0,
79
+ toggleEpoch: 0
80
+ });
81
+ const expandedPresent = listExpandedContent != null;
82
+ const togglePresent = listStructureToggle != null;
83
+ if (disclosureSlotMounts.current.expandedPresent !== expandedPresent) {
84
+ disclosureSlotMounts.current.expandedPresent = expandedPresent;
85
+ disclosureSlotMounts.current.expandedEpoch += 1;
86
+ }
87
+ if (disclosureSlotMounts.current.togglePresent !== togglePresent) {
88
+ disclosureSlotMounts.current.togglePresent = togglePresent;
89
+ disclosureSlotMounts.current.toggleEpoch += 1;
90
+ }
91
+ const expandedEpoch = disclosureSlotMounts.current.expandedEpoch;
92
+ const toggleEpoch = disclosureSlotMounts.current.toggleEpoch;
93
+ const [slotMeasurements, setSlotMeasurements] = useState({
94
+ heights: [0, 0, 0, 0, 0, 0],
95
+ readyEpochs: [-1, -1, -1, -1, -1, -1]
96
+ });
97
+ const hasSidecar = listSidecar !== undefined;
98
+ const sidecarWidth = sidecarLayout?.width;
99
+ const sidecarGap = sidecarLayout?.gap;
100
+ const sidecarMaxContentWidth = sidecarLayout?.maxContentWidth;
101
+ const hasContainerSlots = Boolean(listHeader || listEmpty || listFooter || listSidecar || listExpandedContent || listStructureToggle);
102
+ const validateContainerConfiguration = useCallback(accepted => {
63
103
  if (hasContainerSlots && accepted.layout.orientation === 'horizontal') throw new Error('NativeList container slots require vertical orientation');
64
- return accepted;
65
- }, [hasContainerSlots]);
66
- const snapshot = useMemo(() => containNativeListInput(() => prepareSnapshot(inputSnapshot)) ?? snapshotRef.current, [inputSnapshot, prepareSnapshot]);
104
+ if (hasSidecar !== (sidecarWidth !== undefined)) throw new Error('listSidecar and sidecarLayout must be supplied together');
105
+ if (sidecarWidth !== undefined) {
106
+ const width = sidecarWidth;
107
+ const gap = sidecarGap ?? NaN;
108
+ const maxContentWidth = sidecarMaxContentWidth ?? NaN;
109
+ if (accepted.layout.kind !== 'linear' || !Number.isFinite(width) || width <= 0 || !Number.isFinite(gap) || gap < 0 || !Number.isFinite(maxContentWidth) || maxContentWidth <= width + gap) throw new Error('Invalid NativeList sidecar layout');
110
+ }
111
+ }, [hasContainerSlots, hasSidecar, sidecarWidth, sidecarGap, sidecarMaxContentWidth]);
112
+ const prepareSnapshot = useCallback(next => {
113
+ const accepted = validateSnapshot(captureNativeListInput(next));
114
+ validateContainerConfiguration(accepted);
115
+ return ownValidatedNativeSnapshot(accepted);
116
+ }, [validateContainerConfiguration]);
117
+ const expandedReadyEpoch = slotMeasurements.readyEpochs[4];
118
+ const toggleReadyEpoch = slotMeasurements.readyEpochs[5];
119
+ const snapshot = useMemo(() => {
120
+ // Imperative pending snapshots use this revision to retry the readiness gate.
121
+ // eslint-disable-next-line no-void -- Reading this revision invalidates the readiness memo.
122
+ void pendingDisclosureRevision;
123
+ const propReplaced = previousPropInputRef.current !== inputSnapshot;
124
+ previousPropInputRef.current = inputSnapshot;
125
+ const pending = pendingDisclosureSnapshotRef.current;
126
+ const retained = pending && !propReplaced && (pending.source === 'imperative' || pending.input === inputSnapshot) && pending.expandedEpoch === expandedEpoch && pending.toggleEpoch === toggleEpoch ? pending : undefined;
127
+ const candidate = retained?.owned ?? (!propReplaced && initializedSnapshot.current ? renderedSnapshotRef.current ?? snapshotRef.current : containNativeListInput(() => prepareSnapshot(inputSnapshot)) ?? snapshotRef.current);
128
+ const acceptedCandidate = !propReplaced && initializedSnapshot.current ? containNativeListInput(() => {
129
+ validateContainerConfiguration(candidate);
130
+ return candidate;
131
+ }) : candidate;
132
+ if (!acceptedCandidate) {
133
+ pendingDisclosureSnapshotRef.current = undefined;
134
+ return renderedSnapshotRef.current ?? snapshotRef.current;
135
+ }
136
+ const transitionId = acceptedCandidate.structuralTransition?.id;
137
+ const newDisclosureCommand = initializedSnapshot.current && acceptedCandidate.disclosure !== undefined && transitionId !== undefined && transitionId !== snapshotRef.current.structuralTransition?.id;
138
+ if (newDisclosureCommand && (expandedPresent && expandedReadyEpoch !== expandedEpoch || togglePresent && toggleReadyEpoch !== toggleEpoch)) {
139
+ pendingDisclosureSnapshotRef.current = retained ?? {
140
+ source: 'prop',
141
+ input: inputSnapshot,
142
+ owned: acceptedCandidate,
143
+ expandedEpoch,
144
+ toggleEpoch
145
+ };
146
+ return snapshotRef.current;
147
+ }
148
+ pendingDisclosureSnapshotRef.current = undefined;
149
+ return acceptedCandidate;
150
+ }, [inputSnapshot, prepareSnapshot, validateContainerConfiguration, expandedPresent, togglePresent, expandedEpoch, toggleEpoch, expandedReadyEpoch, toggleReadyEpoch, pendingDisclosureRevision]);
151
+ renderedSnapshotRef.current = snapshot;
67
152
  const fontFamiliesJson = useNativeListFontFamiliesJson();
68
- const [slotHeights, setSlotHeights] = useState([0, 0, 0]);
153
+ const slotHeights = slotMeasurements.heights;
154
+ const [viewportWidth, setViewportWidth] = useState(0);
69
155
  const [topOverlayHeight, setTopOverlayHeight] = useState(0);
70
156
  const hasTopOverlay = Platform?.OS === 'ios' && parseInt(String(Platform.Version), 10) >= 26 && (topScrollEdgeEffect === 'hard' || topScrollEdgeEffect === 'soft') && topOverlay != null;
71
157
  const containerSlotHeightsJson = JSON.stringify(slotHeights);
72
- const slotInset = snapshot.layout.contentPaddingHorizontal ?? snapshot.layout.contentPadding ?? 0;
73
- const slotContents = [listHeader, listEmpty, listFooter];
158
+ const defaultSlotInset = snapshot.layout.contentPaddingHorizontal ?? snapshot.layout.contentPadding ?? 0;
159
+ const bandWidth = sidecarLayout ? Math.min(viewportWidth, sidecarLayout.maxContentWidth) : 0;
160
+ const slotInset = sidecarLayout ? Math.max(0, (viewportWidth - bandWidth) / 2) + defaultSlotInset : defaultSlotInset;
161
+ const sidecarSpace = sidecarLayout ? sidecarLayout.width + sidecarLayout.gap : 0;
162
+ const slotContents = [listHeader, listEmpty, listFooter, listSidecar, listExpandedContent, listStructureToggle];
163
+ const recordSlotLayout = (index, height, epoch) => {
164
+ if (index === 4 && epoch !== disclosureSlotMounts.current.expandedEpoch || index === 5 && epoch !== disclosureSlotMounts.current.toggleEpoch) {
165
+ return;
166
+ }
167
+ setSlotMeasurements(previous => {
168
+ if (previous.heights[index] === height && previous.readyEpochs[index] === epoch) {
169
+ return previous;
170
+ }
171
+ const heights = [...previous.heights];
172
+ const readyEpochs = [...previous.readyEpochs];
173
+ heights[index] = height;
174
+ readyEpochs[index] = epoch;
175
+ return {
176
+ heights,
177
+ readyEpochs
178
+ };
179
+ });
180
+ };
181
+ const sidecarLayoutJson = sidecarLayout ? JSON.stringify(sidecarLayout) : '';
74
182
  const normalizedKeyboardShouldPersistTaps = keyboardShouldPersistTaps === true ? 'always' : keyboardShouldPersistTaps === false ? 'never' : keyboardShouldPersistTaps;
75
183
  const nativeRef = useRef(null);
76
184
  const callbacksRef = useRef({
@@ -80,6 +188,7 @@ export const NativeList = /*#__PURE__*/forwardRef(function NativeList({
80
188
  onReorder,
81
189
  onEndReached,
82
190
  onVisibleRangeChanged,
191
+ onStructuralTransitionSettled,
83
192
  onScrollPositionThresholdChange,
84
193
  onRefresh,
85
194
  onScrollToIndexFailed
@@ -91,6 +200,7 @@ export const NativeList = /*#__PURE__*/forwardRef(function NativeList({
91
200
  onReorder,
92
201
  onEndReached,
93
202
  onVisibleRangeChanged,
203
+ onStructuralTransitionSettled,
94
204
  onScrollPositionThresholdChange,
95
205
  onRefresh,
96
206
  onScrollToIndexFailed
@@ -191,6 +301,20 @@ export const NativeList = /*#__PURE__*/forwardRef(function NativeList({
191
301
  applySnapshot(nextSnapshot) {
192
302
  const accepted = containNativeListInput(() => prepareSnapshot(nextSnapshot));
193
303
  if (!accepted) return;
304
+ const transitionId = accepted.structuralTransition?.id;
305
+ const waitsForSlots = accepted.disclosure !== undefined && transitionId !== undefined && transitionId !== snapshotRef.current.structuralTransition?.id && (expandedPresent && slotMeasurements.readyEpochs[4] !== expandedEpoch || togglePresent && slotMeasurements.readyEpochs[5] !== toggleEpoch);
306
+ if (waitsForSlots) {
307
+ pendingDisclosureSnapshotRef.current = {
308
+ source: 'imperative',
309
+ input: nextSnapshot,
310
+ owned: accepted,
311
+ expandedEpoch,
312
+ toggleEpoch
313
+ };
314
+ setPendingDisclosureRevision(value => value + 1);
315
+ return;
316
+ }
317
+ pendingDisclosureSnapshotRef.current = undefined;
194
318
  const nextSnapshotJson = JSON.stringify(accepted);
195
319
  nativeRef.current?.applySnapshot(nextSnapshotJson);
196
320
  snapshotRef.current = accepted;
@@ -200,16 +324,17 @@ export const NativeList = /*#__PURE__*/forwardRef(function NativeList({
200
324
  }
201
325
  },
202
326
  applyPatches(patches) {
203
- const accepted = containNativeListInput(() => {
204
- const normalized = validatePatches(patches);
205
- return {
206
- patches: normalized,
207
- snapshot: applyRowPatches(snapshotRef.current, normalized),
208
- json: serializePatches(normalized)
209
- };
210
- });
327
+ const pending = pendingDisclosureSnapshotRef.current;
328
+ const accepted = containNativeListInput(() => applyOwnedNativePatches(pending?.owned ?? snapshotRef.current, patches));
211
329
  if (!accepted || accepted.patches.length === 0) return;
212
330
  const nextSnapshot = accepted.snapshot;
331
+ if (pending) {
332
+ pendingDisclosureSnapshotRef.current = {
333
+ ...pending,
334
+ owned: nextSnapshot
335
+ };
336
+ return;
337
+ }
213
338
  const native = nativeRef.current;
214
339
  const dispatch = native ? () => {
215
340
  native.applyPatches(accepted.json);
@@ -222,16 +347,22 @@ export const NativeList = /*#__PURE__*/forwardRef(function NativeList({
222
347
  if (avatarModelRef.current?.applyPatches(accepted.patches, dispatch)) updateAvatarPrefetchRef.current();
223
348
  },
224
349
  reconcileSelection(selectedKeys) {
225
- nativeRef.current?.reconcileSelection(JSON.stringify(selectedKeys));
226
- const current = snapshotRef.current;
227
- if (current.selection && (current.selection.mode !== 'single' || selectedKeys.length <= 1) && selectedKeys.every(key => current.rows.some(row => row.key === key && isSelectableRow(row)))) {
228
- snapshotRef.current = {
350
+ const capturedKeys = captureNativeListInput(selectedKeys);
351
+ const pending = pendingDisclosureSnapshotRef.current;
352
+ if (!pending) nativeRef.current?.reconcileSelection(JSON.stringify(capturedKeys));
353
+ const current = pending?.owned ?? snapshotRef.current;
354
+ if (current.selection && (current.selection.mode !== 'single' || capturedKeys.length <= 1) && capturedKeys.every(key => current.rows.some(row => row.key === key && isSelectableRow(row)))) {
355
+ const next = {
229
356
  ...current,
230
357
  selection: {
231
358
  ...current.selection,
232
- selectedKeys
359
+ selectedKeys: capturedKeys
233
360
  }
234
361
  };
362
+ if (pending) pendingDisclosureSnapshotRef.current = {
363
+ ...pending,
364
+ owned: next
365
+ };else snapshotRef.current = next;
235
366
  }
236
367
  },
237
368
  scrollToKey(paramsOrKey, animated, alignment) {
@@ -338,14 +469,23 @@ export const NativeList = /*#__PURE__*/forwardRef(function NativeList({
338
469
  };
339
470
  updateAvatarPrefetchRef.current();
340
471
  callbacksRef.current.onVisibleRangeChanged?.(payload);
472
+ }),
473
+ onStructuralTransitionSettled: callback(payloadJson => {
474
+ callbacksRef.current.onStructuralTransitionSettled?.(parsePayload(payloadJson));
341
475
  })
342
476
  }), []);
343
477
  return /*#__PURE__*/_jsxs(NativeListHost, {
344
478
  ...viewProps,
479
+ onLayout: event => {
480
+ setViewportWidth(event.nativeEvent.layout.width);
481
+ viewProps.onLayout?.(event);
482
+ },
345
483
  ...nativeCallbacks,
484
+ onStructuralTransitionSettled: onStructuralTransitionSettled ? nativeCallbacks.onStructuralTransitionSettled : clearedStructuralTransitionSettled,
346
485
  fontFamiliesJson: fontFamiliesJson,
347
486
  snapshotJson: snapshotJson,
348
487
  containerSlotHeightsJson: containerSlotHeightsJson,
488
+ sidecarLayoutJson: sidecarLayoutJson,
349
489
  topScrollEdgeEffect: hasTopOverlay ? topScrollEdgeEffect ?? 'none' : 'none',
350
490
  topOverlayHeight: hasTopOverlay ? topOverlayHeight : 0,
351
491
  scrollPositionThresholdsJson: scrollPositionThresholdsJson,
@@ -355,15 +495,22 @@ export const NativeList = /*#__PURE__*/forwardRef(function NativeList({
355
495
  collapsable: false,
356
496
  style: {
357
497
  position: 'absolute',
358
- left: slotInset,
359
- right: slotInset,
498
+ start: index === 3 ? 0 : slotInset,
499
+ end: index === 3 ? undefined : slotInset + sidecarSpace,
500
+ width: index === 3 ? sidecarLayout?.width : undefined,
360
501
  top: 0
361
502
  },
362
503
  onLayout: event => {
363
- const height = Math.ceil(event.nativeEvent.layout.height);
364
- setSlotHeights(previous => previous[index] === height ? previous : previous.map((value, slot) => slot === index ? height : value));
504
+ if (index < 4) {
505
+ recordSlotLayout(index, Math.ceil(event.nativeEvent.layout.height), 0);
506
+ }
365
507
  },
366
- children: content
508
+ children: index >= 4 ? /*#__PURE__*/_jsx(View, {
509
+ collapsable: false,
510
+ style: disclosureSlotMeasurementStyle,
511
+ onLayout: event => recordSlotLayout(index, Math.ceil(event.nativeEvent.layout.height), index === 4 ? expandedEpoch : toggleEpoch),
512
+ children: content
513
+ }, index === 4 ? `expanded-${expandedEpoch}` : `toggle-${toggleEpoch}`) : content
367
514
  }, index)), hasTopOverlay ? /*#__PURE__*/_jsx(View, {
368
515
  collapsable: false,
369
516
  style: {
@@ -8,6 +8,9 @@ import { applyRowPatches, containNativeListInput, emptyNativeListSnapshot, valid
8
8
  import { NativeListWebEngine } from "./web/NativeListWebEngine.js";
9
9
  import { acquireNativeListAvatar, canonicalNativeListAvatarUri } from "./web/NativeListWebAvatarCache.js";
10
10
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
+ const collapsedExpandedContentStyle = {
12
+ display: 'none'
13
+ };
11
14
  export function preloadNativeListAvatarImages(sources) {
12
15
  if (typeof document === 'undefined') return Promise.resolve(false);
13
16
  const uris = [...new Set(sources.map(source => canonicalNativeListAvatarUri(source.uri)).filter(uri => uri !== undefined))];
@@ -26,6 +29,10 @@ export const NativeList = /*#__PURE__*/forwardRef(function NativeList({
26
29
  listHeader,
27
30
  listEmpty,
28
31
  listFooter,
32
+ listExpandedContent,
33
+ listStructureToggle,
34
+ listSidecar,
35
+ sidecarLayout,
29
36
  topOverlay: _topOverlay,
30
37
  topScrollEdgeEffect: _topScrollEdgeEffect,
31
38
  webScrollHost,
@@ -42,6 +49,7 @@ export const NativeList = /*#__PURE__*/forwardRef(function NativeList({
42
49
  onReorder,
43
50
  onEndReached,
44
51
  onVisibleRangeChanged,
52
+ onStructuralTransitionSettled: _onStructuralTransitionSettled,
45
53
  onRefresh,
46
54
  onScrollToIndexFailed,
47
55
  initialScrollIndex,
@@ -56,7 +64,14 @@ export const NativeList = /*#__PURE__*/forwardRef(function NativeList({
56
64
  if (!Number.isFinite(inputStickyInset) || inputStickyInset < 0) throw new Error('webStickyInset must be finite and non-negative');
57
65
  return inputStickyInset;
58
66
  }) ?? 0, [inputStickyInset]);
59
- const preparedSnapshot = useMemo(() => containNativeListInput(() => validateSnapshot(inputSnapshot)), [inputSnapshot]);
67
+ // Freshness is limited to this component invocation; later renders revalidate.
68
+ let freshlyValidatedSnapshot;
69
+ const preparedSnapshot = useMemo(() => {
70
+ const prepared = containNativeListInput(() => validateSnapshot(inputSnapshot));
71
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- The value is intentionally render-local.
72
+ freshlyValidatedSnapshot = prepared;
73
+ return prepared;
74
+ }, [inputSnapshot]);
60
75
  const snapshot = preparedSnapshot ?? acceptedSnapshotRef.current;
61
76
  const fontFamiliesJson = useNativeListFontFamiliesJson();
62
77
  const fontFamiliesJsonRef = useRef(fontFamiliesJson);
@@ -64,29 +79,18 @@ export const NativeList = /*#__PURE__*/forwardRef(function NativeList({
64
79
  const hasEmptySlot = listEmpty !== undefined;
65
80
  const hasEmptySlotRef = useRef(hasEmptySlot);
66
81
  hasEmptySlotRef.current = hasEmptySlot;
67
- const [slotState, setSlotState] = useState(() => ({
68
- hasRows: snapshot.rows.length > 0,
69
- emptyBottomPadding: !snapshot.rows.length && hasEmptySlot ? snapshot.layout.contentPaddingBottom ?? snapshot.layout.contentPadding ?? 0 : 0,
70
- padding: snapshot.layout.contentPaddingHorizontal ?? snapshot.layout.contentPadding ?? 0
71
- }));
72
- const synchronizeSlots = next => {
73
- const hasRows = next.rows.length > 0;
74
- const padding = next.layout.contentPaddingHorizontal ?? next.layout.contentPadding ?? 0;
75
- const emptyBottomPadding = !hasRows && hasEmptySlotRef.current ? next.layout.contentPaddingBottom ?? next.layout.contentPadding ?? 0 : 0;
76
- setSlotState(previous => previous.hasRows === hasRows && previous.padding === padding && previous.emptyBottomPadding === emptyBottomPadding ? previous : {
77
- hasRows,
78
- padding,
79
- emptyBottomPadding
80
- });
81
- };
82
- const normalizeWebSnapshot = (next, hasReactEmpty = hasEmptySlotRef.current, hasSlotLayout = useSlotLayoutRef.current) => {
82
+ const [viewportWidth, setViewportWidth] = useState(0);
83
+ const [sidecarHeight, setSidecarHeight] = useState(0);
84
+ const normalizeWebSnapshot = (next, hasReactEmpty = hasEmptySlotRef.current, hasSlotLayout = useSlotLayoutRef.current, validatedInThisRender = false) => {
83
85
  if (hasSlotLayout && next.layout.orientation === 'horizontal') throw new Error('Web parent scrolling and container slots support vertical lists only');
84
86
  const emptySlot = hasReactEmpty && next.rows.length === 0;
85
- const validated = validateSnapshot(emptySlot ? {
87
+ const normalized = emptySlot && next.emptyState !== undefined ? {
86
88
  ...next,
87
89
  emptyState: undefined
88
- } : next);
89
- return emptySlot ? {
90
+ } : next;
91
+ const validated = validatedInThisRender ? normalized : validateSnapshot(normalized);
92
+ // Preserve an already normalized fallback's identity across rerenders.
93
+ return emptySlot && validated.layout.contentPaddingBottom !== 0 ? {
90
94
  ...validated,
91
95
  layout: {
92
96
  ...validated.layout,
@@ -99,12 +103,56 @@ export const NativeList = /*#__PURE__*/forwardRef(function NativeList({
99
103
  const [ownedScrollHost, setOwnedScrollHost] = useState(null);
100
104
  const useSlotLayoutRef = useRef(false);
101
105
  useSlotLayoutRef.current ||= webScrollHost !== undefined || listHeader !== undefined || listEmpty !== undefined || listFooter !== undefined;
106
+ useSlotLayoutRef.current ||= listSidecar !== undefined;
107
+ useSlotLayoutRef.current ||= listExpandedContent !== undefined || listStructureToggle !== undefined;
102
108
  const useSlotLayout = useSlotLayoutRef.current;
103
109
  const resolvedScrollHost = webScrollHost === undefined ? useSlotLayout ? ownedScrollHost : undefined : webScrollHost;
104
110
  const engineRef = useRef(undefined);
105
111
  const webVirtualizationEnabledRef = useRef(webVirtualizationEnabled);
106
112
  webVirtualizationEnabledRef.current = webVirtualizationEnabled;
107
- const validatedSnapshot = useMemo(() => containNativeListInput(() => normalizeWebSnapshot(snapshot, hasEmptySlot, useSlotLayout)) ?? acceptedSnapshotRef.current, [snapshot, hasEmptySlot, useSlotLayout]);
113
+ const validatedSnapshot = useMemo(() => containNativeListInput(() => {
114
+ if (listSidecar !== undefined !== (sidecarLayout !== undefined)) throw new Error('listSidecar and sidecarLayout must be supplied together');
115
+ if (sidecarLayout) {
116
+ const {
117
+ width,
118
+ gap,
119
+ maxContentWidth
120
+ } = sidecarLayout;
121
+ if (snapshot.layout.kind !== 'linear' || !Number.isFinite(width) || width <= 0 || !Number.isFinite(gap) || gap < 0 || !Number.isFinite(maxContentWidth) || maxContentWidth <= width + gap) throw new Error('Invalid NativeList sidecar layout');
122
+ }
123
+ return normalizeWebSnapshot(snapshot, hasEmptySlot, useSlotLayout, preparedSnapshot !== undefined && preparedSnapshot === freshlyValidatedSnapshot);
124
+ }) ?? acceptedSnapshotRef.current,
125
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- Preserve the existing memo dependencies; freshness is render-local.
126
+ [snapshot, hasEmptySlot, useSlotLayout, listSidecar, sidecarLayout]);
127
+ const [slotState, setSlotState] = useState(() => ({
128
+ hasRows: validatedSnapshot.rows.length > 0,
129
+ emptyBottomPadding: !validatedSnapshot.rows.length && hasEmptySlot ? validatedSnapshot.layout.contentPaddingBottom ?? validatedSnapshot.layout.contentPadding ?? 0 : 0,
130
+ padding: validatedSnapshot.layout.contentPaddingHorizontal ?? validatedSnapshot.layout.contentPadding ?? 0,
131
+ disclosurePresent: validatedSnapshot.disclosure !== undefined,
132
+ disclosureExpanded: validatedSnapshot.disclosure?.expanded === true,
133
+ hasExpandedContent: validatedSnapshot.disclosure?.expanded === true,
134
+ expandedTrailingInset: validatedSnapshot.disclosure?.expandedTrailingInset ?? 0
135
+ }));
136
+ const synchronizeSlots = next => {
137
+ const hasRows = next.rows.length > 0;
138
+ const padding = next.layout.contentPaddingHorizontal ?? next.layout.contentPadding ?? 0;
139
+ const emptyBottomPadding = !hasRows && hasEmptySlotRef.current ? next.layout.contentPaddingBottom ?? next.layout.contentPadding ?? 0 : 0;
140
+ const disclosurePresent = next.disclosure !== undefined;
141
+ const disclosureExpanded = next.disclosure?.expanded === true;
142
+ const expandedTrailingInset = next.disclosure?.expandedTrailingInset ?? 0;
143
+ setSlotState(previous => {
144
+ const hasExpandedContent = previous.hasExpandedContent || disclosureExpanded;
145
+ return previous.hasRows === hasRows && previous.padding === padding && previous.emptyBottomPadding === emptyBottomPadding && previous.disclosurePresent === disclosurePresent && previous.disclosureExpanded === disclosureExpanded && previous.hasExpandedContent === hasExpandedContent && previous.expandedTrailingInset === expandedTrailingInset ? previous : {
146
+ hasRows,
147
+ padding,
148
+ emptyBottomPadding,
149
+ disclosurePresent,
150
+ disclosureExpanded,
151
+ hasExpandedContent,
152
+ expandedTrailingInset
153
+ };
154
+ });
155
+ };
108
156
  const snapshotRef = useRef(validatedSnapshot);
109
157
  const snapshotPropRef = useRef(validatedSnapshot);
110
158
  // An imperative snapshot survives host mounting until the snapshot prop changes.
@@ -112,7 +160,7 @@ export const NativeList = /*#__PURE__*/forwardRef(function NativeList({
112
160
  snapshotPropRef.current = validatedSnapshot;
113
161
  snapshotRef.current = validatedSnapshot;
114
162
  acceptedSnapshotRef.current = validatedSnapshot;
115
- synchronizeSlots(snapshot);
163
+ synchronizeSlots(validatedSnapshot);
116
164
  }
117
165
  if (!initializedSnapshot.current) {
118
166
  acceptedSnapshotRef.current = validatedSnapshot;
@@ -296,8 +344,15 @@ export const NativeList = /*#__PURE__*/forwardRef(function NativeList({
296
344
  ref: setHostRef
297
345
  });
298
346
  const padding = slotState.padding;
347
+ const footerBandStyle = {
348
+ paddingHorizontal: padding
349
+ };
350
+ const trailingInsetStyle = {
351
+ height: slotState.disclosureExpanded ? slotState.expandedTrailingInset : 0
352
+ };
353
+ const hasFooterBand = listFooter !== undefined || listExpandedContent !== undefined || listStructureToggle !== undefined;
299
354
  const content = /*#__PURE__*/_jsxs(View, {
300
- ref: setBoundsRef,
355
+ ref: sidecarLayout ? undefined : setBoundsRef,
301
356
  style: {
302
357
  position: 'relative',
303
358
  flexShrink: 0,
@@ -320,15 +375,59 @@ export const NativeList = /*#__PURE__*/forwardRef(function NativeList({
320
375
  paddingBottom: slotState.emptyBottomPadding
321
376
  },
322
377
  children: listEmpty
323
- }) : null, listFooter !== undefined ? /*#__PURE__*/_jsx(View, {
324
- style: {
325
- paddingHorizontal: padding
326
- },
327
- children: listFooter
378
+ }) : null, hasFooterBand ? /*#__PURE__*/_jsxs(View, {
379
+ testID: "native-list-footer-slot",
380
+ style: footerBandStyle,
381
+ children: [listExpandedContent !== undefined && slotState.hasExpandedContent ? /*#__PURE__*/_jsx(View, {
382
+ testID: "native-list-expanded-content-slot",
383
+ "aria-hidden": !slotState.disclosureExpanded,
384
+ pointerEvents: slotState.disclosureExpanded ? 'auto' : 'none',
385
+ style: slotState.disclosureExpanded ? undefined : collapsedExpandedContentStyle,
386
+ children: listExpandedContent
387
+ }) : null, listStructureToggle !== undefined ? /*#__PURE__*/_jsx(View, {
388
+ testID: "native-list-structure-toggle-slot",
389
+ "aria-hidden": !slotState.disclosurePresent,
390
+ pointerEvents: slotState.disclosurePresent ? 'auto' : 'none',
391
+ style: slotState.disclosurePresent ? undefined : collapsedExpandedContentStyle,
392
+ children: listStructureToggle
393
+ }) : null, slotState.expandedTrailingInset > 0 && (listExpandedContent !== undefined || listStructureToggle !== undefined) ? /*#__PURE__*/_jsx(View, {
394
+ testID: "native-list-trailing-inset-slot",
395
+ style: trailingInsetStyle
396
+ }) : null, listFooter]
328
397
  }) : null]
329
398
  });
399
+ const bandWidth = sidecarLayout ? Math.min(viewportWidth, sidecarLayout.maxContentWidth) : 0;
400
+ const sidecarMargin = Math.max(0, (viewportWidth - bandWidth) / 2);
401
+ const leftWidth = Math.max(0, bandWidth - (sidecarLayout?.width ?? 0) - (sidecarLayout?.gap ?? 0));
402
+ const scrollContent = sidecarLayout ? /*#__PURE__*/_jsxs(View, {
403
+ ref: setBoundsRef,
404
+ style: {
405
+ position: 'relative',
406
+ minHeight: sidecarHeight
407
+ },
408
+ children: [/*#__PURE__*/_jsx(View, {
409
+ style: {
410
+ marginStart: sidecarMargin,
411
+ width: leftWidth
412
+ },
413
+ children: content
414
+ }), /*#__PURE__*/_jsx(View, {
415
+ style: {
416
+ position: 'absolute',
417
+ end: sidecarMargin,
418
+ top: 0,
419
+ width: sidecarLayout.width
420
+ },
421
+ onLayout: event => setSidecarHeight(event.nativeEvent.layout.height),
422
+ children: listSidecar
423
+ })]
424
+ }) : content;
330
425
  return /*#__PURE__*/_jsx(View, {
331
426
  ...viewProps,
427
+ onLayout: event => {
428
+ setViewportWidth(event.nativeEvent.layout.width);
429
+ viewProps.onLayout?.(event);
430
+ },
332
431
  style: [viewProps.style, {
333
432
  position: 'relative',
334
433
  minHeight: 0
@@ -340,8 +439,8 @@ export const NativeList = /*#__PURE__*/forwardRef(function NativeList({
340
439
  inset: 0,
341
440
  overflow: 'scroll'
342
441
  },
343
- children: content
344
- }) : content
442
+ children: scrollContent
443
+ }) : scrollContent
345
444
  });
346
445
  });
347
446
  //# sourceMappingURL=NativeList.web.js.map
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  export { NativeList, preloadNativeListAvatarImages } from './NativeList';
4
- export { applyRowPatches, serializePatches, serializeSnapshot, validatePatches, validateSnapshot } from "./validation.js";
4
+ export { applyRowPatches, planNativeListPatches, serializePatches, serializeSnapshot, validatePatches, validateSnapshot } from "./validation.js";
5
5
  export { checkboxStateForKeys, checkboxStateForSection, isSelectableRow, reduceSelection, selectionStateFromSnapshot } from "./selection.js";
6
6
  export { configureNativeListFonts } from "./fonts.js";
7
7
  export { ScrollEdgeEffect } from "./ScrollEdgeEffect.js";
@@ -24,6 +24,7 @@ export const ROW_BOX_STYLE_KEYS_BY_TYPE = {
24
24
  metricCard: ['horizontalPadding', 'verticalPadding', 'leadingGap', 'lineGap', 'image'],
25
25
  sectionHeader: ['horizontalPadding', 'verticalPadding', 'lineGap', 'trailingGap'],
26
26
  action: ['horizontalPadding', 'verticalPadding', 'leadingGap', 'trailingGap', 'image'],
27
+ buttonGroup: [],
27
28
  system: ['horizontalPadding', 'verticalPadding', 'lineGap']
28
29
  };
29
30
 
@@ -4,7 +4,7 @@ export function isSelectableRow(row) {
4
4
  return !row.disabled && (row.type === 'identity' || row.type === 'rail' || row.type === 'activity' || row.type === 'message' || row.type === 'dataRow' || row.type === 'mediaTile' || row.type === 'metricCard');
5
5
  }
6
6
  export function isRowPressEnabled(row) {
7
- return !row.disabled && !row.pressDisabled && (row.type !== 'system' || row.variant === 'retry');
7
+ return !row.disabled && !row.pressDisabled && row.type !== 'buttonGroup' && (row.type !== 'system' || row.variant === 'retry');
8
8
  }
9
9
  function deltaBetween(before, after, source, sourceKey) {
10
10
  return {