@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 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"}
@@ -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"}
@@ -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