@onekeyfe/react-native-pager-view 6.7.0-2 → 6.7.0-21

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 (43) hide show
  1. package/README.md +14 -0
  2. package/android/src/androidTest/AndroidManifest.xml +7 -0
  3. package/android/src/androidTest/java/com/reactnativepagerview/CollapsiblePagerTabReservationTest.kt +306 -0
  4. package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerHost.kt +130 -30
  5. package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerNativeHeaders.kt +72 -7
  6. package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerViewManager.kt +27 -0
  7. package/android/src/main/java/com/reactnativepagerview/NativeScrollerScrollAwayInsets.kt +19 -0
  8. package/android/src/main/java/com/reactnativepagerview/NativeScrollerView.kt +50 -6
  9. package/android/src/main/java/com/reactnativepagerview/NestedScrollableHost.kt +8 -2
  10. package/android/src/test/java/com/reactnativepagerview/NativeScrollerScrollAwayInsetsTest.kt +40 -0
  11. package/docs/SPEC.md +257 -10
  12. package/ios/RNCCollapsiblePagerViewComponentView.mm +614 -42
  13. package/ios/RNCNativeScrollerComponentView.h +2 -0
  14. package/ios/RNCNativeScrollerComponentView.mm +42 -0
  15. package/ios/tests/RNCCollapsiblePagerFirstFrameTests.mm +403 -1
  16. package/ios/tests/RNCCollapsiblePagerHitTestTests.mm +338 -0
  17. package/ios/tests/RNCCollapsiblePagerPressCancellationTests.mm +74 -9
  18. package/ios/tests/RNCCollapsiblePagerRefreshControlTests.mm +50 -0
  19. package/lib/module/CollapsiblePagerView.js +32 -7
  20. package/lib/module/CollapsiblePagerView.web.js +439 -39
  21. package/lib/module/CollapsiblePagerViewNativeComponent.ts +13 -0
  22. package/lib/module/NativeScroller.web.js +16 -3
  23. package/lib/module/WebCollapsiblePagerPage.js +182 -0
  24. package/lib/module/index.js +2 -0
  25. package/lib/module/index.native.js +9 -0
  26. package/lib/module/index.web.js +2 -0
  27. package/lib/typescript/src/CollapsiblePagerView.d.ts +10 -3
  28. package/lib/typescript/src/CollapsiblePagerView.web.d.ts +66 -3
  29. package/lib/typescript/src/CollapsiblePagerViewNativeComponent.d.ts +5 -0
  30. package/lib/typescript/src/WebCollapsiblePagerPage.d.ts +42 -0
  31. package/lib/typescript/src/index.d.ts +4 -0
  32. package/lib/typescript/src/index.native.d.ts +13 -0
  33. package/lib/typescript/src/index.web.d.ts +4 -0
  34. package/package.json +6 -4
  35. package/react-native-pager-view.podspec +1 -0
  36. package/src/CollapsiblePagerView.tsx +99 -8
  37. package/src/CollapsiblePagerView.web.tsx +657 -51
  38. package/src/CollapsiblePagerViewNativeComponent.ts +13 -0
  39. package/src/NativeScroller.web.tsx +24 -2
  40. package/src/WebCollapsiblePagerPage.tsx +258 -0
  41. package/src/index.native.tsx +30 -0
  42. package/src/index.tsx +4 -0
  43. package/src/index.web.tsx +4 -0
package/docs/SPEC.md CHANGED
@@ -14,17 +14,32 @@ valid prop values. Both the style and overlay are
14
14
  disabled by default. On iOS 26 or newer, a local overlay remains fixed at the
15
15
  top of the viewport while the owned vertical `UIScrollView` can draw behind it.
16
16
  The component measures the overlay's Fabric layout height and owns the
