@onekeyfe/react-native-native-overlay 3.0.162-alpha.268

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 (163) hide show
  1. package/LICENSE +21 -0
  2. package/NativeOverlay.podspec +30 -0
  3. package/README.md +35 -0
  4. package/android/build.gradle +82 -0
  5. package/android/src/main/AndroidManifest.xml +2 -0
  6. package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayAnimation.kt +176 -0
  7. package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayEntryRootView.kt +415 -0
  8. package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayEvents.kt +38 -0
  9. package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayHost.kt +174 -0
  10. package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayPackage.kt +16 -0
  11. package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayPageHostView.kt +122 -0
  12. package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayPageHostViewManager.kt +40 -0
  13. package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayView.kt +465 -0
  14. package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayViewManager.kt +156 -0
  15. package/android/src/main/jni/CMakeLists.txt +44 -0
  16. package/android/src/main/jni/RNCNativeOverlay.h +19 -0
  17. package/common/cpp/react/renderer/components/RNCNativeOverlay/NativeOverlayComponentDescriptor.h +12 -0
  18. package/common/cpp/react/renderer/components/RNCNativeOverlay/NativeOverlayShadowNode.cpp +12 -0
  19. package/common/cpp/react/renderer/components/RNCNativeOverlay/NativeOverlayShadowNode.h +41 -0
  20. package/common/cpp/react/renderer/components/RNCNativeOverlay/NativeOverlayState.h +40 -0
  21. package/docs/SPEC.md +693 -0
  22. package/ios/NativeOverlayAnimation.swift +160 -0
  23. package/ios/NativeOverlayContainerView.swift +688 -0
  24. package/ios/NativeOverlayKeyboard.swift +88 -0
  25. package/ios/NativeOverlayPageHostView.swift +98 -0
  26. package/ios/NativeOverlaySheetController.swift +391 -0
  27. package/ios/NativeOverlayWindowManager.swift +194 -0
  28. package/ios/RCTNativeOverlayComponentView.h +11 -0
  29. package/ios/RCTNativeOverlayComponentView.mm +193 -0
  30. package/ios/RCTNativeOverlayPageHostComponentView.h +11 -0
  31. package/ios/RCTNativeOverlayPageHostComponentView.mm +64 -0
  32. package/lib/module/NativeOverlayNativeComponent.ts +61 -0
  33. package/lib/module/NativeOverlayPageHostNativeComponent.ts +13 -0
  34. package/lib/module/OverlayErrors.js +6 -0
  35. package/lib/module/OverlayErrors.js.map +1 -0
  36. package/lib/module/OverlayLevelContext.js +27 -0
  37. package/lib/module/OverlayLevelContext.js.map +1 -0
  38. package/lib/module/OverlayLevels.js +33 -0
  39. package/lib/module/OverlayLevels.js.map +1 -0
  40. package/lib/module/OverlayPageHost.js +24 -0
  41. package/lib/module/OverlayPageHost.js.map +1 -0
  42. package/lib/module/OverlayPageHost.web.js +49 -0
  43. package/lib/module/OverlayPageHost.web.js.map +1 -0
  44. package/lib/module/OverlayPageScope.js +41 -0
  45. package/lib/module/OverlayPageScope.js.map +1 -0
  46. package/lib/module/OverlayStore.js +305 -0
  47. package/lib/module/OverlayStore.js.map +1 -0
  48. package/lib/module/OverlayView.js +141 -0
  49. package/lib/module/OverlayView.js.map +1 -0
  50. package/lib/module/OverlayView.web.js +290 -0
  51. package/lib/module/OverlayView.web.js.map +1 -0
  52. package/lib/module/OverlayViewTypes.js +4 -0
  53. package/lib/module/OverlayViewTypes.js.map +1 -0
  54. package/lib/module/animation/motionPresets.js +57 -0
  55. package/lib/module/animation/motionPresets.js.map +1 -0
  56. package/lib/module/animation/platformMotionPresets.js +4 -0
  57. package/lib/module/animation/platformMotionPresets.js.map +1 -0
  58. package/lib/module/animation/platformMotionPresets.web.js +4 -0
  59. package/lib/module/animation/platformMotionPresets.web.js.map +1 -0
  60. package/lib/module/animation/resolveAnimation.js +80 -0
  61. package/lib/module/animation/resolveAnimation.js.map +1 -0
  62. package/lib/module/animation/spring.js +69 -0
  63. package/lib/module/animation/spring.js.map +1 -0
  64. package/lib/module/animation/types.js +2 -0
  65. package/lib/module/animation/types.js.map +1 -0
  66. package/lib/module/index.js +16 -0
  67. package/lib/module/index.js.map +1 -0
  68. package/lib/module/package.json +1 -0
  69. package/lib/module/types.js +2 -0
  70. package/lib/module/types.js.map +1 -0
  71. package/lib/module/useOverlayController.js +127 -0
  72. package/lib/module/useOverlayController.js.map +1 -0
  73. package/lib/module/useSuspendedPageOwners.js +16 -0
  74. package/lib/module/useSuspendedPageOwners.js.map +1 -0
  75. package/lib/module/web/animateTransition.js +115 -0
  76. package/lib/module/web/animateTransition.js.map +1 -0
  77. package/lib/module/web/overlayLayers.js +147 -0
  78. package/lib/module/web/overlayLayers.js.map +1 -0
  79. package/lib/module/web/useKeyboardShift.js +110 -0
  80. package/lib/module/web/useKeyboardShift.js.map +1 -0
  81. package/lib/module/web/useSheetDrag.js +126 -0
  82. package/lib/module/web/useSheetDrag.js.map +1 -0
  83. package/lib/typescript/package.json +1 -0
  84. package/lib/typescript/src/NativeOverlayNativeComponent.d.ts +41 -0
  85. package/lib/typescript/src/NativeOverlayNativeComponent.d.ts.map +1 -0
  86. package/lib/typescript/src/NativeOverlayPageHostNativeComponent.d.ts +9 -0
  87. package/lib/typescript/src/NativeOverlayPageHostNativeComponent.d.ts.map +1 -0
  88. package/lib/typescript/src/OverlayErrors.d.ts +4 -0
  89. package/lib/typescript/src/OverlayErrors.d.ts.map +1 -0
  90. package/lib/typescript/src/OverlayLevelContext.d.ts +12 -0
  91. package/lib/typescript/src/OverlayLevelContext.d.ts.map +1 -0
  92. package/lib/typescript/src/OverlayLevels.d.ts +13 -0
  93. package/lib/typescript/src/OverlayLevels.d.ts.map +1 -0
  94. package/lib/typescript/src/OverlayPageHost.d.ts +9 -0
  95. package/lib/typescript/src/OverlayPageHost.d.ts.map +1 -0
  96. package/lib/typescript/src/OverlayPageHost.web.d.ts +11 -0
  97. package/lib/typescript/src/OverlayPageHost.web.d.ts.map +1 -0
  98. package/lib/typescript/src/OverlayPageScope.d.ts +17 -0
  99. package/lib/typescript/src/OverlayPageScope.d.ts.map +1 -0
  100. package/lib/typescript/src/OverlayStore.d.ts +72 -0
  101. package/lib/typescript/src/OverlayStore.d.ts.map +1 -0
  102. package/lib/typescript/src/OverlayView.d.ts +3 -0
  103. package/lib/typescript/src/OverlayView.d.ts.map +1 -0
  104. package/lib/typescript/src/OverlayView.web.d.ts +3 -0
  105. package/lib/typescript/src/OverlayView.web.d.ts.map +1 -0
  106. package/lib/typescript/src/OverlayViewTypes.d.ts +68 -0
  107. package/lib/typescript/src/OverlayViewTypes.d.ts.map +1 -0
  108. package/lib/typescript/src/animation/motionPresets.d.ts +13 -0
  109. package/lib/typescript/src/animation/motionPresets.d.ts.map +1 -0
  110. package/lib/typescript/src/animation/platformMotionPresets.d.ts +2 -0
  111. package/lib/typescript/src/animation/platformMotionPresets.d.ts.map +1 -0
  112. package/lib/typescript/src/animation/platformMotionPresets.web.d.ts +2 -0
  113. package/lib/typescript/src/animation/platformMotionPresets.web.d.ts.map +1 -0
  114. package/lib/typescript/src/animation/resolveAnimation.d.ts +23 -0
  115. package/lib/typescript/src/animation/resolveAnimation.d.ts.map +1 -0
  116. package/lib/typescript/src/animation/spring.d.ts +24 -0
  117. package/lib/typescript/src/animation/spring.d.ts.map +1 -0
  118. package/lib/typescript/src/animation/types.d.ts +56 -0
  119. package/lib/typescript/src/animation/types.d.ts.map +1 -0
  120. package/lib/typescript/src/index.d.ts +18 -0
  121. package/lib/typescript/src/index.d.ts.map +1 -0
  122. package/lib/typescript/src/types.d.ts +59 -0
  123. package/lib/typescript/src/types.d.ts.map +1 -0
  124. package/lib/typescript/src/useOverlayController.d.ts +29 -0
  125. package/lib/typescript/src/useOverlayController.d.ts.map +1 -0
  126. package/lib/typescript/src/useSuspendedPageOwners.d.ts +7 -0
  127. package/lib/typescript/src/useSuspendedPageOwners.d.ts.map +1 -0
  128. package/lib/typescript/src/web/animateTransition.d.ts +18 -0
  129. package/lib/typescript/src/web/animateTransition.d.ts.map +1 -0
  130. package/lib/typescript/src/web/overlayLayers.d.ts +10 -0
  131. package/lib/typescript/src/web/overlayLayers.d.ts.map +1 -0
  132. package/lib/typescript/src/web/useKeyboardShift.d.ts +29 -0
  133. package/lib/typescript/src/web/useKeyboardShift.d.ts.map +1 -0
  134. package/lib/typescript/src/web/useSheetDrag.d.ts +13 -0
  135. package/lib/typescript/src/web/useSheetDrag.d.ts.map +1 -0
  136. package/package.json +188 -0
  137. package/react-native.config.js +16 -0
  138. package/src/NativeOverlayNativeComponent.ts +61 -0
  139. package/src/NativeOverlayPageHostNativeComponent.ts +13 -0
  140. package/src/OverlayErrors.ts +3 -0
  141. package/src/OverlayLevelContext.tsx +32 -0
  142. package/src/OverlayLevels.ts +40 -0
  143. package/src/OverlayPageHost.tsx +21 -0
  144. package/src/OverlayPageHost.web.tsx +53 -0
  145. package/src/OverlayPageScope.tsx +42 -0
  146. package/src/OverlayStore.ts +401 -0
  147. package/src/OverlayView.tsx +165 -0
  148. package/src/OverlayView.web.tsx +356 -0
  149. package/src/OverlayViewTypes.ts +81 -0
  150. package/src/animation/motionPresets.ts +52 -0
  151. package/src/animation/platformMotionPresets.ts +1 -0
  152. package/src/animation/platformMotionPresets.web.ts +1 -0
  153. package/src/animation/resolveAnimation.ts +103 -0
  154. package/src/animation/spring.ts +80 -0
  155. package/src/animation/types.ts +71 -0
  156. package/src/index.ts +55 -0
  157. package/src/types.ts +79 -0
  158. package/src/useOverlayController.ts +199 -0
  159. package/src/useSuspendedPageOwners.ts +26 -0
  160. package/src/web/animateTransition.ts +137 -0
  161. package/src/web/overlayLayers.ts +182 -0
  162. package/src/web/useKeyboardShift.ts +130 -0
  163. package/src/web/useSheetDrag.ts +143 -0
