@sigx/lynx-safe-area 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +217 -0
- package/android/com/sigx/safearea/SafeAreaPublisher.kt +125 -0
- package/dist/globals.d.ts +42 -0
- package/dist/globals.d.ts.map +1 -0
- package/dist/hooks.d.ts +67 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +157 -0
- package/dist/index.js.map +1 -0
- package/dist/injectable.d.ts +16 -0
- package/dist/injectable.d.ts.map +1 -0
- package/dist/provider.d.ts +44 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/safe-area-view.d.ts +37 -0
- package/dist/safe-area-view.d.ts.map +1 -0
- package/dist/types.d.ts +45 -0
- package/dist/types.d.ts.map +1 -0
- package/ios/SafeAreaPublisher.swift +189 -0
- package/package.json +44 -0
- package/sigx-module.json +14 -0
- package/src/globals.ts +70 -0
- package/src/hooks.ts +107 -0
- package/src/index.ts +30 -0
- package/src/injectable.ts +17 -0
- package/src/provider.tsx +209 -0
- package/src/safe-area-view.tsx +73 -0
- package/src/types.ts +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Andreas Ekdahl
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# @sigx/lynx-safe-area
|
|
2
|
+
|
|
3
|
+
Safe-area insets (notch, home indicator, status bar, navigation bar, keyboard) for sigx-lynx. Native publisher on iOS + Android emits insets every time they change; the JS side surfaces them as a reactive BG signal, four per-edge `SharedValue`s for MT-driven layout, and CSS variables for utility-class styling.
|
|
4
|
+
|
|
5
|
+
Mirrors React Native's `react-native-safe-area-context` API where it makes sense, but built for sigx-lynx's two-thread model so layout-bound insets don't bounce through the bridge.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @sigx/lynx-safe-area
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then declare it in your `sigx.lynx.config.ts` so prebuild auto-links the native publisher:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { defineLynxConfig } from '@sigx/lynx-cli/config';
|
|
17
|
+
|
|
18
|
+
export default defineLynxConfig({
|
|
19
|
+
modules: [
|
|
20
|
+
// ...
|
|
21
|
+
'@sigx/lynx-safe-area',
|
|
22
|
+
],
|
|
23
|
+
});
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`sigx prebuild` then copies `SafeAreaPublisher.swift` / `SafeAreaPublisher.kt` into your `ios/` and `android/` source trees and registers them in the auto-generated `GeneratedLifecyclePublishers.{swift,kt}` so they attach to every `LynxView` before first paint. No additional native wiring required.
|
|
27
|
+
|
|
28
|
+
## Quick start
|
|
29
|
+
|
|
30
|
+
Wrap your app once, anywhere above the views that need insets:
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import { defineApp } from '@sigx/lynx';
|
|
34
|
+
import { SafeAreaProvider, SafeAreaView } from '@sigx/lynx-safe-area';
|
|
35
|
+
|
|
36
|
+
defineApp(() => () => (
|
|
37
|
+
<SafeAreaProvider>
|
|
38
|
+
<SafeAreaView edges={['top', 'bottom']} class="bg-base-100 flex-1">
|
|
39
|
+
<PageContent />
|
|
40
|
+
</SafeAreaView>
|
|
41
|
+
</SafeAreaProvider>
|
|
42
|
+
));
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`<SafeAreaView>` reactively applies the current insets as `padding` (default) or `margin` to the configured `edges`. Inset-aware first paint: insets are seeded synchronously from `lynx.__globalProps` before render, so there's no flash of unsafe content.
|
|
46
|
+
|
|
47
|
+
## API
|
|
48
|
+
|
|
49
|
+
### `<SafeAreaProvider>`
|
|
50
|
+
|
|
51
|
+
Provides the context that hooks consume. Mount once at the app root.
|
|
52
|
+
|
|
53
|
+
| Prop | Type | Notes |
|
|
54
|
+
| ------- | --------------------------------- | ------------------------------------------- |
|
|
55
|
+
| `class` | `string` | Forwarded to the host `<view>`. |
|
|
56
|
+
| `style` | `Record<string, string \| number>` | Merged after the auto-injected CSS vars. |
|
|
57
|
+
|
|
58
|
+
The host view exposes the current insets as CSS variables (`--sat`, `--sar`, `--sab`, `--sal`, `--safe-area-keyboard`) — handy for utility-class consumers:
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
<SafeAreaProvider>
|
|
62
|
+
<view class="pt-[var(--sat)] pb-[var(--sab)]">…</view>
|
|
63
|
+
</SafeAreaProvider>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### `<SafeAreaView>`
|
|
67
|
+
|
|
68
|
+
Drop-in container that applies insets as padding or margin.
|
|
69
|
+
|
|
70
|
+
| Prop | Type | Default |
|
|
71
|
+
| ------- | --------------------------------- | -------------------------------- |
|
|
72
|
+
| `edges` | `('top' \| 'right' \| 'bottom' \| 'left')[]` | All four sides |
|
|
73
|
+
| `mode` | `'padding' \| 'margin'` | `'padding'` |
|
|
74
|
+
| `class` | `string` | — |
|
|
75
|
+
| `style` | `Record<string, string \| number>` | Merged after inset styles |
|
|
76
|
+
|
|
77
|
+
Implementation note: applies insets via inline style (BG signal), not via `useAnimatedStyle`. `setStyleProperties` writes that affect layout fire **after** the first layout pass, and children that capture their frame eagerly (notably `<scroll-view>`) don't reflow when insets arrive that way. Inline style avoids the timing trap.
|
|
78
|
+
|
|
79
|
+
### `useSafeAreaInsets()`
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
function useSafeAreaInsets(): PrimitiveSignal<EdgeInsets> | Computed<EdgeInsets>;
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Returns a BG-side reactive signal of `EdgeInsets`. Components calling this re-render on every inset change (rotation, keyboard show/hide, split-view resize on iPad).
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
const insets = useSafeAreaInsets();
|
|
89
|
+
return () => <view style={{ paddingTop: `${insets.value.top}px` }}>…</view>;
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
If no `<SafeAreaProvider>` is in scope, returns a signal seeded with `ZERO_INSETS` and warns in dev (so test/storybook fragments degrade gracefully instead of throwing).
|
|
93
|
+
|
|
94
|
+
### `useSafeAreaSharedValues()`
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
function useSafeAreaSharedValues(): {
|
|
98
|
+
top: SharedValue<number>;
|
|
99
|
+
right: SharedValue<number>;
|
|
100
|
+
bottom: SharedValue<number>;
|
|
101
|
+
left: SharedValue<number>;
|
|
102
|
+
} | null;
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Per-edge `SharedValue`s for MT-driven `useAnimatedStyle` bindings. Use when an animation or gesture worklet needs the current inset on MT without a BG round-trip. Returns `null` outside of `<SafeAreaProvider>`.
|
|
106
|
+
|
|
107
|
+
### `useSafeAreaFrame(viewportWidth, viewportHeight)`
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
function useSafeAreaFrame(
|
|
111
|
+
viewportWidth: number,
|
|
112
|
+
viewportHeight: number,
|
|
113
|
+
): Computed<{ x: number; y: number; width: number; height: number }>;
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Computed inner safe frame — `(x, y)` origin and `width`/`height` of the rect inside the insets. Useful for absolute-positioned overlays and modal bounds that need to know "the visible content rect", not just inset deltas.
|
|
117
|
+
|
|
118
|
+
`viewportWidth`/`viewportHeight` are caller-supplied (typically a one-time read via `@sigx/lynx-device-info`); the safe-area module deliberately doesn't pull device-info as a transitive dependency.
|
|
119
|
+
|
|
120
|
+
### `useSafeAreaInsetsMT()`
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
function useSafeAreaInsetsMT(): EdgeInsets;
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Synchronous read from inside a `'main thread'`-marked worklet. Reads `lynx.__globalProps` directly — there's no signal subscription, so callers re-evaluate per worklet invocation rather than reactively. For declarative MT-driven layout the recommended path is `<SafeAreaView>` (which composes `useSafeAreaSharedValues()` with `useAnimatedStyle`).
|
|
127
|
+
|
|
128
|
+
### Types
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
interface EdgeInsets {
|
|
132
|
+
top: number;
|
|
133
|
+
right: number;
|
|
134
|
+
bottom: number;
|
|
135
|
+
left: number;
|
|
136
|
+
/** IME (soft keyboard) height when visible, 0 when hidden. */
|
|
137
|
+
keyboard: number;
|
|
138
|
+
/** Status-bar height. Often equal to `top`, but on notched devices the
|
|
139
|
+
* safe-area top includes the notch and `statusBar` is the smaller
|
|
140
|
+
* status-only inset. */
|
|
141
|
+
statusBar: number;
|
|
142
|
+
/** Navigation-bar height (Android gesture/3-button nav at bottom). */
|
|
143
|
+
navigationBar: number;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const ZERO_INSETS: EdgeInsets;
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
All values are in dp/pt (logical pixels), not raw pixels.
|
|
150
|
+
|
|
151
|
+
### Lower-level escape hatches
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
import { readGlobalSafeArea, GLOBAL_PROPS_KEY } from '@sigx/lynx-safe-area';
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
- `readGlobalSafeArea()` — synchronous one-shot read from `lynx.__globalProps`. Returns `EdgeInsets` (zeros if the publisher hasn't run yet). What `<SafeAreaProvider>` uses to seed initial values.
|
|
158
|
+
- `GLOBAL_PROPS_KEY` — the key the native publisher writes under. Exported for tests/debugging.
|
|
159
|
+
|
|
160
|
+
## CSS variables
|
|
161
|
+
|
|
162
|
+
The provider's host view exposes these on the element style — descendant selectors inherit them via the cascade:
|
|
163
|
+
|
|
164
|
+
| Variable | Maps to |
|
|
165
|
+
| ----------------------- | ---------------------------------------- |
|
|
166
|
+
| `--sat` | `insets.top` (in px) |
|
|
167
|
+
| `--sar` | `insets.right` |
|
|
168
|
+
| `--sab` | `insets.bottom` |
|
|
169
|
+
| `--sal` | `insets.left` |
|
|
170
|
+
| `--safe-area-keyboard` | `insets.keyboard` |
|
|
171
|
+
|
|
172
|
+
Works uniformly across iOS and Android — upstream's `env(safe-area-inset-*)` is iOS-only, so this is what you reach for if you're using DaisyUI/Tailwind utilities like `pt-[var(--sat)]`.
|
|
173
|
+
|
|
174
|
+
## How it works
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
┌──────────────────────────────────────┐
|
|
178
|
+
│ Native (iOS UIView / Android View) │
|
|
179
|
+
│ - SafeAreaPublisher attached to │
|
|
180
|
+
│ LynxView at construction │
|
|
181
|
+
│ - On each insets/keyboard change: │
|
|
182
|
+
│ ┌──────────────────────────────┐ │
|
|
183
|
+
│ │ updateGlobalProps({safeArea})│ │
|
|
184
|
+
│ │ + emit 'safeAreaChanged' │ │
|
|
185
|
+
│ └──────────────────────────────┘ │
|
|
186
|
+
└──────────────────────────────────────┘
|
|
187
|
+
│
|
|
188
|
+
▼
|
|
189
|
+
┌──────────────────────────────────────┐
|
|
190
|
+
│ JS (BG thread) │
|
|
191
|
+
│ ┌─────────────────┐ ┌──────────────┐ │
|
|
192
|
+
│ │ readGlobal- │ │ Global- │ │
|
|
193
|
+
│ │ SafeArea() seed │ │ EventEmitter │ │
|
|
194
|
+
│ │ (sync, before │ │ subscription │ │
|
|
195
|
+
│ │ first render) │ │ │ │
|
|
196
|
+
│ └────────┬────────┘ └──────┬───────┘ │
|
|
197
|
+
│ │ │ │
|
|
198
|
+
│ ▼ ▼ │
|
|
199
|
+
│ ┌──────────────────────────┐ │
|
|
200
|
+
│ │ runOnMainThread worklet │ │
|
|
201
|
+
│ │ writes 4 per-edge SVs │ │
|
|
202
|
+
│ └────────────┬─────────────┘ │
|
|
203
|
+
│ │ │
|
|
204
|
+
│ ▼ │
|
|
205
|
+
│ SharedValue diff → BG signal │
|
|
206
|
+
│ mirror → computed → re-render │
|
|
207
|
+
│ useSafeAreaInsets() consumers │
|
|
208
|
+
└──────────────────────────────────────┘
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Why `SharedValue`s for the four edges but a plain `signal` for keyboard/statusBar/navigationBar? The four edges drive layout (`<SafeAreaView>` wants to write padding from a worklet on every flush) and the SV bridge is the right tool for that. The extras are informational — keyboard already lives in `bottom` on iOS, statusBar/navigationBar are decorative — so the SV plumbing isn't worth the cost there.
|
|
212
|
+
|
|
213
|
+
A custom `safeAreaChanged` event is used instead of upstream's `onGlobalPropsChanged` because the upstream event-name conventions have churned across Lynx releases and we want the contract in our hands.
|
|
214
|
+
|
|
215
|
+
## Reference app
|
|
216
|
+
|
|
217
|
+
`examples/lynx-one/my-sigx-app/src/App.tsx` mounts `<SafeAreaProvider>` and a `<SafeAreaView>` for the page chrome — useful as a copy-paste reference and as the smoke-test target when porting the publisher to a new platform.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
package com.sigx.safearea
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
import androidx.core.view.ViewCompat
|
|
5
|
+
import androidx.core.view.WindowInsetsCompat
|
|
6
|
+
import com.lynx.react.bridge.JavaOnlyArray
|
|
7
|
+
import com.lynx.react.bridge.JavaOnlyMap
|
|
8
|
+
import com.lynx.tasm.LynxView
|
|
9
|
+
import com.lynx.tasm.TemplateData
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Publishes the device safe-area insets (status bar, navigation bar, gesture
|
|
13
|
+
* inset, IME / soft keyboard) to JS via two channels:
|
|
14
|
+
*
|
|
15
|
+
* 1. **`lynx.__globalProps.safeArea`** — populated synchronously via
|
|
16
|
+
* [LynxView.updateGlobalProps]. The MT bundle reads `__globalProps`
|
|
17
|
+
* synchronously before its first render, giving inset-aware first paint
|
|
18
|
+
* with no flash of unsafe content.
|
|
19
|
+
*
|
|
20
|
+
* 2. **`safeAreaChanged` global event** — fired via [LynxView.sendGlobalEvent]
|
|
21
|
+
* after each republish. The JS `<SafeAreaProvider>` subscribes via
|
|
22
|
+
* `lynx.getJSModule("GlobalEventEmitter").addListener` and updates its
|
|
23
|
+
* reactive signal. Drives live updates on rotation, multi-window
|
|
24
|
+
* split-screen, foldable hinge state, IME show/hide.
|
|
25
|
+
*
|
|
26
|
+
* Lifecycle: instantiate one publisher per [LynxView]; call [attach] *before*
|
|
27
|
+
* `renderTemplateUrl` so the initial inset map is in `__globalProps` before
|
|
28
|
+
* MT first paint. The publisher hooks `OnApplyWindowInsetsListener` on the
|
|
29
|
+
* LynxView itself, which delivers updates as soon as the view is laid out
|
|
30
|
+
* and on every subsequent insets change.
|
|
31
|
+
*
|
|
32
|
+
* Fills the gap from upstream Lynx: as of LynxJS 3.6 the C++ renderer
|
|
33
|
+
* doesn't populate `env(safe-area-inset-*)` on Android (open PR #5296).
|
|
34
|
+
* Going through `__globalProps` works on every supported Android version.
|
|
35
|
+
*/
|
|
36
|
+
class SafeAreaPublisher(private val lynxView: LynxView) {
|
|
37
|
+
|
|
38
|
+
private companion object {
|
|
39
|
+
const val TAG = "SafeAreaPublisher"
|
|
40
|
+
const val EVENT_NAME = "safeAreaChanged"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private var lastInsets: WindowInsetsCompat? = null
|
|
44
|
+
private var lastKeyboard: Float = 0f
|
|
45
|
+
|
|
46
|
+
fun attach() {
|
|
47
|
+
// Set the listener BEFORE the LynxView is added to a window so we
|
|
48
|
+
// catch the initial WindowInsets dispatch. The listener returns the
|
|
49
|
+
// insets unchanged so other consumers in the view tree still see them.
|
|
50
|
+
ViewCompat.setOnApplyWindowInsetsListener(lynxView) { _, insets ->
|
|
51
|
+
lastInsets = insets
|
|
52
|
+
publish(insets)
|
|
53
|
+
insets
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Force a publish in case insets were already delivered before the
|
|
57
|
+
// listener attached (happens when LynxView is reattached during HMR
|
|
58
|
+
// / activity recreation). ViewCompat.getRootWindowInsets is
|
|
59
|
+
// null-safe and returns whatever the platform last computed.
|
|
60
|
+
ViewCompat.getRootWindowInsets(lynxView)?.let { publish(it) }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private fun publish(insets: WindowInsetsCompat) {
|
|
64
|
+
val density = lynxView.resources.displayMetrics.density.takeIf { it > 0 } ?: 1f
|
|
65
|
+
|
|
66
|
+
// Lynx layout works in density-independent pixels; WindowInsets
|
|
67
|
+
// delivers physical pixels. Divide by density once at the boundary
|
|
68
|
+
// so the JS side gets dp values that map 1:1 to its other layout
|
|
69
|
+
// numbers (preferredLayoutWidth, etc.).
|
|
70
|
+
val sys = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
|
71
|
+
val ime = insets.getInsets(WindowInsetsCompat.Type.ime())
|
|
72
|
+
val cutout = insets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
|
73
|
+
val gestures = insets.getInsets(WindowInsetsCompat.Type.systemGestures())
|
|
74
|
+
|
|
75
|
+
// Top: max of system-bar (status bar) and display cutout (notch).
|
|
76
|
+
// The two can disagree on devices where the notch extends slightly
|
|
77
|
+
// into the status-bar area; taking the max gives a conservative
|
|
78
|
+
// safe area that always clears both.
|
|
79
|
+
val top = maxOf(sys.top, cutout.top) / density
|
|
80
|
+
val right = maxOf(sys.right, cutout.right) / density
|
|
81
|
+
val bottom = maxOf(sys.bottom, cutout.bottom, gestures.bottom) / density
|
|
82
|
+
val left = maxOf(sys.left, cutout.left) / density
|
|
83
|
+
val keyboard = ime.bottom / density
|
|
84
|
+
lastKeyboard = keyboard
|
|
85
|
+
|
|
86
|
+
val map: Map<String, Any> = mapOf(
|
|
87
|
+
"top" to top.toDouble(),
|
|
88
|
+
"right" to right.toDouble(),
|
|
89
|
+
"bottom" to bottom.toDouble(),
|
|
90
|
+
"left" to left.toDouble(),
|
|
91
|
+
"keyboard" to keyboard.toDouble(),
|
|
92
|
+
"statusBar" to (sys.top / density).toDouble(),
|
|
93
|
+
"navigationBar" to (sys.bottom / density).toDouble(),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
// Channel 1: __globalProps — sync MT read at first paint.
|
|
98
|
+
lynxView.updateGlobalProps(TemplateData.fromMap(mapOf("safeArea" to map)))
|
|
99
|
+
|
|
100
|
+
// Channel 2: safeAreaChanged event — live update for the BG-side
|
|
101
|
+
// <SafeAreaProvider> listener. sendGlobalEvent expects a
|
|
102
|
+
// JavaOnlyArray (Lynx bridge type), not a Kotlin List. The
|
|
103
|
+
// first array element is what GlobalEventEmitter.addListener
|
|
104
|
+
// delivers as the listener's first argument.
|
|
105
|
+
val payload = JavaOnlyMap().apply {
|
|
106
|
+
for ((k, v) in map) {
|
|
107
|
+
when (v) {
|
|
108
|
+
is Double -> putDouble(k, v)
|
|
109
|
+
is Int -> putInt(k, v)
|
|
110
|
+
is Boolean -> putBoolean(k, v)
|
|
111
|
+
is String -> putString(k, v)
|
|
112
|
+
else -> putString(k, v.toString())
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
val params = JavaOnlyArray().apply { pushMap(payload) }
|
|
117
|
+
lynxView.sendGlobalEvent(EVENT_NAME, params)
|
|
118
|
+
} catch (e: Throwable) {
|
|
119
|
+
// Defensive: if the LynxView is mid-teardown the bridge call
|
|
120
|
+
// can throw. Don't crash the host app over a missed inset
|
|
121
|
+
// notification — the next layout pass will re-publish.
|
|
122
|
+
Log.w(TAG, "publish failed: ${e.message}")
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type EdgeInsets } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The key under `lynx.__globalProps` where the native publisher writes the
|
|
4
|
+
* inset map. Kept as a constant so iOS/Android publishers, the JS reader, and
|
|
5
|
+
* tests all agree on a single string.
|
|
6
|
+
*/
|
|
7
|
+
export declare const GLOBAL_PROPS_KEY = "safeArea";
|
|
8
|
+
/**
|
|
9
|
+
* Shape of the safe-area sub-object the native publishers write to
|
|
10
|
+
* `lynx.__globalProps[GLOBAL_PROPS_KEY]`. Some fields may be absent on
|
|
11
|
+
* platforms that don't expose them (e.g. Android pre-31 navigation-bar API);
|
|
12
|
+
* the reader fills missing keys with 0.
|
|
13
|
+
*/
|
|
14
|
+
export interface RawSafeAreaProps {
|
|
15
|
+
top?: number;
|
|
16
|
+
right?: number;
|
|
17
|
+
bottom?: number;
|
|
18
|
+
left?: number;
|
|
19
|
+
keyboard?: number;
|
|
20
|
+
statusBar?: number;
|
|
21
|
+
navigationBar?: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Synchronously read the current safe-area insets from `lynx.__globalProps`.
|
|
25
|
+
*
|
|
26
|
+
* Returns `ZERO_INSETS` when the publisher hasn't populated yet, when the
|
|
27
|
+
* package is bundled into a non-Lynx host (web preview, SSR), or when the
|
|
28
|
+
* host runtime omits the global. All callers must be prepared for the
|
|
29
|
+
* zero-fallback — it's the natural state during cold start before the native
|
|
30
|
+
* publisher has fired its first `updateGlobalProps`.
|
|
31
|
+
*
|
|
32
|
+
* Safe to call from both the Background Thread (BG) and the Main Thread
|
|
33
|
+
* (MT), since `lynx.__globalProps` is mirrored across both. Sync read on MT
|
|
34
|
+
* is what gives us inset-aware first paint.
|
|
35
|
+
*
|
|
36
|
+
* The `lynx` symbol is a closure-injected identifier (provided by
|
|
37
|
+
* `@lynx-js/runtime-wrapper-webpack-plugin`'s `__init_card_bundle__`
|
|
38
|
+
* wrapper), NOT a property of `globalThis`. Access it as a bare identifier
|
|
39
|
+
* with a `typeof` guard — same pattern used by `runtime-lynx/src/bg-bridge.ts`.
|
|
40
|
+
*/
|
|
41
|
+
export declare function readGlobalSafeArea(): EdgeInsets;
|
|
42
|
+
//# sourceMappingURL=globals.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"globals.d.ts","sourceRoot":"","sources":["../src/globals.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,UAAU,EAAe,MAAM,YAAY,CAAC;AAE1D;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,aAAa,CAAC;AAE3C;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AASD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,IAAI,UAAU,CAe/C"}
|
package/dist/hooks.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { type Computed, type PrimitiveSignal } from '@sigx/reactivity';
|
|
2
|
+
import { type EdgeInsets, type SafeAreaContextValue } from './types.js';
|
|
3
|
+
type InsetsRead = PrimitiveSignal<EdgeInsets> | Computed<EdgeInsets>;
|
|
4
|
+
/**
|
|
5
|
+
* BG-side reactive read of current safe-area insets. Returns the live signal
|
|
6
|
+
* — components calling this re-render on inset change (rotation, keyboard,
|
|
7
|
+
* split-view).
|
|
8
|
+
*
|
|
9
|
+
* If no `<SafeAreaProvider>` is in scope, returns a signal seeded with
|
|
10
|
+
* `ZERO_INSETS` and warns in dev. (We don't throw because mounting an app
|
|
11
|
+
* fragment for tests/storybook without a provider is convenient and the
|
|
12
|
+
* zero fallback degrades gracefully.)
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```tsx
|
|
16
|
+
* const insets = useSafeAreaInsets();
|
|
17
|
+
* return () => <view style={{ paddingTop: insets.value.top }} />;
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export declare function useSafeAreaInsets(): InsetsRead;
|
|
21
|
+
/**
|
|
22
|
+
* Per-edge SharedValues for MT-driven `useAnimatedStyle` bindings — the
|
|
23
|
+
* recommended path for `<SafeAreaView>`-style layouts that need padding to
|
|
24
|
+
* track insets without a BG re-render. See `safe-area-view.tsx` for the
|
|
25
|
+
* canonical consumer.
|
|
26
|
+
*/
|
|
27
|
+
export declare function useSafeAreaSharedValues(): SafeAreaContextValue['sv'] | null;
|
|
28
|
+
/**
|
|
29
|
+
* Computed signal of the inner safe frame (origin + size) in dp/pt.
|
|
30
|
+
*
|
|
31
|
+
* Useful for absolute-positioned overlays, modal bounds, and layout math
|
|
32
|
+
* that needs to know "the visible content rect" rather than just the inset
|
|
33
|
+
* deltas. The frame size is computed from the host viewport — you must pass
|
|
34
|
+
* `viewportWidth`/`viewportHeight` (typically read once via
|
|
35
|
+
* `@sigx/lynx-device-info`) since the safe-area module deliberately avoids
|
|
36
|
+
* pulling that whole dependency.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```tsx
|
|
40
|
+
* const { screenWidth, screenHeight } = await DeviceInfo.getInfo();
|
|
41
|
+
* const frame = useSafeAreaFrame(screenWidth, screenHeight);
|
|
42
|
+
* return () => <view style={{
|
|
43
|
+
* position: 'absolute',
|
|
44
|
+
* top: frame.value.y, left: frame.value.x,
|
|
45
|
+
* width: frame.value.width, height: frame.value.height,
|
|
46
|
+
* }} />;
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export declare function useSafeAreaFrame(viewportWidth: number, viewportHeight: number): Computed<{
|
|
50
|
+
x: number;
|
|
51
|
+
y: number;
|
|
52
|
+
width: number;
|
|
53
|
+
height: number;
|
|
54
|
+
}>;
|
|
55
|
+
/**
|
|
56
|
+
* **MT-thread** synchronous read of the current safe-area insets. For use
|
|
57
|
+
* inside `'main thread'`-marked worklet bodies. Reads `lynx.__globalProps`
|
|
58
|
+
* directly — there's no signal subscription, so callers re-evaluate per
|
|
59
|
+
* worklet invocation rather than reactively.
|
|
60
|
+
*
|
|
61
|
+
* For declarative MT-driven layout (the common case), prefer
|
|
62
|
+
* `<SafeAreaView edges={…}>`, which composes `useSafeAreaSharedValues()`
|
|
63
|
+
* with `useAnimatedStyle` — that path is reactive and applies on flush.
|
|
64
|
+
*/
|
|
65
|
+
export declare function useSafeAreaInsetsMT(): EdgeInsets;
|
|
66
|
+
export {};
|
|
67
|
+
//# sourceMappingURL=hooks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,KAAK,QAAQ,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAGjF,OAAO,EAAe,KAAK,UAAU,EAAE,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAErF,KAAK,UAAU,GAAG,eAAe,CAAC,UAAU,CAAC,GAAG,QAAQ,CAAC,UAAU,CAAC,CAAC;AAErE;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,iBAAiB,IAAI,UAAU,CAI9C;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,IAAI,oBAAoB,CAAC,IAAI,CAAC,GAAG,IAAI,CAG3E;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,gBAAgB,CAC9B,aAAa,EAAE,MAAM,EACrB,cAAc,EAAE,MAAM,GACrB,QAAQ,CAAC;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAWnE;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,IAAI,UAAU,CAEhD"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { SafeAreaProvider, SAFE_AREA_EVENT } from './provider.js';
|
|
2
|
+
export type { SafeAreaProviderProps } from './provider.js';
|
|
3
|
+
export { SafeAreaView } from './safe-area-view.js';
|
|
4
|
+
export type { SafeAreaViewProps } from './safe-area-view.js';
|
|
5
|
+
export { useSafeAreaInsets, useSafeAreaSharedValues, useSafeAreaFrame, useSafeAreaInsetsMT, } from './hooks.js';
|
|
6
|
+
export { useSafeAreaContext } from './injectable.js';
|
|
7
|
+
export { readGlobalSafeArea, GLOBAL_PROPS_KEY, } from './globals.js';
|
|
8
|
+
export type { RawSafeAreaProps } from './globals.js';
|
|
9
|
+
export { ZERO_INSETS } from './types.js';
|
|
10
|
+
export type { EdgeInsets, Edge, SafeAreaMode, SafeAreaContextValue, } from './types.js';
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAClE,YAAY,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAE3D,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,YAAY,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAE7D,OAAO,EACL,iBAAiB,EACjB,uBAAuB,EACvB,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAErD,OAAO,EACL,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,cAAc,CAAC;AACtB,YAAY,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,YAAY,EACV,UAAU,EACV,IAAI,EACJ,YAAY,EACZ,oBAAoB,GACrB,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { component as e, computed as t, defineInjectable as n, defineProvide as r, onMounted as i, onUnmounted as a, runOnMainThread as o, signal as s, useMainThreadRef as c, useSharedValue as l } from "@sigx/lynx";
|
|
2
|
+
import { jsx as u } from "@sigx/lynx/jsx-runtime";
|
|
3
|
+
import { computed as d } from "@sigx/reactivity";
|
|
4
|
+
//#region src/injectable.ts
|
|
5
|
+
var f = n(() => null), p = {
|
|
6
|
+
top: 0,
|
|
7
|
+
right: 0,
|
|
8
|
+
bottom: 0,
|
|
9
|
+
left: 0,
|
|
10
|
+
keyboard: 0,
|
|
11
|
+
statusBar: 0,
|
|
12
|
+
navigationBar: 0
|
|
13
|
+
}, m = "safeArea";
|
|
14
|
+
function h() {
|
|
15
|
+
let e = (typeof lynx < "u" ? lynx : void 0)?.__globalProps?.[m];
|
|
16
|
+
return !e || typeof e != "object" ? p : {
|
|
17
|
+
top: g(e.top),
|
|
18
|
+
right: g(e.right),
|
|
19
|
+
bottom: g(e.bottom),
|
|
20
|
+
left: g(e.left),
|
|
21
|
+
keyboard: g(e.keyboard),
|
|
22
|
+
statusBar: g(e.statusBar),
|
|
23
|
+
navigationBar: g(e.navigationBar)
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function g(e) {
|
|
27
|
+
return typeof e == "number" && Number.isFinite(e) ? e : 0;
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/provider.tsx
|
|
31
|
+
var _ = "safeAreaChanged", v = e(({ props: e, slots: n }) => {
|
|
32
|
+
let d = h(), p = l(d.top), m = l(d.right), g = l(d.bottom), v = l(d.left), b = s({
|
|
33
|
+
keyboard: d.keyboard,
|
|
34
|
+
statusBar: d.statusBar,
|
|
35
|
+
navigationBar: d.navigationBar
|
|
36
|
+
}), S = t(() => ({
|
|
37
|
+
top: p.value,
|
|
38
|
+
right: m.value,
|
|
39
|
+
bottom: g.value,
|
|
40
|
+
left: v.value,
|
|
41
|
+
keyboard: b.keyboard,
|
|
42
|
+
statusBar: b.statusBar,
|
|
43
|
+
navigationBar: b.navigationBar
|
|
44
|
+
})), C = {
|
|
45
|
+
insets: S,
|
|
46
|
+
sv: {
|
|
47
|
+
top: p,
|
|
48
|
+
right: m,
|
|
49
|
+
bottom: g,
|
|
50
|
+
left: v
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
r(f, () => C);
|
|
54
|
+
let w = o((e, t, n, r) => {
|
|
55
|
+
"main thread";
|
|
56
|
+
p.current.value = e, m.current.value = t, g.current.value = n, v.current.value = r;
|
|
57
|
+
}), T = c(null), E, D;
|
|
58
|
+
return i(() => {
|
|
59
|
+
D = (typeof lynx < "u" ? lynx : void 0)?.getJSModule?.("GlobalEventEmitter"), D && (E = (e) => {
|
|
60
|
+
let t = y(e, S.value);
|
|
61
|
+
b.$set({
|
|
62
|
+
keyboard: t.keyboard,
|
|
63
|
+
statusBar: t.statusBar,
|
|
64
|
+
navigationBar: t.navigationBar
|
|
65
|
+
}), w(t.top, t.right, t.bottom, t.left);
|
|
66
|
+
}, D.addListener(_, E));
|
|
67
|
+
}), a(() => {
|
|
68
|
+
D && E && D.removeListener(_, E);
|
|
69
|
+
}), () => /* @__PURE__ */ u("view", {
|
|
70
|
+
class: e.class,
|
|
71
|
+
"main-thread:ref": T,
|
|
72
|
+
style: x(S.value, e.style),
|
|
73
|
+
children: n.default?.()
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
function y(e, t) {
|
|
77
|
+
if (!e || typeof e != "object") return t;
|
|
78
|
+
let n = e;
|
|
79
|
+
return {
|
|
80
|
+
top: b(n.top, t.top),
|
|
81
|
+
right: b(n.right, t.right),
|
|
82
|
+
bottom: b(n.bottom, t.bottom),
|
|
83
|
+
left: b(n.left, t.left),
|
|
84
|
+
keyboard: b(n.keyboard, t.keyboard),
|
|
85
|
+
statusBar: b(n.statusBar, t.statusBar),
|
|
86
|
+
navigationBar: b(n.navigationBar, t.navigationBar)
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function b(e, t) {
|
|
90
|
+
return typeof e == "number" && Number.isFinite(e) ? e : t;
|
|
91
|
+
}
|
|
92
|
+
function x(e, t) {
|
|
93
|
+
let n = {
|
|
94
|
+
"--sat": `${e.top}px`,
|
|
95
|
+
"--sar": `${e.right}px`,
|
|
96
|
+
"--sab": `${e.bottom}px`,
|
|
97
|
+
"--sal": `${e.left}px`,
|
|
98
|
+
"--safe-area-keyboard": `${e.keyboard}px`
|
|
99
|
+
};
|
|
100
|
+
return t ? {
|
|
101
|
+
...n,
|
|
102
|
+
...t
|
|
103
|
+
} : n;
|
|
104
|
+
}
|
|
105
|
+
//#endregion
|
|
106
|
+
//#region src/hooks.ts
|
|
107
|
+
function S() {
|
|
108
|
+
let e = f();
|
|
109
|
+
return e ? e.insets : D();
|
|
110
|
+
}
|
|
111
|
+
function C() {
|
|
112
|
+
return f()?.sv ?? null;
|
|
113
|
+
}
|
|
114
|
+
function w(e, t) {
|
|
115
|
+
let n = S();
|
|
116
|
+
return d(() => {
|
|
117
|
+
let r = n.value;
|
|
118
|
+
return {
|
|
119
|
+
x: r.left,
|
|
120
|
+
y: r.top,
|
|
121
|
+
width: Math.max(0, e - r.left - r.right),
|
|
122
|
+
height: Math.max(0, t - r.top - r.bottom - r.keyboard)
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
function T() {
|
|
127
|
+
return h();
|
|
128
|
+
}
|
|
129
|
+
var E;
|
|
130
|
+
function D() {
|
|
131
|
+
return E ||= (globalThis.process?.env?.NODE_ENV !== "production" && console.warn("[sigx-safe-area] useSafeAreaInsets() called outside <SafeAreaProvider>. Returning ZERO_INSETS. Wrap your app in <SafeAreaProvider> to receive live device insets."), d(() => p)), E;
|
|
132
|
+
}
|
|
133
|
+
//#endregion
|
|
134
|
+
//#region src/safe-area-view.tsx
|
|
135
|
+
var O = [
|
|
136
|
+
"top",
|
|
137
|
+
"right",
|
|
138
|
+
"bottom",
|
|
139
|
+
"left"
|
|
140
|
+
], k = e(({ props: e, slots: t }) => {
|
|
141
|
+
let n = S(), r = e.edges ?? O, i = e.mode ?? "padding";
|
|
142
|
+
return () => {
|
|
143
|
+
let a = n.value, o = {};
|
|
144
|
+
return r.includes("top") && (o[i === "padding" ? "paddingTop" : "marginTop"] = `${a.top}px`), r.includes("right") && (o[i === "padding" ? "paddingRight" : "marginRight"] = `${a.right}px`), r.includes("bottom") && (o[i === "padding" ? "paddingBottom" : "marginBottom"] = `${a.bottom}px`), r.includes("left") && (o[i === "padding" ? "paddingLeft" : "marginLeft"] = `${a.left}px`), /* @__PURE__ */ u("view", {
|
|
145
|
+
class: e.class,
|
|
146
|
+
style: e.style ? {
|
|
147
|
+
...e.style,
|
|
148
|
+
...o
|
|
149
|
+
} : o,
|
|
150
|
+
children: t.default?.()
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
//#endregion
|
|
155
|
+
export { m as GLOBAL_PROPS_KEY, _ as SAFE_AREA_EVENT, v as SafeAreaProvider, k as SafeAreaView, p as ZERO_INSETS, h as readGlobalSafeArea, f as useSafeAreaContext, w as useSafeAreaFrame, S as useSafeAreaInsets, T as useSafeAreaInsetsMT, C as useSafeAreaSharedValues };
|
|
156
|
+
|
|
157
|
+
//# sourceMappingURL=index.js.map
|