17
- corresponding top content inset. It binds UIKit's
17
+ corresponding top content inset.
18
+ For callers with a known initial height, `topOverlayHeightHint` reserves that
19
+ height on the first frame after the overlay is enabled, before Fabric delivers
20
+ `onLayout`. The measured height replaces the hint, and disabling the overlay
21
+ clears the measurement so a later enablement can use a new hint. The hint does
22
+ not enable the effect or reserve an inset without an active local overlay.
23
+ This uses the existing native `topOverlayHeight` prop without changing the
24
+ native interface. For native smooth local Hard mode, a complete Fabric mount
25
+ attaches the overlay slot synchronously before `didMount` returns; incomplete
26
+ slots retain the deferred fallback. It binds UIKit's
18
27
  `UIScrollEdgeElementContainerInteraction` to that exact scroll view and selects
19
28
  the requested top edge style. Hard has UIKit's dividing line; Soft has its
20
29
  gradual blurred transition. A pager owns the effect and inset for its current page;
21
30
  its nested NativeList or NativeScroller must not opt into another top overlay.
22
31
  Invisible, noninteractive native labels let UIKit recognize the React-drawn
23
32
  overlay and sticky header as scroll-edge elements. The pager does not add an
24
- auxiliary full-area glass or opaque material. It clips scrolling assets above
33
+ auxiliary full-area glass. It clips scrolling assets above
25
34
  the fixed overlay boundary; during horizontal transitions it clips its owned
26
35
  visible page hosts below the current sticky boundary so text cannot pass in
27
36
  front of the fixed controls. Masked assets must not intercept their hit targets.
37
+ On iOS 27 or newer, a local smooth Hard pager with a valid shared-header mask
38
+ fades a bottommost overlay layer using the resolved opaque pager or ancestor
39
+ theme color as the real header collapses, without hard-coded colors or changing
40
+ UIKit's Hard style, owner or interactions. During temporary horizontal owner
41
+ transitions the layer holds its last opacity until geometry is valid again;
42
+ disabling the local overlay, changing style or recycling removes it.
28
43
 
29
44
  For an iOS 26 pager whose fixed header is rendered by a sibling in the same
30
45
  React surface, `externalTopOverlayTag` may identify that header's native view.
@@ -59,8 +74,10 @@ the shared host before their old scroll-edge interaction is released.
59
74
  The measured overlay height is excluded from the collapsible header distance:
60
75
  `header` remains collapsible and `stickyHeader` pins immediately below the
61
76
  overlay. Selection, cancellation, reuse and disposal rebind or release the
62
- interaction and restore any prior scroll-edge style. Native scroll/refresh
77
+ interaction and restore any prior scroll-edge style. The existing owner-scoped native sticky-height callback also reaches NativeList hosts, so their React container slots clip below the same fixed viewport region and release it when the owner detaches. Native scroll/refresh
63
78
  coordination continues on the main thread without per-frame JavaScript work.
79
+ Refresh retains the pager-owned `overlay + header + sticky` top inset but offsets
80
+ the indicator by `header + sticky`, so its spinner begins below the fixed overlay.
64
81
 
65
82
  On iOS before 26, the external tag is not forwarded to native. On iOS before
66
83
  26, Android and Web the new props have no scroll-edge effect.
@@ -73,17 +90,15 @@ Acceptance requires real iOS 26 scrolling, partial collapse, short/empty page,
73
90
  refresh, page transition/cancellation, dynamic tabs and light/dark checks;
74
91
  source and build checks alone do not establish visual correctness.
75
92
 
76
- Experimental trial: `CollapsiblePagerView` paints a cover at each owned
93
+ `CollapsiblePagerView` paints a cover at each owned
77
94
  fixed-overlay or pinned-sticky boundary with one physical pixel of the nearest
78
95
  opaque ancestor background color (system background if none is available).
79
96
  These native layers do not receive touches, change insets, or clip glass shadows.
80
97
  They are removed for Soft, disabled/detached effects, and recycling. This is a
81
- visual workaround using public layer APIs, not a UIKit separator setting; rendered
82
- light/dark, scrolling, and transition acceptance is pending. Standalone
98
+ visual workaround using public layer APIs, not a UIKit separator setting. While
99
+ the sticky header is pinned, only the top cover is hidden; it reappears on unpin,
100
+ and the sticky cover and both Hard interactions remain active. Standalone
83
101
  NativeScroller and NativeList retain UIKit's unmodified Hard appearance.