package/docs/SPEC.md ADDED
@@ -0,0 +1,693 @@
1
+ # react-native-native-overlay SPEC
2
+
3
+ Status: Implemented through the P5 caller migration in app-monorepo. Android
4
+ and Web runtime checks passed there; iOS runtime, physical-device and iPad
5
+ acceptance remain unverified. Packaging in app-modules is validated separately.
6
+
7
+ The implementation is migrated from app-monorepo commit
8
+ `376633bd9f1c410dbccb01fb6a0492be9a5c3f38`, package
9
+ `packages/react-native-native-overlay`. This independent package owns the native
10
+ and Web overlay hosts. Caller migrations remain in app-monorepo.
11
+
12
+ ### Template exception and migration boundary
13
+
14
+ The package was initialized with `yarn create:view native-overlay` on
15
+ 2026-10-01, from app-modules main `5b4574bf0d2a5fdd394b7bd81dd1c8c0d15d101f`.
16
+ The checked-in template is Nitro; the verified implementation uses Fabric
17
+ codegen components and custom shadow nodes to preserve React subtrees, native
18
+ hit testing and `measureInWindow` across overlay windows. Rewriting it to Nitro
19
+ would change those boundaries and requires separate design/runtime validation.
20
+ This SPEC records the explicit template exception allowed by
21
+ `docs/NATIVE_MODULE_DEVELOPMENT.md` section 3.
22
+
23
+ Keep the generated package/configuration structure, but replace the template's
24
+ Nitro sample view, nitrogen config/bindings and CMake adapter with the existing
25
+ Fabric Android, iOS and custom C++ implementation. The package has no Nitro
26
+ peer dependency or nitrogen build step. Builder Bob emits JavaScript and
27
+ TypeScript declarations; React Native generates the Fabric bindings at app
28
+ build time. Publish `common/cpp`, `src`, `lib`, Android/iOS sources, podspec and
29
+ autolinking configuration. Web consumers additionally provide `react-dom`.
30
+
31
+ `react-native-native-sheet` remains separately published for existing consumers;
32
+ its compatibility shim/deprecation is a subsequent migration step, not a
33
+ silent API change in this release. Sheet behavior derived from that package
34
+ retains its original implementation attribution in source comments.
35
+
36
+ ### Release boundary
37
+
38
+ This branch aligns all 42 publishable workspaces to `3.0.162-alpha.268`.
39
+ Publish with the existing `package-publish` workflow dispatch, `npm_dist_tag=next`
40
+ and an empty `only_workspace`, which publishes and registry-verifies all 42.
41
+ Do not merge either repository PR or change the `latest` tag. The app-monorepo
42
+ workspace copy is removed only after this release succeeds and every target
43
+ version and tarball is available. Versions already published on npm are immutable.
44
+
45
+ ## 5.1 Purpose, scope, and non-goals
46
+
47
+ The package owns **where an overlay is drawn, which overlay is on top, and how
48
+ it enters and exits** on iOS, Android, and Web (web, desktop, extension).
49
+
50
+ Callers are `@onekeyhq/components` (Dialog, Toast, Popover, ActionList, Select,
51
+ DatePicker) and `@onekeyhq/kit` global containers: hardware stage and dialogs,
52
+ password verify, DialogLoading, app lock, Spotlight, in-app notification.
53
+
54
+ Non-goals:
55
+
56
+ - Business UI. Titles, buttons, forms, toast cards, the hardware stage morph and
57
+ the lock screen UI stay React content owned by components and kit.
58
+ - Navigation. Screens and navigation modals stay in react-navigation and
59
+ react-native-screens.
60
+ - Third-party windows such as RN `Modal`, WalletConnect, and system dialogs.
61
+ They are only handled defensively by the `lock` level (§5.7).
62
+
63
+ ## 5.2 Definitions and ownership boundaries
64
+
65
+ | Term | Meaning |
66
+ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
67
+ | **scope** | `global`: attached to the app window. `page`: owned by one screen, rendered in that screen's root-route host. |
68
+ | **root-route host** | A single `PageOverlayHost` rendered as the last child of each root-stack route (Main tabs, Modal, Onboarding, FullScreenPush, WebView). Because it sits there, page overlays cover native headers and the tab bar. |
69
+ | **owner page** | The nearest screen that requested a page overlay. The overlay is visible only while its owner is on top inside its root route. |
70
+ | **level** | Fixed layer. `modal` 100 < `hardware` 200 < `secure` 300 < `toast` 400 < `lock` 500 < `debug` 900. |
71
+ | **lane** | The entries sharing (scope, host, level). Strategies apply per lane. |
72
+ | **strategy** | What a new entry does to its lane. `stack` (default) appears on top. `queue` waits until the lane is empty and is ordered by `priority`. `replace` closes the top entry, or the entries sharing `replaceKey`. |
73
+ | **presentation** | Frame and default motion: `sheet`, `center`, `fullscreen`, `toast`, `anchored`. |
74
+
75
+ Ordering rule:
76
+
77
+ - Every page entry renders below every global entry.
78
+ - Inside a scope, entries are ordered by level, then by request order.
79
+ - Nothing else affects order: not mount order, portal order, or window add
80
+ order.
81
+
82
+ Ownership:
83
+
84
+ - **JS store** (`OverlayStore`, one per JS runtime, `main` only) owns ordering,
85
+ queues, page visibility, and back/Escape resolution.
86
+ - **Native hosts** own drawing, hit testing, animation, keyboard, and back-key
87
+ capture, and report lifecycle back to the store.
88
+ - **Content** is the caller's React subtree. The overlay view stays where the
89
+ caller rendered it. Native code reparents its content into the host, so React
90
+ context is preserved; there is no root-siblings portal.
91
+
92
+ The package MUST NOT infer anything from product data. Levels and strategies
93
+ come only from props.
94
+
95
+ ## 5.3 Public API and defaults
96
+
97
+ ### Store (implemented)
98
+
99
+ - `overlayStore.request(request, { onRemoved })` returns an `IOverlayEntry`.
100
+ - Defaults: `scope: 'global'`, `level: 'modal'`, `strategy: 'stack'`,
101
+ `priority: 0`.
102
+ - `blocking` defaults to false for `toast` and `debug`, and true for every
103
+ other level.
104
+ - `dismissible` defaults to the value of `blocking`.
105
+ - `page` scope requires `hostKey` and `ownerKey`. Missing either throws
106
+ `OverlayRequestError`.
107
+ - Re-requesting an id that already exists is a no-op.
108
+ - `dismiss(id, reason)` moves an entry to `closing`. `finalize(id)` removes it
109
+ after the native exit animation. A closing entry that is never finalized is
110
+ removed after 5 s.
111
+ - `dismissAll({ scope, belowLevel, reason })`, `setPageVisible(ownerKey,
112
+ visible)`, `removePage(ownerKey)`.
113
+ - `resolveBack(focusedHostKey?)` returns `dismiss(id)`, `block`, or `pass`.
114
+ - `getBlockingTop()` returns the entry that makes everything below it inert.
115
+ - `subscribe` and `getSnapshot` are compatible with `useSyncExternalStore`.
116
+
117
+ ### Component (P1)
118
+
119
+ ```tsx
120
+ <OverlayView
121
+ open
122
+ scope="global" | "page"
123
+ level="modal"
124
+ strategy="stack"
125
+ priority={0}
126
+ replaceKey?
127
+ presentation="sheet" | "center" | "fullscreen" | "toast" | "anchored"
128
+ animation?={{ enter, exit, backdrop }}
129
+ backdrop?={{ opacity, dismissOnPress }}
130
+ dismissOnBackPress
131
+ dismissOnPanDown // sheet and toast only
132
+ anchor? // anchored only: a trigger ref or a window rect
133
+ onPresented onDismissRequest onDismissed(reason)
134
+ />
135
+ ```
136
+
137
+ - `usePageOverlayHost()` resolves `hostKey` and `ownerKey` from navigation
138
+ context.
139
+ - `useInPageDialog`, `useInModalDialog`, and `useInTabDialog` become aliases of
140
+ page scope. Tab scope is removed.
141
+
142
+ ### Animation (implemented model)
143
+
144
+ - `animation.enter` and `animation.exit` are each
145
+ `{ type: none|fade|slide|scale, edge, distance, scale, offsetY, fade, origin: center|anchor, motion }`.
146
+ - `exit` defaults to `enter`.
147
+ - `motion` is a preset name or a concrete `{ type: 'spring', spring: { mass, stiffness, damping, overshootClamping } }`
148
+ or `{ type: 'timing', timing: { durationMs, easing: cubicBezier } }`.
149
+ - Presets are resolved per platform:
150
+ - Native mirrors the Reanimated driver: `quick` is a spring with m 0.1,
151
+ k 100, c 20 (damping ratio ≈ 3.16).
152
+ - Web mirrors the CSS driver: `quick` is 150 ms `cubic-bezier(.25,.1,.25,1)`.
153
+ - Converting native springs for web is opt-in (`springToCssLinear`), because
154
+ it would slow web overlays down 4–9×.
155
+ - Presentation defaults (`DEFAULT_OVERLAY_ANIMATIONS`):
156
+
157
+ | Presentation | Enter / exit | Backdrop |
158
+ | ------------ | -------------------------------------------------------------------------------- | ------------- |
159
+ | sheet | slide from bottom, `quick` | fade, `quick` |
160
+ | center | scale 0.85 + fade, `quick` | fade, `quick` |
161
+ | toast | slide from top + fade, `toastSlide` (native 300 ms quad in-out, web 400 ms ease) | none |
162
+ | fullscreen | fade, `quick` | none |
163
+ | anchored | scale 0.95 + fade from the resolved anchor placement, `popoverQuick` | fade |
164
+
165
+ - Special cases the callers set explicitly:
166
+ - App lock: `enter: none`, `exit: fade lockFade`. The web tamper check reloads
167
+ the page if the lock fades in from opacity 0.
168
+ - Hardware stage (MorphOverlay), TradingView fullscreen, ScreenshotBranding,
169
+ DevOverlay: `presentation: fullscreen`, `enter`/`exit: none`, transparent,
170
+ touches pass through where content is `box-none`. MorphOverlay keeps its
171
+ Reanimated springs. The container must not animate alpha, transform, or
172
+ frame while its content is animating.
173
+ - Custom toast (`Toast.show`): `scale 0.8 + offsetY -20`, `quick`, with an
174
+ opt-in blocking backdrop.
175
+
176
+ ## 5.4 Lifecycle and concurrency
177
+
178
+ Lifecycle:
179
+
180
+ ```
181
+ request ─► queued ─► active ─► (enter anim) presented
182
+ │
183
+ dismiss / replace / page-removed / back / backdrop / pan
184
+ ▼
185
+ closing ─► (exit anim) onDismissed ─► finalize ─► onRemoved
186
+ ```
187
+
188
+ - Content stays mounted until `finalize`. This replaces today's hardcoded
189
+ timers: the Dialog 300 ms portal destroy, the ColorPicker 300 ms, the
190
+ ActionList and Select 150 ms waits, and the DialogLoading 50 ms delays.
191
+ - A queued entry is promoted only after the previous entry in its lane is
192
+ finalized, so exit and enter never overlap. `replace` is the exception: the
193
+ new entry enters while the replaced one exits.
194
+ - Reopening an entry that is still closing retargets the running animation from
195
+ its current value. It does not restart.
196
+ - `onRemoved` fires exactly once per entry, even when a finalize and the
197
+ timeout race.
198
+ - Page entries:
199
+ - They are suspended (hidden but kept) while the owner page is covered or
200
+ detached.
201
+ - Queued page entries are not promoted into a hidden page.
202
+ - `removePage` closes them with reason `page-removed`.
203
+ - Hide signals must come from native, because tab stacks use `freezeOnBlur`
204
+ and frozen React trees receive no updates:
205
+ - iOS: the owner `RNSScreen`'s `viewWillDisappear` / `viewWillAppear`.
206
+ - Android: the owner `ScreenFragment`'s view detach / attach.
207
+ - Web: navigation focus.
208
+ - Threads: store mutations happen on the JS thread. Native animation and layout
209
+ run on the UI thread. Events are delivered as direct events.
210
+
211
+ ## 5.5 Data, cache, and identity
212
+
213
+ - Identity is the entry `id`: caller-provided, otherwise `overlay-<seq>`.
214
+ - Nothing is persisted. The store is per runtime and empty on reload.
215
+ - `replaceKey` is the only identity shared across entries.
216
+
217
+ ## 5.6 Platform contract
218
+
219
+ | Concern | iOS | Android | Web |
220
+ | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
221
+ | Global level host | One passthrough `UIWindow` per active level per `UIWindowScene`, ordered by `windowLevel`. `lock` sits above RN Alert (2001). | One `OverlayHost` FrameLayout in `android.R.id.content` above the ReactRootView, with one child container per level. | Sibling `div[data-onekey-layer]` roots under `body`, one z-index band per level (`OVERLAY_WEB_Z_INDEX_BASE`). |
222
+ | Order within a level | The window's presentation chain (sheet) and subview order (other presentations). | Child order. | DOM order. |
223
+ | Global sheet | `UISheetPresentationController`, presented from the level window's root VC (reuses native-sheet). | View sheet: CoordinatorLayout + BottomSheetBehavior + dimming view (native-sheet reworked off `BottomSheetDialog`). | Custom sheet: pointer drag, snap by velocity, `visualViewport` keyboard. |
224
+ | Page host | Fabric `PageOverlayHost` as the last child of the root-route screen. Custom-drawn sheet, because UIKit sheets cannot be page-scoped. | The same host inside the root-route screen. The ReactRootView already dispatches touches, so no nested RootView. | `position: absolute` host inside the root-route card. |
225
+ | Header / tab bar coverage (page) | Covered: the host is above the tab controller and navigation bars. | Covered. | Covered. |
226
+ | Touch passthrough (toast, debug, box-none) | `hitTest` returns nil on the window or root view. | A non-blocking entry hit-tests its React content with `TouchTargetHelper` (honoring `box-none`) and falls through to the FrameLayout sibling on a miss. | `pointer-events: none` root, `auto` per entry. |
227
+ | Back / Escape | Escape via `accessibilityPerformEscape`. The interactive pop gesture is disabled while a blocking page overlay is shown. | A `Window.Callback` wrapper consumes `KEYCODE_BACK` while a blocking overlay is shown. `ReactActivity.onBackPressed` hands back to JS BackHandler (react-navigation) before the `OnBackPressedDispatcher`, and the app opts out of predictive back, so a dispatcher callback alone never runs first. The dispatcher callback is kept for the predictive-back path. The IME still gets back first to close the keyboard. | Capture-phase `keydown` that ignores IME composition. |
228
+ | Modality / accessibility | `accessibilityViewIsModal` on the blocking level window; `.screenChanged` posted on present. | `IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS` on the content below. | `inert` on the app root and lower layers, `role=dialog`, `aria-modal`, focus trap and restore. |
229
+ | Keyboard | System sheet avoidance; `keyboardLayoutGuide` for custom presentations. The main window regains key status on dismiss. | `WindowInsetsAnimationCompat` per sheet (the app uses `adjustPan`). | `visualViewport`. |
230
+ | Animation engine | `UIViewPropertyAnimator` + `UISpringTimingParameters(mass:stiffness:damping:)`, which maps 1:1 and supports damping ratio > 1. | `SpringAnimation` with `stiffness = k/m` and `dampingRatio`; `PathInterpolator` for timing curves. | WAAPI `element.animate`. Timing presets use `cubic-bezier`; custom springs use `linear()`. |
231
+ | Content hosting | `RCTSurfaceTouchHandler` on the host root. A state-driven shadow node sizes Yoga, which replaces native-sheet's forced `frame`. A nested `SafeAreaProvider`. | A `RootView` + `JSTouchDispatcher` container for global levels. A state-driven shadow node, like RN Modal. | `createPortal`. The Tamagui theme class is set on the layer root, and React event bubbling stops at the entry root. |
232
+
233
+ Known divergences:
234
+
235
+ | Divergence | Status |
236
+ | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
237
+ | iOS global sheet (system physics) vs. iOS page sheet (custom physics) | Accepted (decision A). Revisit after on-device comparison. |
238
+ | RN `Modal` and third-party dialogs on Android are windows above every layer | Accepted, mitigated by the lock policy (§5.7). |
239
+ | Web anchored origin follows the resolved placement after flip; today's code uses the requested placement | Intentional fix. |
240
+
241
+ ## 5.7 Failure, fallback, and safety
242
+
243
+ - A page request without host or owner keys throws `OverlayRequestError`. This
244
+ is a programming error; there is no silent global fallback, unlike today's
245
+ `Portal.Render` retry.
246
+ - A closing entry is removed after 5 s even if native never reports the end of
247
+ its exit animation.
248
+ - Lock level:
249
+ - iOS: the lock window sits above everything this app creates.
250
+ - Android: when lock shows, the app's foreign windows are found with
251
+ `WindowInspector.getGlobalWindowViews()` (API 29+) and hidden or
252
+ dismissed. An alternative is to make lock a focus-owning `ComponentDialog`
253
+ that re-raises itself.
254
+ - Web: layers below lock get `inert`. Third-party roots are left alone.
255
+ - System view controllers (document picker, PHPicker, share sheet,
256
+ ASWebAuthenticationSession anchor) must be presented through
257
+ `OverlayPresenterResolver` on iOS, so they appear above the overlay windows.
258
+ `lock` is never lowered.
259
+ - No secrets or content are logged. Logs carry only entry id, level, scope and
260
+ reason.
261
+
262
+ ## 5.8 Performance and resource budget
263
+
264
+ - iOS level windows are created lazily per active level and hidden when empty.
265
+ There are at most 6 per scene.
266
+ - Android layers are views, not windows: no extra surfaces or WindowManager IPC.
267
+ - Store operations are O(n) in the number of live entries. Snapshots are
268
+ rebuilt once per mutation.
269
+ - Animations run entirely on the UI thread. JS receives only lifecycle events:
270
+ no per-frame bridge traffic.
271
+ - Content height changes animate only between settled measurements (the
272
+ two-frame stable pattern from Popover). Continuous per-frame height changes,
273
+ such as the DatePicker month-row interpolation, are clipped inside the
274
+ content.
275
+
276
+ ## 5.9 Conformance and acceptance
277
+
278
+ Automated (P0):
279
+
280
+ - `src/__tests__/OverlayStore.test.ts` covers:
281
+ - level ordering regardless of request order;
282
+ - page below global;
283
+ - `stack`, `queue` (priority, lane isolation, promotion after finalize) and
284
+ `replace` (top entry and `replaceKey`);
285
+ - the closing timeout and single `onRemoved`;
286
+ - page suspend, restore and removal;
287
+ - back resolution (global first, `block`, `pass`, focused host) and the
288
+ blocking top.
289
+ - `src/__tests__/animation.test.ts` covers:
290
+ - the spring response in all damping regimes, overshoot clamping and settle
291
+ time;
292
+ - Android `SpringForce` conversion and CSS `linear()` sampling;
293
+ - presentation defaults and per-platform preset resolution.
294
+
295
+ Runtime matrix, required per platform before migrating callers (P1–P3):
296
+
297
+ 1. Show sheet, toast, hardware dialog, password and lock in all 5! orders. The
298
+ final z-order is always level order.
299
+ 2. A queue of 3 hardware dialogs with a priority insert.
300
+ 3. `replace` DialogLoading text updates without flicker.
301
+ 4. Page overlay:
302
+ - push another screen: the overlay hides;
303
+ - pop: it restores;
304
+ - switch tab and come back: it restores;
305
+ - remove the page: it closes with `page-removed`;
306
+ - the native header and tab bar are covered.
307
+ 5. Back/Escape reaches the topmost blocking overlay; the lock swallows it.
308
+ 6. Keyboard: an input in a global sheet, a page sheet and a secure dialog.
309
+ 7. iOS: a system picker opened from an overlay appears above it.
310
+ 8. Split view: page overlays stay in their own pane.
311
+ 9. Reduce motion degrades to fade.
312
+
313
+ P3 page scope notes:
314
+
315
+ - kit wraps every root route component (`withOverlayPageHost`) with
316
+ `OverlayPageHostScope` and renders `OverlayPageHost` after the navigator,
317
+ keyed by the root route key. components `Page` wraps its content in
318
+ `PageOverlayOwner`, keyed by the screen's route key.
319
+ - Visibility: `PageOverlayOwner` listens to the screen's `focus` / `blur`
320
+ events (they fire even while `freezeOnBlur` freezes the React tree) and
321
+ reports `setPageVisible(owner, activeWithinHost)`. "Active within host"
322
+ checks every navigator up to, but not including, the root stack, so a root
323
+ modal over Main does not hide Main's page overlays; a push or tab switch
324
+ does. Unmounting the page calls `removePage`.
325
+ - The owner may be frozen, so the host applies suspension: `OverlayPageHost`
326
+ subscribes to the store and hides entries of suspended owners natively
327
+ (`suspendedOwners`) or in the DOM (`visibility: hidden` + `inert`). Page
328
+ entries keep `visible=true` while suspended; they are not dismissed.
329
+ - Page entries live inside the React surface: iOS does not attach the
330
+ overlay's own `RCTSurfaceTouchHandler`, Android disables the entry's
331
+ `JSTouchDispatcher` (the ReactRootView already dispatches), otherwise every
332
+ touch would be delivered twice.
333
+ - iOS page sheets are drawn by the entry (bottom-pinned content view, pan to
334
+ close) because UIKit sheets always cover the window. Android and Web reuse
335
+ their global sheet implementation inside the page host.
336
+ - Android back also resolves page entries: global overlays first, then the
337
+ topmost visible page overlay.
338
+ - A page request without a resolvable host or owner falls back to global
339
+ scope.
340
+
341
+ P2 sheet notes:
342
+
343
+ - iOS presents the sheet (`.pageSheet` + one custom detent, ported from
344
+ native-sheet) from the top of its level window's presentation chain.
345
+ Non-sheet entries are subviews of the same window, so within a level,
346
+ sheets and center / toast entries stack by request order. Dismissing a
347
+ lower sheet makes UIKit dismiss the sheets presented above it; those report
348
+ `onDismissed('system')` and the store closes them.
349
+ - Android hosts the sheet in a CoordinatorLayout + BottomSheetBehavior inside
350
+ the level container (no BottomSheetDialog window). Enter / exit use the
351
+ overlay motion; dragging fades the backdrop; a drag to hidden reports
352
+ `pan`.
353
+ - Web renders a bottom surface with pointer drag (40% distance or 0.5 px/ms
354
+ flick dismisses, never from scrolled content).
355
+ - A fitted sheet measures its content with `onLayout` before presenting.
356
+ Hidden content stays mounted inside the zero-width host (clipped on iOS,
357
+ INVISIBLE on Android), and the host height stays auto: a fixed 0 height
358
+ makes Yoga measure the content at most 0 tall.
359
+ - `keepContentMounted` renders content while closed so callers can measure it
360
+ before opening (Popover waits for header / scroll measurements).
361
+ - `pan` dismissals cannot be vetoed; `onRequestDismiss('pan')` is informative.
362
+
363
+ P4a toast notes:
364
+
365
+ - Message toasts (`Toast.success` / `message` / `error` …) keep their
366
+ libraries (sonner on web, `@backpackapp-io/react-native-toast` on native)
367
+ and render inside one persistent, non-blocking `toast`-level fullscreen
368
+ entry (kit `ToastOverlayContainer`). The iOS raise tokens are gone.
369
+ - Custom toasts (`Toast.show` / `ShowCustom`) are their own `toast` entries:
370
+ transparent blocking backdrop (tap closes when `dismissOnOverlayPress`),
371
+ `scale 0.8 + offsetY -20 + fade` on `quick`, PanResponder swipe up to
372
+ close unless `disableSwipeGesture` (the Tamagui toast ignored the flag). `Toast.show` unmounts its portal on the overlay's `onClose`, after
373
+ the exit animation, instead of a 300 ms timer.
374
+
375
+ P4b dialog loading notes:
376
+
377
+ - Dialog takes an internal `overlayLevel` prop. With it, `DialogFrame`
378
+ renders through `OverlayDialogPresentation` on every platform: a sheet
379
+ (24 pt corners, no handle) on narrow windows, a centered 400 pt card
380
+ otherwise, the theme `$bgBackdrop`, Escape never closes on web, Android
381
+ back closes unless `disableSystemClose`.
382
+ - The kit DialogLoading container uses `overlayLevel="modal"`: request order
383
+ puts it above earlier dialogs, and hardware / password prompts stay above
384
+ it. Its 50 ms delays and the iOS remount key are gone; text updates in
385
+ place.
386
+ - Android sheets pad their content by the navigation-bar inset, matching the
387
+ UIKit sheet's home-indicator inset.
388
+
389
+ P1 implementation map:
390
+
391
+ | Contract | iOS | Android | Web |
392
+ | ---------------------------- | -------------------------------------------------------------- | ---------------------------------------------------------- | --------------------------------------------- |
393
+ | Level host | `NativeOverlayWindowManager.swift` | `NativeOverlayHost.kt` | `web/overlayLayers.ts` |
394
+ | Entry, hit testing, backdrop | `NativeOverlayEntryView` in `NativeOverlayContainerView.swift` | `NativeOverlayEntryRootView.kt` | `WebOverlayEntry` in `OverlayView.web.tsx` |
395
+ | Page host | `NativeOverlayPageHostView.swift` | `NativeOverlayPageHostView.kt` | `OverlayPageHost.web.tsx` |
396
+ | Sheet | `NativeOverlaySheetController.swift` | `NativeOverlayEntryRootView.configureSheet` | `OverlayView.web.tsx` + `web/useSheetDrag.ts` |
397
+ | Enter / exit animation | `NativeOverlayAnimation.swift` (`UIViewPropertyAnimator`) | `NativeOverlayAnimation.kt` (analytic spring interpolator) | `web/animateTransition.ts` (WAAPI) |
398
+ | Store bridge | `useOverlayController.ts` | same | same |
399
+
400
+ P4c dialog notes:
401
+
402
+ - Every Dialog renders through `OverlayDialogPresentation` (sheet on narrow
403
+ windows, centered card otherwise); the Tamagui Sheet / Dialog and the
404
+ NativeSheetPresentation branches of `DialogFrame` are gone. Removed
405
+ `nativeSheet`, `sheetOverlayProps` and JS portal/layering options throw at
406
+ the component and imperative API entry points, including object spreads.
407
+ - `floatingPanelProps` style the centered card (width, maxWidth, maxHeight,
408
+ bg, overflow, radius…); `zIndex`, `sheetOverlayProps`, `sheetProps`
409
+ (except `disableDrag`), `modal` and `forceMount` no longer apply.
410
+ - Level: `overlayLevel` prop, else from the portal container (`lock` for the
411
+ lock screen's container, `secure` for the password prompt's, otherwise
412
+ `modal`). `isOverTopAllViews` is redundant: every level is above app
413
+ content.
414
+ - `useInPageDialog` / `useInTabDialog` pass the page's host and owner keys:
415
+ in-page dialogs are page overlays (hidden while the page is covered).
416
+ - `Dialog.show` unmounts its portal when the exit animation ends
417
+ (`onExited`), with a 1.5 s fallback, instead of a 300 ms timer.
418
+ - Web: a blocking page overlay inerts the siblings on the path from its page
419
+ host to the app root, not the app root that contains it.
420
+ - Native sheets pad the home indicator / navigation bar and lift above the
421
+ keyboard natively; the dialog adds no keyboard padding on native (the
422
+ bounded login layout still caps its scroll view by the keyboard height).
423
+
424
+ P4d hardware / password notes:
425
+
426
+ - The hardware stage (MorphOverlay / DeviceStage) renders its portal inside
427
+ one persistent, non-blocking `hardware`-level fullscreen entry (kit
428
+ `HardwareStageOverlayContainer`); the stage's own wall blocks the app
429
+ while it is up, and taps pass through once it is gone. The iOS raise
430
+ tokens and the Android split-view offset are gone.
431
+ - Hardware dialogs (device confirm, errors, BLE permission, third-party
432
+ install / permission) use `overlayLevel: 'hardware'`: above every modal
433
+ dialog, ordered by request with the stage.
434
+ - Password setup / verify prompts use `overlayLevel: 'secure'`, above the
435
+ stage and every dialog; their web z-index and portal workarounds are gone.
436
+
437
+ P4e lock screen notes:
438
+
439
+ - kit `AppStateContainer` (one file for every platform) hosts the lock
440
+ screen and its dialog portal in a persistent blocking `lock`-level
441
+ fullscreen entry with no overlay animation (the lock screen keeps its own
442
+ AnimatePresence fade). Everything below it stays open and is covered.
443
+ - `setSecurityBlocked`, the `security` dismiss reason and
444
+ `snapshot.securityBlockedBelow` are removed.
445
+ - Dialogs opened into the lock screen's container (forgot passcode, export
446
+ logs) take the `lock` level from the container, so they render above the
447
+ lock screen; on web in the same `document.body` child the lock screen keeps
448
+ interactive (OK-62416).
449
+ - Android back is swallowed on the lock screen (blocking, not dismissible);
450
+ it no longer reaches navigation behind the lock.
451
+ - The web lock tamper check and the lock screen's body-level inert pass are
452
+ unchanged; the lock layer root is a `document.body` child.
453
+
454
+ P5a native popover notes:
455
+
456
+ - Every native Popover / ActionList / Select sheet is a native overlay sheet
457
+ (`NativeSheetPresentation`); the Tamagui Popover sheet (`TMPopover.Adapt`),
458
+ its external backdrop, z-index and back handler are gone, and
459
+ the removed `nativeSheet` option throws. iPad / wide windows keep the 400–480 pt centered
460
+ frame inside the sheet. `usingSheet={false}` still renders no panel on
461
+ native (web / desktop floating panels).
462
+ - The native popover renders inline in the caller's tree (its contexts
463
+ apply) instead of through the full-window portal. Every popover remains
464
+ mounted until the exit callback (1 s fallback); content then unmounts
465
+ unless `keepChildrenMounted`. Reopening cancels pending teardown.
466
+ - `closePopover()` waits for exit completion; repeated requests share the
467
+ same completion promise, and completion or unmount clears its timer.
468
+ - Sheet options are package-owned. Unsupported legacy `sheetProps` keys
469
+ and floating-panel `zIndex` / `portalProps` throw instead of being ignored.
470
+
471
+ P5b web popover notes:
472
+
473
+ - Web Popover / ActionList / Select render in `OverlayView` at the `modal`
474
+ level; the Tamagui Popover and `PopoverContent` are gone.
475
+ - Below `md` with `usingSheet` (the default): an overlay sheet with a
476
+ header and a dismissing backdrop.
477
+ - Otherwise: `presentation="anchored"`, non-blocking and without a backdrop.
478
+ Floating UI (`@floating-ui/dom`) places the panel: offset, flip, shift,
479
+ and a max height from the available space. `autoUpdate` follows scroll
480
+ and resize.
481
+ - The enter animation scales an ancestor of the panel, so the panel measures
482
+ smaller while it runs. The panel is positioned again on `onPresented`.
483
+ - Anchored panels close on an outside press. Presses on the trigger, on the
484
+ panel, or inside an overlay ranked above it do not close it, so a nested
485
+ Select or a Dialog opened from the panel keeps it open. They also close on
486
+ Escape, unless `hoverable`.
487
+ - `hoverable` opens and closes on mouse enter / leave of the trigger and
488
+ panel. The default close delay is 100 ms, so the pointer can cross the gap.
489
+ - `keepChildrenMounted` maps to `keepContentMounted`. On web a closed entry
490
+ stays in its layer, `visibility: hidden` and `inert`.
491
+ - `OverlayStore.resolveBack` also resolves non-blocking entries that are
492
+ dismissible, so Escape reaches an anchored panel.
493
+
494
+ P5c tooltip and nested-level notes:
495
+
496
+ - `OverlayView` provides its level to its content.
497
+ - `useEnclosingOverlayLevel()` reads that level.
498
+ - `useNestedOverlayLevel(minimum = 'modal')` returns the higher of it and
499
+ `minimum`.
500
+ - Popover (web anchored and sheet), the native popover sheet
501
+ (`NativeSheetPresentation`) and Tooltip open at that level. A popover in a
502
+ password prompt or on the lock screen then renders above it, not beneath.
503
+ - Web Tooltip renders in `OverlayView`: anchored, non-blocking, no backdrop.
504
+ Floating UI places it with offset 6, flip and shift. The Tamagui tooltip is
505
+ gone; the trigger is still a Tamagui `View`, so `triggerAsChild` works.
506
+ - Hover and keyboard focus (`:focus-visible`) open it; leaving or blurring
507
+ the trigger closes it.
508
+ - A tap toggles it on touch.
509
+ - A mouse/pen press closes it until the pointer leaves (unchanged).
510
+ - Escape and an outside press close it.
511
+ - Non-`hovering` tooltips let the pointer pass through. `hovering` ones
512
+ stay open while the pointer is over the content.
513
+ - A controlled `open` wins; requests go to `onOpenChange`.
514
+ - Native Tooltip still renders only the trigger.
515
+ - iOS: an overlay whose host view lives inside a level window resolves that
516
+ window back to the app window. Before this, closing a sheet opened from a
517
+ `secure` overlay recursed forever in `preferredStatusBarStyle`
518
+ (`_appearingOrAppearedChildModalViewController` stack overflow). The level
519
+ root controller also never forwards to itself.
520
+
521
+ P5d Spotlight notes:
522
+
523
+ - Spotlight renders in a blocking `OverlayView`: `presentation="fullscreen"`
524
+ (fade), a 0.3 black backdrop, and the nested level. Back and Escape are
525
+ swallowed, and the backdrop does not dismiss it; only "Done" ends the tour.
526
+ - It renders inline, so the highlighted copy of the trigger keeps its
527
+ contexts. The deferred-trigger plumbing and `SPOTLIGHT_OVERLAY_PORTAL`
528
+ (container and enum member) are gone.
529
+ - The Windows / Linux desktop -30 px title-bar offset is dropped: the overlay
530
+ layer covers the whole window, so `measureInWindow` coordinates line up.
531
+ Not yet checked on a Windows / Linux desktop build.
532
+
533
+ P5e full-window portal notes:
534
+
535
+ - `FULL_WINDOW_OVERLAY_PORTAL` and `TOASTER_OVERLAY_PORTAL` are only mount
536
+ roots for the imperative APIs (`Dialog.show`, `Toast.show`,
537
+ `ActionList.show`, and other `Portal.Render` callers). What mounts there
538
+ renders its own `OverlayView`. `Portal.Body` into either one throws.
539
+ - These now render their `OverlayView`-backed dialogs and toasts inline
540
+ instead of through `Portal.Body`: LocalDbDowngrade, the hardware and
541
+ third-party hardware UI state containers, and ColorPicker's mobile dialog.
542
+ - The Perp OrderBook hover summary is a non-blocking, backdrop-less,
543
+ unanimated `fullscreen` overlay at `modal`.
544
+ - The Market inline action bar (long-press on a watchlist row) is a
545
+ `fullscreen` overlay with a transparent, dismissing backdrop. Back now
546
+ dismisses it as well.
547
+ - `DialogFrame` defaults its level to the nested level, so a Dialog declared
548
+ inside another overlay opens above it. `Dialog.show` keeps the level of its
549
+ portal container.
550
+
551
+ P5f full-window container notes:
552
+
553
+ - `OverlayContainer` (react-native-screens `FullWindowOverlay` on iOS) and
554
+ `useOverlayZIndex` are removed. `Dialog.show`'s `isOverTopAllViews` is
555
+ removed: a dialog's level decides its stacking, and on iOS the flag mounted
556
+ the dialog twice. `FullWindowOverlayContainer` now only mounts
557
+ self-hosting overlays and the imperative mount roots.
558
+ - The TradingView native fullscreen chart keeps its global host, because the
559
+ chart owner's page freezes while fullscreen. The host presents each layer
560
+ in a non-blocking `fullscreen` overlay at `modal`, without animation, and
561
+ back is not dismissed. Insets come from the app's safe-area context.
562
+ - ScreenshotBranding is a non-interactive, unanimated overlay at `toast`.
563
+ - The dev overlay window and the TradingView debug panel are non-interactive
564
+ hosts at `debug`.
565
+ - The Market desktop fullscreen chart stays in place (a remount would reload
566
+ TradingView), fixed at `OVERLAY_WEB_Z_INDEX_BASE.modal - 1`.
567
+ - iOS: a touched level window becomes key, and React Native reads its window
568
+ dimensions from the key window. After a rotation while one was key (the
569
+ fullscreen chart), JS kept the landscape size, so the page stayed in its
570
+ wide layout and the native header stayed hidden. A level window now
571
+ re-reports the dimensions when it resigns key or resizes while key, by
572
+ re-assigning the app window's frame (React Native observes it).
573
+
574
+ Measurement:
575
+
576
+ - `RNCNativeOverlay` has a hand-written shadow node (`common/cpp`, codegen
577
+ `interfaceOnly`), like `<Modal>`: it is a `RootNodeKind` (measurements stop
578
+ at it) and `Unstable_uncullableView`. Its state holds the window origin of
579
+ the view the content currently lives in (level window, sheet, page host,
580
+ or the host itself while staged), returned from
581
+ `getContentOriginOffset`, so Fabric `measure` / `measureInWindow` report
582
+ on-screen frames.
583
+ - iOS reports the origin after presenting, on entry / sheet layout, and after
584
+ restaging; Android on the same events plus container layout changes. Both
585
+ read model geometry, so a running enter / exit transform is ignored.
586
+ - Web measures the DOM and needs nothing extra.
587
+ - Android builds the codegen library from `android/src/main/jni/CMakeLists.txt`
588
+ (as react-native-screens does); `jni/RNCNativeOverlay.h` shadows the
589
+ generated header so autolinking sees the custom descriptor.
590
+
591
+ Keyboard avoidance:
592
+
593
+ - One rule on every platform (`computeKeyboardShift` on web,
594
+ `NativeOverlayKeyboardShift` on iOS, `updateKeyboardShift` on Android): a
595
+ sheet rises until its content bottom clears the keyboard; other content
596
+ rises until its bottom is 16 pt above it; nothing rises above the top safe
597
+ area, so full-window content (toaster host, lock screen) never moves.
598
+ - The lift lives on a keyboard layer between the entry and its animated
599
+ content, so it composes with enter / exit transforms, and `measure`
600
+ includes it.
601
+ - iOS: a shared tracker follows `keyboardWillChangeFrame` from the first
602
+ overlay host on, animated with the keyboard's own curve; UIKit sheets keep
603
+ their native avoidance.
604
+ - Android: each entry reads IME insets (applied insets at rest, the insets
605
+ animation per frame, nothing consumed); the app runs edge-to-edge through
606
+ react-native-keyboard-controller, so the window never pans for overlays.
607
+ - Web: `visualViewport` resize / scroll on mobile browsers.
608
+ - Center dialogs put the card itself at the overlay root (auto margins), so
609
+ the native content extent is the card, not a full-window wrapper.
610
+
611
+ Status bar:
612
+
613
+ - `statusBarStyle` (`light` | `dark`) on an overlay; the topmost shown,
614
+ unsuspended entry that sets one owns the status bar
615
+ (`findStatusBarOwner`, render order: level, then request order).
616
+ - The owner mounts a React Native `<StatusBar>`, the same stack the pages
617
+ use: the app runs without view-controller-based status bar appearance on
618
+ iOS, and Android sets the window appearance. Web has no status bar.
619
+ - iOS asks the root view controller of level windows above the status bar
620
+ (`lock`, `debug`) anyway; without view-controller-based appearance it
621
+ answers with the app-wide style, so the StatusBar stack still decides.
622
+
623
+ Migration rules (decided 2026-09-28):
624
+
625
+ - No transition period. Every overlay (Dialog, Sheet, Toast, DialogLoading,
626
+ hardware stage and dialogs, password prompts, lock screen, Popover,
627
+ ActionList, Select, Spotlight) moves onto this package, and the JS
628
+ overlays (Tamagui Dialog / Sheet / Popover portals, `Portal.Container`
629
+ overlay hosts, `useOverlayZIndex`) are removed. No lint rule: each old
630
+ entry point that has to stay exported throws at runtime once its last
631
+ caller migrates, so a leftover or new use fails at once (2026-09-29).
632
+ - The global Portal exists only for the JS overlays; it is removed once
633
+ its last caller migrates.
634
+ - The app lock is unified with the rest: the lock screen renders at the
635
+ `lock` level and covers everything below it. Overlays are no longer
636
+ closed on lock (done in P4e).
637
+ - Every presentation avoids the keyboard (center, sheet, page sheet) on
638
+ iOS, Android, and web.
639
+ - Fabric `measure` / `measureInWindow` inside an overlay must report the
640
+ on-screen frame.
641
+ - The status bar follows the topmost full-screen overlay (`statusBarStyle`).
642
+ - Custom toasts honor `disableSwipeGesture`.
643
+
644
+ Accepted limitations:
645
+
646
+ - The iOS page sheet is drawn by the entry (UIKit sheets always cover the
647
+ window): it has no grabber and does not coordinate its pan with inner
648
+ scroll views. Page sheets should keep scrollable content short or pass
649
+ `dismissOnPanDown={false}`.
650
+ - Custom toasts stack by request order and overlap at the top, as before;
651
+ message toasts stack inside their libraries.
652
+
653
+ Future work (better to have):
654
+
655
+ - VoiceOver / TalkBack cross-window modality, verified on device.
656
+ - Screen-reader toasts: the native toast library renders nothing under a
657
+ screen reader today; the toast host should announce content and extend
658
+ its timer instead.
659
+
660
+ Known gaps:
661
+
662
+ - `statusBarStyle` follows React Native's StatusBar stack, so a page that
663
+ mounts a new `<StatusBar>` while an overlay owns the style, or the
664
+ imperative theme calls (`StatusBar.setBarStyle`), take over until the owner
665
+ changes; rare while a full-screen overlay covers the page.
666
+ - The podspec normalizes an npm prerelease version to its stable numeric core.
667
+ npm remains pinned to the exact prerelease; local CocoaPods never uses a
668
+ prerelease version string in its path-based dependency resolver.
669
+
670
+
671
+ ## Package migration conformance audit (2026-10-01)
672
+
673
+ - Template deviations: Fabric codegen/custom shadow nodes replace Nitro sample
674
+ code and bindings as documented above. The template's ESLint configuration
675
+ uses an unregistered Prettier plugin; use RN's exported ESLint 9 flat config
676
+ and a separate Prettier check. Import CodegenTypes from RN's public namespace
677
+ so the package typechecks under the template's strict API condition.
678
+ - Behavioral contract: no new overlay behavior in this migration. Formatting,
679
+ public codegen type imports and build metadata preserve the implementation
680
+ from the recorded app-monorepo commit. Native-sheet API remains intact.
681
+ - Passed: standalone typecheck; three Jest suites / 39 tests; lint (zero errors,
682
+ 11 advisory style warnings); Builder Bob JS/declaration build; npm tarball
683
+ contents including custom C++, Web JS, declarations and autolinking files;
684
+ 36 release-tool tests including all four overlay consumer migrations;
685
+ repository test-integrity check; immutable Yarn installation.
686
+ - Passed: full Android example `:app:assembleDebug` for arm64-v8a, including
687
+ the overlay Fabric schema, Kotlin and custom C++ autolinking.
688
+ - Passed: example CocoaPods install with NativeOverlay and the synchronized
689
+ prerelease packages. This is dependency/codegen evidence, not iOS execution.
690
+ - Previous app-monorepo Android/Web interaction evidence remains recorded in
691
+ the handoff. iOS runtime, physical devices, iPad, screen-reader cross-window
692
+ modality and real password-lock flows remain outstanding; CI adds packaging
693
+ checks and builds this pod through the example application.