84
- On the iOS 26.5 simulator, a runtime probe confirmed Hard and the cover layers,
85
- including pinned-header ownership. The original hairline was not reliably
86
- reproduced with the covers hidden, so separator suppression is not yet verified.
87
102
 
88
103
  Style selection crosses Fabric as the enum `none | hard | soft`, defaulting to
89
104
  `none`, rather than a hard-effect boolean. Switching Hard ↔ Soft on an attached
@@ -156,6 +171,24 @@ intent. Real vertical consumption, a newer valid command, or owner detachment
156
171
  releases the target. Existing NativeList and NativeScroller public coordinates
157
172
  are unchanged. The same page-top/sticky-top destinations are available without smooth-header mode.
158
173
 
174
+ ## Current-page partial header command
175
+
176
+ `scrollHeaderBy(delta: number)` moves the current page's shared collapsible
177
+ header from its displayed offset C to `clamp(C + delta, 0, D)`.
178
+ `scrollHeaderToOffset(offset: number)` moves it to `clamp(offset, 0, D)`
179
+ regardless of intervening native keyboard or user scrolling. D is the latest
180
+ measured collapsible height. Inputs use logical points on iOS and Android and
181
+ CSS pixels on Web. Non-finite values are ignored. The header and current body move immediately
182
+ so a focused input becomes visible before the next keyboard frame. A valid command
183
+ clears only the current page's body reading position B; it never changes
184
+ another page's memory or adds permanent content/insets. It requires the same
185
+ stable attached current owner as the page-top commands and is a safe no-op
186
+ during horizontal transitions, before mount, or after disposal. A newer
187
+ command or real user vertical consumption takes ownership from an older one.
188
+ Dynamic header measurements clamp any in-flight target to the latest D.
189
+ This command allows a focused input inside the shared header to move above a
190
+ keyboard without replacing the pager's sole vertical scroll owner.
191
+
159
192
  ## Independent NativeScroller migration
160
193
 
161
194
  Status: implemented in source. Both native builds pass. Android focused emulator
@@ -318,6 +351,43 @@ warnings were observed. Keyboard, multi-touch, physical-device performance and
318
351
  the full lifecycle/interaction matrix remain unverified. These are local source
319
352
  builds; the currently installed npm version does not yet contain this migration.
320
353
 
354
+ ## Android removal-transition geometry retention
355
+
356
+ Status: implemented locally; unreleased. On the Android UI thread, during a
357
+ screen removal transition,
358
+ NativeList and CollapsiblePager layout entry points respect the RecyclerView
359
+ suppression state established by the screen-transition owner. The owner restores
360
+ the original state when its last transition ownership ends. In addition,
361
+ `CollapsiblePagerHost` disables `DefaultItemAnimator` on its internal
362
+ `ViewPager2` `RecyclerView` (`itemAnimator = null`) and skips pre-draw
363
+ (`layoutPagerIfRequested`) and host-driven `ViewPager2` layout passes when all
364
+ React page children have been removed from the adapter (`adapter.itemCount == 0`)
365
+ while previously rendered page `ViewHolder` views remain attached
366
+ (`attachedPageCount() > 0`), matching `PagerViewViewManagerImpl`'s unmount
367
+ guard. Initial empty measurement before any page is attached
368
+ (`attachedPageCount() == 0`) and subsequent page insertions (`adapter.itemCount > 0`)
369
+ still measure and lay out normally. This prevents unmount-time empty-adapter
370
+ layout passes from clearing visible exit frames or recycling attached
371
+ `ViewHolder` views held in `mDisappearingChildren` by `ViewGroup.startViewTransition`.
372
+ This adds no public API and does not change Web or iOS behavior.
373
+
374
+ If Fabric removes the current NativeScroller React child while its origin-frame
375
+ transition remains active, Android retains the already displayed native extent
376
+ for drawing. The current child and raw content dimensions still clear normally;
377
+ only native content measurement and layout use the retained effective extent.
378
+ The matching transition end or detach clears the private child and extent state
379
+ idempotently. Adding a new child cancels prior retention, and a late end for the
380
+ old child cannot alter the new content. Ordinary removal, size events and scroll
381
+ commands keep their existing semantics.
382
+
383
+ Local arm64 DevShell acceptance covered Base, CXO and Overview exit content,
384
+ normal scrolling, RecyclerView ownership and suppression cleanup for the
385
+ observed CXO exit, and NativeScroller raw-zero content dimensions with a
386
+ nonzero displayed origin followed by detach cleanup. Nested-owner and cancel
387
+ paths, Overview rebound/cancel, natural resize and new-child runtime cases,
388
+ and the full performance matrix remain unverified. SDK publication and
389
+ consumer version alignment are separate steps.
390
+
321
391
  ## iOS vertical-scroll press cancellation
322
392
 
323
393
  Status: implemented in source. A source-identical `node_modules` build was
@@ -363,7 +433,11 @@ content, the shared header, and touches beginning on an inner horizontal banner.
363
433
  Normal taps MUST remain functional. Press-retention behavior in application
364
434
  controls belongs to the consuming responder implementation; the Pager package
365
435
  does not contain or publish Tamagui helpers. The native application tab bar has
366
- a separate contract in react-native-tab-view/docs/SPEC.md.
436
+ a separate contract in react-native-bottom-tabs/docs/SPEC.md.
437
+
438
+ On Android, smooth-touch coordination and nested interception share the same
439
+ native-gesture state. Once either path starts the gesture, later MOVE events
440
+ MUST NOT start it again before its end or cancellation clears that state.
367
441
 
368
442
  The current native implementation confirms horizontal intent after at least
369
443
  32 points on iOS or 32 dp on Android, with horizontal displacement exceeding
@@ -437,6 +511,40 @@ The attached UIKit regression intentionally constrains the real pager pan to two
437
511
  required touches to prove the no-pan path; it is distinct from natural Home OS
438
512
  input, coalesced-event delivery, and physical-device acceptance.
439
513
 
514
+ ## Native segmented tab bar
515
+
516
+ Status: implemented locally. Focused production-class iOS XCTest and Android
517
+ instrumentation checks pass; consuming App interaction acceptance is separate.
518
+ `nativeTabBar.style.variant` accepts `"underline"` (the default) or
519
+ `"segmented"` on iOS and Android. The optional
520
+ `nativeTabBar.style.contentPaddingVertical` is a nonnegative logical inset,
521
+ defaulting to zero. Omitting both fields preserves the existing content-width,
522
+ horizontally scrollable underline bar. Web ignores `nativeTabBar` and retains
523
+ its caller-rendered sticky header.
524
+
525
+ In segmented mode, the existing `height` is the entire tab slot. The track
526
+ fills the space inside `contentPaddingHorizontal` and
527
+ `contentPaddingVertical`, uses `backgroundColor`, and has a fully rounded
528
+ outline. Its visible items divide the track into equal-width segments in their
529
+ current order, including after item insertion, removal, relabeling, or resize.
530
+ The thumb uses `indicatorColor`, is inset two logical points from its segment,
531
+ and is fully rounded. Segment titles use the existing font and active/inactive
532
+ text colors, remain single-line with truncation, and retain their existing
533
+ accessibility labels, test IDs, and native press event keys. The segmented bar
534
+ does not scroll horizontally; `itemSpacing`, `indicatorHeight`, and
535
+ `indicatorBottom` apply only to underline mode. The pager's native fractional
536
+ page progress moves the thumb continuously between equal-width segments and
537
+ interpolates text emphasis without per-frame JavaScript updates. It settles at
538
+ the actual selected page after a completed or cancelled gesture. RTL reverses
539
+ physical segment order without changing logical page keys or indices.
540
+
541
+ Switching variants or removing the native tab bar clears the previous visual
542
+ state. The existing native press command, page callbacks, retention, vertical
543
+ header geometry, and Web behavior remain unchanged. Acceptance requires
544
+ focused native geometry/progress/press checks on iOS and Android, including
545
+ two or three segments, dynamic labels/items, RTL, narrow widths, and variant
546
+ switching. Type forwarding alone does not prove rendered behavior.
547
+
440
548
  ## Native tab-press animation default
441
549
 
442
550
  Behavior change: previously an omitted `nativeTabPressAnimationEnabled` animated
@@ -457,6 +565,28 @@ Status: implemented; focused native-wrapper tests cover both directions,
457
565
  explicit overrides and in-flight/superseded command targets. Home applies this policy to its JS tab bar, with iOS and
458
566
  Android simulator recordings verifying distant jumps and adjacent transitions.
459
567
 
568
+ ## Native tab trailing control reservation
569
+
570
+ Status: implemented locally, unreleased. The focused Android host test passed
571
+ (1/1); iOS hit tests had 8 passes and 1 existing skip. iOS companion-app UI
572
+ acceptance remains unverified.
573
+ `nativeTabBar.style.trailingReservedWidth` is an optional logical trailing
574
+ reservation for a sibling React control in the full-width sticky header. It
575
+ defaults to zero. JavaScript normalizes non-finite or negative input to zero;
576
+ native also clamps the reservation to the actual tab viewport width. On Android
577
+ and iOS, only the native tab viewport shrinks from `W` to `W - R`: its frame
578
+ begins at zero in LTR and at `R` in RTL. The sticky React header, its height,
579
+ and page content keep their original full-width geometry. Touches in the
580
+ reserved region reach the sibling control, while native tab hit testing and
581
+ last-tab visibility use the reduced viewport. Changing the reservation updates
582
+ that viewport; removing it or recycling the pager restores the default zero.
583
+ Web does not render the native tab and ignores this style field. For a positive
584
+ reservation, the box-none sticky React control draws above the native tab so
585
+ its inward fade and toolbar hit area remain visible; zero preserves the old
586
+ native-tab foreground order. Acceptance requires focused native layout and hit
587
+ testing for zero, nonzero, reset, and RTL reservations; type forwarding alone
588
+ is insufficient.
589
+
460
590
  ## Android refresh foreground coordination
461
591
 
462
592
  Status: implemented; Android Home simulator placement verified on 2026-09-28.
@@ -802,6 +932,64 @@ fallback debounce, including replacement-state restoration. Other collection
802
932
  fallbacks keep that safety delay. Caller insets and retained logical offsets
803
933
  remain governed by the existing restoration policy.
804
934
 
935
+ A verified, mounted NativeList with usable vertical bounds is a scroll-owner
936
+ boundary. Its React container slots may contain independent Fabric scroll views,
937
+ NativeScroller views or nested pagers; those descendants MUST NOT replace that
938
+ NativeList as the enclosing page's owner or receive its shared header. Candidate
939
+ priority between sibling page owners remains unchanged, including an explicit
940
+ NativeScroller sibling. Pages without an enclosing NativeList retain nested-pager
941
+ active-owner delegation. This boundary does not change scroll offsets, insets,
942
+ shared-header collapse or per-page body memory.
943
+
944
+ Status: implemented and verified by production-native UIKit regressions on
945
+ iOS 27.1 for footer insertion, explicit-sibling priority and nested-pager
946
+ delegation. Full App and physical-device visual acceptance are reported separately.
947
+
948
+ When the active scroll owner is removed before its replacement becomes usable,
949
+ the pager MUST retain bounded recovery (at most 12 retries per recovery round,
950
+ spaced 100 ms apart). Clearing the completed mount's reattachment flag MUST NOT
951
+ cancel that recovery. A replacement that becomes usable during that round,
952
+ without another Fabric insertion, MUST receive the observer, shared headers,
953
+ insets and retained collapse state. Page selection changes and recycling
954
+ invalidate stale retry callbacks. Repeated mount attempts MUST NOT reset the
955
+ retry budget. Once all 12 retries have been scheduled, an Insert whose parent is
956
+ in the current page subtree MUST reopen recovery if that page retains a released
957
+ scroll owner, including when the final retry callback is still pending. Reopening
958
+ invalidates the old callback before clearing its scheduled state. The Insert
959
+ MUST be recorded during a horizontal drag or transition, while observer and shared
960
+ header attachment remain deferred until selection settles. Cancelling the drag
961
+ on the same page MUST retain the reopened recovery round. Empty transactions,
962
+ removals and inserts outside the current page MUST NOT reopen the budget. Owned
963
+ `NativeScroller` and `NativeList` hosts additionally emit push readiness
964
+ notifications when their scroll views attach and obtain positive bounds so late
965
+ replacements recover immediately even without another Fabric insertion or after
966
+ the retry budget expires; static pages with neither an unready scroll host nor a
967
+ released scroll owner do not schedule retry timers.
968
+
969
+ Status of transition-time and pending-final-retry recovery: implemented and
970
+ UIKit regression verified on iOS 26.5 (2026-10-27). Both new regressions fail
971
+ before the fix and pass afterward. Cancellation settles on the source page
972
+ before the replacement becomes usable; the pending-final-retry case makes the
973
+ replacement usable after the old timer's deadline. Observer and shared header
974
+ ownership stay detached during the drag, then recover with the retained collapse.
975
+ The full Pager suite executes 85 tests, with one iOS-27-only skip and no failures.
976
+
977
+ Status of exhausted-budget recovery: implemented and UIKit regression verified
978
+ on iOS 26.5 (2026-10-27). The regression exhausts the initial round, checks that
979
+ empty transactions, removals and offscreen inserts do not reopen it, then inserts
980
+ a current-page replacement whose scroller appears after did-mount. It fails
981
+ before the fix and passes afterward, including preservation of an active round
982
+ and restoration of the observer, shared headers and retained collapse. The full
983
+ Pager suite executes 83 tests, with one iOS-27-only skip and no failures.
984
+
985
+ Status of replacement recovery: implemented and runtime verified on iOS 26.5
986
+ (2026-10-23). The late-replacement UIKit regression fails before the fix and
987
+ passes afterward; the full Pager suite executes 82 tests, with one iOS-27-only
988
+ skip and no failures. In the consuming app based on x at b0789571d8 with SDK
989
+ 6.7.0-9 and this local native change, two empty Favorites-to-Wallet-add-to-Market
990
+ cycles and a Trending/Favorites round trip keep the first row aligned with the
991
+ sticky header. The previously observed 212-point gap is absent.
992
+
805
993
  This is a UI-thread, main-runtime native coordination change with no new props,
806
994
  events, background-runtime work, persistence or retained strong view cache.
807
995
  Android already prepares discovered scroll children before draw and is unchanged;
@@ -841,3 +1029,62 @@ The native Home Spotlight adapter consumes this snapshot in the main UI runtime
841
1029
  Removing a page or clearing the page collection MUST cancel the active horizontal transition and its temporary display snapshots before notifying the adapter. Cancellation MUST NOT rewrite the shared Header collapse or surviving pages' saved body reading memory. The previous observed owner MUST be detached without committing an offset under a shifted page key.
842
1030
 
843
1031
  An empty adapter MUST reject transition starts and transition completion without coercing an index into an empty range. The selected index uses zero as an empty-state sentinel; nonempty mutations clamp it to the remaining valid range. Readding pages MUST allow a fresh selection and MUST NOT resume a removed transition source. Conformance requires actual Android active-transition → zero-pages → readd/selection regression and remaining-page mutation checks; structural checks and compilation alone do not establish runtime behavior.
1032
+
1033
+
1034
+ ## Web parent-scroll owner identity
1035
+
1036
+ Web Header and sticky overlays use the named scroll timeline only when the
1037
+ browser supports every CSS property needed for that linkage. Otherwise, the
1038
+ active scroll owner's offset drives the same clamped Header collapse in JS:
1039
+ `C = min(max(scrollTop, 0), max(headerHeight, 0))`. The fallback keeps the
1040
+ existing page owner, insets, body offset memory, and sticky anchor. It updates
1041
+ when scrolling, selecting or restoring a page, replacing an owner, or changing
1042
+ Header height, and releases its listener when that owner detaches. An inactive
1043
+ page cannot move the visible Header. This is a Web-only compatibility path; no
1044
+ native behavior or public prop changes.
1045
+ When an overlay forwards a vertical wheel or touch scroll to the active owner,
1046
+ the owner receives the same internal user-input notification used by route
1047
+ restoration to cancel an obsolete pending offset.
1048
+
1049
+ On Web, an explicitly marked NativeScroller is selected before an implicit NativeList viewport. There is one vertical owner per page; callers using parent-owned NativeList pass that same scroll element as webScrollHost.
1050
+
1051
+ The existing NativeScroller pagerScrollKey identifies a Web page scroll owner. Changing the key invalidates that page body offset without remounting its React business content. The active owner also resets the shared header collapse and cancels queued old restore/top commands. Inactive owners invalidate their saved body offset without imperative scrolling; selection checks the current identity before restoring. A→B→A identity changes never revive a prior owner offset. Stable keys preserve ordinary page C+B restoration. Current-page top commands continue to affect only the selected page.
1052
+
1053
+ Identity notifications and DOM owner listeners are internal implementation details, removed when the pager root or owner is replaced/unmounted. No native behavior or public configuration is added.
1054
+
1055
+ ### Web overlay input and keyed-page updates
1056
+
1057
+ - Header and sticky slots remain overlays: overflow outside the sticky slot may remain visible without adding its portal height to page insets twice.
1058
+ - Vertical wheel or touch input over those overlays transfers only to the active page scroll owner when no nested scrollable consumes it. Horizontal controls, editable inputs, and ordinary button/tap behavior retain their own events. Input originating inside the page owner follows native scrolling once.
1059
+ - Reordering or removing keyed pages must save the previously configured key and owner, rather than resolving the old selected index against new children. Caller index synchronization preserves keyed body offsets and shared header collapse.
1060
+ - For an explicit NativeScroller, Pager chrome insets apply to the existing ScrollView content element, not its viewport. Insets taller than the available window cannot enlarge the scroll viewport. Existing owner/content padding is preserved and restored separately from Pager insets; direct NativeList/page-host fallback retains its prior layout path.
1061
+ - The Pager root clips without becoming a programmatically scrollable overflow box. Browser focus and descendant scrollIntoView cannot add a second horizontal page offset outside the selected-page transform.
1062
+
1063
+ ## Web page geometry and route restoration
1064
+
1065
+ Status: Implemented in Web source and focused tests; browser and native runtime acceptance remain separate.
1066
+
1067
+ `WebCollapsiblePagerView` is the existing Web `CollapsiblePagerView` class under an explicit root export. Its `isActive` prop defaults to true and represents route visibility, independently of the selected page. The Web-only `useWebCollapsiblePagerPage()` hook returns null outside a Web pager and otherwise exposes `isFocused`, `isRouteActive`, `owner`, `content`, optional `contentHeight`, `width`, `height`, content-relative `scrollTop`, `registerContent`, and read-only `pendingRouteScrollTop`. Native `CollapsiblePagerView`, its bridge, and its props are unchanged. The Web component rejects mounting without a DOM; importing the root or Web module alone must not access DOM globals.
1068
+
1069
+ Web callers may supply finite nonnegative `headerHeight` and `stickyHeaderHeight`. When omitted or invalid (negative, NaN, or infinite), the SDK measures the natural inner Header and sticky content with `ResizeObserver`; explicit valid values take precedence, including zero. An initial unmeasured zero is not ready for route restoration; a measured empty chrome element may legitimately have zero height. Measurement must not use the constrained slot height as its own input. Height changes update the owned insets before route restoration. Existing explicit-height callers retain their layout path.
1070
+
1071
+ Each page has one SDK-discovered vertical owner: explicit `NativeScroller`, then implicit NativeList viewport, then page host. Registration accepts a connected content element and optional finite nonnegative explicit height; invalid height metadata is ignored. It does not change DOM layout or make a pending target reachable. A later registration of the same element supersedes an earlier one, and stale cleanup cannot remove it. An explicit-height change schedules geometry measurement even if the element border box is unchanged. Metrics use owner client width; `pinnedBottom = ownerRect.top + max(0, headerHeight - owner.scrollTop) + stickyHeaderHeight`; `visibleTop = max(ownerRect.top, pinnedBottom, contentRect.top if present)`; visible height is `max(0, ownerRect.bottom - visibleTop)` and content-relative scroll top is `max(0, pinnedBottom - contentRect.top)`. Unregistered content falls back to the first owner child. Detached content and callbacks from old owners are ignored. One animation-frame measurement per page coalesces scroll, resize, registration, owner configuration, and active-page changes. Disposal releases observers, listeners, frames, and subscriptions.
1072
+
1073
+ On active-to-inactive transition, the Web pager freezes the selected logical React page key, `pagerScrollKey` identity, actual owner generation, and last valid raw DOM top. Inactive scroll/geometry changes cannot replace the snapshot. A second blur of the same pending identity preserves the original target. On focus, owner, content, chrome, or inset changes, the pager retries without a timer only when the route is active, that exact key/identity/owner remains selected and connected, the viewport has positive height, the pager has configured the expected insets, and real `scrollHeight - clientHeight` reaches the target. Short content keeps the target pending. Restoration clears pending before one instant scroll and synchronizes the existing shared Header C and per-page body B memory. An idempotent Web `restoreScrollPosition()` returns false while inactive or not ready and true when no target remains or it applies; normal callers need no polling.
1074
+
1075
+ Valid vertical wheel, scroll keys, scrollbar drag, overlay-forwarded input, accepted NativeList commands, and explicit page-top/sticky-top commands cancel pending route intent through the existing SDK takeover path. As in the prior Web app adapter, any owner `pointerdown` or `touchstart`, including a child target, cancels only route pending; this does not newly commit or clear SDK C+B memory. Geometry-only scroll/resize/scrollend cannot cancel intent. Valid same-offset commands cancel it; invalid or stale commands do not. A matching route target takes priority over automatic tab C+B restoration. Selection of a different key, owner identity reset (including A→B→A), genuine DOM owner replacement, or removal cancels pending; a same-key index reorder preserves it. A same-commit blur and Activity/Offscreen effect disconnect captures the current route target before releasing the owner; repeated disconnects preserve an existing pending target. A temporary React ref null or Activity/Offscreen effect disconnect followed by the identical connected owner preserves the logical route target and owner generation while releasing all observers, listeners, frames, and DOM configuration during disconnect. Restoration is unavailable while disconnected; reconnect remeasures automatic chrome before applying pending. A truly removed pager instance cannot transfer its pending target to a new instance. Older queued tab/top callbacks must validate generation before writing after route restoration or cancellation. The app owns navigation focus and business tab names; the SDK never infers them from escaped React child keys.
1076
+
1077
+ Platform matrix: Web owns this DOM geometry, registration, route pending, and explicit Web export. iOS and Android keep native scrolling, restoration, and page coordination documented above, with no new prop, native bridge call, JS heap sharing, or process resource. The UI main runtime owns Web state; background runtimes do not deserialize it. Source/type tests establish only Web contract implementation, not browser rendering or native device behavior. Acceptance requires real Web/Desktop route blur, short content then growth, user takeover, dynamic chrome, key reset, and Home/Market scroll regressions.
1078
+
1079
+ ## Native package entry isolation
1080
+
1081
+ The Native package entry exports the existing Native PagerView,
1082
+ CollapsiblePagerView, NativeScroller, usePagerView and ScrollEdgeEffect APIs.
1083
+ Native package resolution MUST NOT evaluate the Web pager or Web page-context
1084
+ modules. The react-native field and conditional export resolve to a real Native
1085
+ entry; the browser entry retains the Web runtime APIs. The generic declaration
1086
+ entry preserves the existing shared Web value/type API for TypeScript callers.
1087
+ This separates package initialization in the Native UI main runtime; it does not
1088
+ add a bridge, share JS heaps with background, or change Native scroll commands.
1089
+ Acceptance includes the actual Native dependency graph and unchanged startup
1090
+ budgets, Web runtime/type exports, and package source/module/declaration entries.