@sigx/lynx-safe-area 0.4.2 → 0.4.4
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/README.md +0 -55
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +51 -8
- package/dist/provider.js.map +1 -1
- package/package.json +6 -6
- package/src/provider.tsx +53 -8
package/README.md
CHANGED
|
@@ -1,25 +1,16 @@
|
|
|
1
1
|
# @sigx/lynx-safe-area
|
|
2
|
-
|
|
3
2
|
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
3
|
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
4
|
## Install
|
|
8
|
-
|
|
9
5
|
```bash
|
|
10
6
|
pnpm add @sigx/lynx-safe-area
|
|
11
7
|
```
|
|
12
|
-
|
|
13
8
|
`sigx prebuild` auto-discovers the package, 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.
|
|
14
|
-
|
|
15
9
|
## Quick start
|
|
16
|
-
|
|
17
10
|
Wrap your app once, anywhere above the views that need insets:
|
|
18
|
-
|
|
19
11
|
```tsx
|
|
20
12
|
import { defineApp } from '@sigx/lynx';
|
|
21
13
|
import { SafeAreaProvider, SafeAreaView } from '@sigx/lynx-safe-area';
|
|
22
|
-
|
|
23
14
|
defineApp(() => () => (
|
|
24
15
|
<SafeAreaProvider>
|
|
25
16
|
<SafeAreaView edges={['top', 'bottom']} class="bg-base-100">
|
|
@@ -28,64 +19,45 @@ defineApp(() => () => (
|
|
|
28
19
|
</SafeAreaProvider>
|
|
29
20
|
));
|
|
30
21
|
```
|
|
31
|
-
|
|
32
22
|
`<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.
|
|
33
|
-
|
|
34
23
|
**Sensible layout defaults** — `<SafeAreaProvider>` defaults its host
|
|
35
24
|
view to `height: 100vh` + `flex-direction: column`, and `<SafeAreaView>`
|
|
36
25
|
defaults to flex-fill long-form. Consumers don't need to add inline
|
|
37
26
|
`height: '100vh'` anchors or `flex-1` classes for the layout chain to
|
|
38
27
|
work. Pass `style={…}` to override.
|
|
39
|
-
|
|
40
28
|
## API
|
|
41
|
-
|
|
42
29
|
### `<SafeAreaProvider>`
|
|
43
|
-
|
|
44
30
|
Provides the context that hooks consume. Mount once at the app root.
|
|
45
|
-
|
|
46
31
|
| Prop | Type | Notes |
|
|
47
32
|
| ------- | --------------------------------- | ------------------------------------------- |
|
|
48
33
|
| `class` | `string` | Forwarded to the host `<view>`. |
|
|
49
34
|
| `style` | `Record<string, string \| number>` | Merged after the auto-injected CSS vars. |
|
|
50
|
-
|
|
51
35
|
The host view exposes the current insets as CSS variables (`--sat`, `--sar`, `--sab`, `--sal`, `--safe-area-keyboard`) — handy for utility-class consumers:
|
|
52
|
-
|
|
53
36
|
```tsx
|
|
54
37
|
<SafeAreaProvider>
|
|
55
38
|
<view class="pt-[var(--sat)] pb-[var(--sab)]">…</view>
|
|
56
39
|
</SafeAreaProvider>
|
|
57
40
|
```
|
|
58
|
-
|
|
59
41
|
### `<SafeAreaView>`
|
|
60
|
-
|
|
61
42
|
Drop-in container that applies insets as padding or margin.
|
|
62
|
-
|
|
63
43
|
| Prop | Type | Default |
|
|
64
44
|
| ------- | --------------------------------- | -------------------------------- |
|
|
65
45
|
| `edges` | `('top' \| 'right' \| 'bottom' \| 'left')[]` | All four sides |
|
|
66
46
|
| `mode` | `'padding' \| 'margin'` | `'padding'` |
|
|
67
47
|
| `class` | `string` | — |
|
|
68
48
|
| `style` | `Record<string, string \| number>` | Merged after inset styles |
|
|
69
|
-
|
|
70
49
|
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.
|
|
71
|
-
|
|
72
50
|
### `useSafeAreaInsets()`
|
|
73
|
-
|
|
74
51
|
```ts
|
|
75
52
|
function useSafeAreaInsets(): PrimitiveSignal<EdgeInsets> | Computed<EdgeInsets>;
|
|
76
53
|
```
|
|
77
|
-
|
|
78
54
|
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).
|
|
79
|
-
|
|
80
55
|
```tsx
|
|
81
56
|
const insets = useSafeAreaInsets();
|
|
82
57
|
return () => <view style={{ paddingTop: `${insets.value.top}px` }}>…</view>;
|
|
83
58
|
```
|
|
84
|
-
|
|
85
59
|
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).
|
|
86
|
-
|
|
87
60
|
### `useSafeAreaSharedValues()`
|
|
88
|
-
|
|
89
61
|
```ts
|
|
90
62
|
function useSafeAreaSharedValues(): {
|
|
91
63
|
top: SharedValue<number>;
|
|
@@ -94,32 +66,22 @@ function useSafeAreaSharedValues(): {
|
|
|
94
66
|
left: SharedValue<number>;
|
|
95
67
|
} | null;
|
|
96
68
|
```
|
|
97
|
-
|
|
98
69
|
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>`.
|
|
99
|
-
|
|
100
70
|
### `useSafeAreaFrame(viewportWidth, viewportHeight)`
|
|
101
|
-
|
|
102
71
|
```ts
|
|
103
72
|
function useSafeAreaFrame(
|
|
104
73
|
viewportWidth: number,
|
|
105
74
|
viewportHeight: number,
|
|
106
75
|
): Computed<{ x: number; y: number; width: number; height: number }>;
|
|
107
76
|
```
|
|
108
|
-
|
|
109
77
|
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.
|
|
110
|
-
|
|
111
78
|
`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.
|
|
112
|
-
|
|
113
79
|
### `useSafeAreaInsetsMT()`
|
|
114
|
-
|
|
115
80
|
```ts
|
|
116
81
|
function useSafeAreaInsetsMT(): EdgeInsets;
|
|
117
82
|
```
|
|
118
|
-
|
|
119
83
|
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`).
|
|
120
|
-
|
|
121
84
|
### Types
|
|
122
|
-
|
|
123
85
|
```ts
|
|
124
86
|
interface EdgeInsets {
|
|
125
87
|
top: number;
|
|
@@ -135,25 +97,17 @@ interface EdgeInsets {
|
|
|
135
97
|
/** Navigation-bar height (Android gesture/3-button nav at bottom). */
|
|
136
98
|
navigationBar: number;
|
|
137
99
|
}
|
|
138
|
-
|
|
139
100
|
const ZERO_INSETS: EdgeInsets;
|
|
140
101
|
```
|
|
141
|
-
|
|
142
102
|
All values are in dp/pt (logical pixels), not raw pixels.
|
|
143
|
-
|
|
144
103
|
### Lower-level escape hatches
|
|
145
|
-
|
|
146
104
|
```ts
|
|
147
105
|
import { readGlobalSafeArea, GLOBAL_PROPS_KEY } from '@sigx/lynx-safe-area';
|
|
148
106
|
```
|
|
149
|
-
|
|
150
107
|
- `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.
|
|
151
108
|
- `GLOBAL_PROPS_KEY` — the key the native publisher writes under. Exported for tests/debugging.
|
|
152
|
-
|
|
153
109
|
## CSS variables
|
|
154
|
-
|
|
155
110
|
The provider's host view exposes these on the element style — descendant selectors inherit them via the cascade:
|
|
156
|
-
|
|
157
111
|
| Variable | Maps to |
|
|
158
112
|
| ----------------------- | ---------------------------------------- |
|
|
159
113
|
| `--sat` | `insets.top` (in px) |
|
|
@@ -161,11 +115,8 @@ The provider's host view exposes these on the element style — descendant selec
|
|
|
161
115
|
| `--sab` | `insets.bottom` |
|
|
162
116
|
| `--sal` | `insets.left` |
|
|
163
117
|
| `--safe-area-keyboard` | `insets.keyboard` |
|
|
164
|
-
|
|
165
118
|
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)]`.
|
|
166
|
-
|
|
167
119
|
## How it works
|
|
168
|
-
|
|
169
120
|
```
|
|
170
121
|
┌──────────────────────────────────────┐
|
|
171
122
|
│ Native (iOS UIView / Android View) │
|
|
@@ -200,11 +151,5 @@ Works uniformly across iOS and Android — upstream's `env(safe-area-inset-*)` i
|
|
|
200
151
|
│ useSafeAreaInsets() consumers │
|
|
201
152
|
└──────────────────────────────────────┘
|
|
202
153
|
```
|
|
203
|
-
|
|
204
154
|
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.
|
|
205
|
-
|
|
206
155
|
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.
|
|
207
|
-
|
|
208
|
-
## Reference app
|
|
209
|
-
|
|
210
|
-
`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.
|
package/dist/provider.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAWL,KAAK,MAAM,EAEX,KAAK,WAAW,EACjB,MAAM,YAAY,CAAC;AAKpB;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,oBAAoB,CAAC;AAwBjD,MAAM,MAAM,qBAAqB,GAC7B,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,GACnC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,EAAE,KAAK,CAAC,GAC5D,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;AAE3B;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,gBAAgB;;EAiI3B,CAAC;AAiDH,YAAY,EAAE,WAAW,EAAE,CAAC"}
|
package/dist/provider.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "@sigx/lynx/jsx-runtime";
|
|
2
|
-
import { component, defineProvide, computed, signal, onMounted, onUnmounted, useSharedValue, useMainThreadRef, runOnMainThread, } from '@sigx/lynx';
|
|
2
|
+
import { component, effect, defineProvide, computed, signal, onMounted, onUnmounted, useSharedValue, useMainThreadRef, runOnMainThread, } from '@sigx/lynx';
|
|
3
3
|
import { useSafeAreaContext } from './injectable.js';
|
|
4
4
|
import { readGlobalSafeArea } from './globals.js';
|
|
5
5
|
/**
|
|
@@ -13,6 +13,9 @@ import { readGlobalSafeArea } from './globals.js';
|
|
|
13
13
|
* churned across Lynx releases).
|
|
14
14
|
*/
|
|
15
15
|
export const SAFE_AREA_EVENT = 'safeAreaChanged';
|
|
16
|
+
// Unique host id per provider instance so the runtime `setProperty` call can
|
|
17
|
+
// target this provider's own view.
|
|
18
|
+
let safeAreaIdSeq = 0;
|
|
16
19
|
/**
|
|
17
20
|
* Mount once at the root of an app. Responsibilities:
|
|
18
21
|
*
|
|
@@ -85,8 +88,12 @@ export const SafeAreaProvider = component(({ props, slots }) => {
|
|
|
85
88
|
// Hold the elRef purely so consumers can extend the provider's host view
|
|
86
89
|
// via the published CSS variables. Not used internally for any MT writes.
|
|
87
90
|
const elRef = useMainThreadRef(null);
|
|
91
|
+
// Host id for the runtime `setProperty` CSS-variable application (below).
|
|
92
|
+
const hostId = `safe-area-${++safeAreaIdSeq}`;
|
|
88
93
|
let listener;
|
|
89
94
|
let emitter;
|
|
95
|
+
let varsEffect;
|
|
96
|
+
let insetsGen = 0;
|
|
90
97
|
onMounted(() => {
|
|
91
98
|
// `lynx` is a closure-injected identifier (provided by
|
|
92
99
|
// `@lynx-js/runtime-wrapper-webpack-plugin`'s `__init_card_bundle__`
|
|
@@ -95,6 +102,39 @@ export const SafeAreaProvider = component(({ props, slots }) => {
|
|
|
95
102
|
const lynxObj = typeof lynx !== 'undefined'
|
|
96
103
|
? lynx
|
|
97
104
|
: undefined;
|
|
105
|
+
// Publish insets as real, inheritable CSS custom properties via the runtime
|
|
106
|
+
// `setProperty` API. Lynx does NOT honor custom properties declared through
|
|
107
|
+
// the inline `style` attribute, so `class="pt-[var(--sat)]"` consumers rely
|
|
108
|
+
// on this. On cold start the host view isn't queryable from the background
|
|
109
|
+
// thread the instant this runs, so retry on a short timer until it resolves
|
|
110
|
+
// (`insetsGen` drops a superseded retry). Reactive on `insets.value`.
|
|
111
|
+
const pushInsets = () => {
|
|
112
|
+
const i = insets.value;
|
|
113
|
+
if (!lynxObj?.getElementById)
|
|
114
|
+
return;
|
|
115
|
+
const vars = {
|
|
116
|
+
'--sat': `${i.top}px`,
|
|
117
|
+
'--sar': `${i.right}px`,
|
|
118
|
+
'--sab': `${i.bottom}px`,
|
|
119
|
+
'--sal': `${i.left}px`,
|
|
120
|
+
'--safe-area-keyboard': `${i.keyboard}px`,
|
|
121
|
+
};
|
|
122
|
+
const gen = ++insetsGen;
|
|
123
|
+
let tries = 0;
|
|
124
|
+
const attempt = () => {
|
|
125
|
+
if (gen !== insetsGen)
|
|
126
|
+
return;
|
|
127
|
+
const el = lynxObj.getElementById(hostId);
|
|
128
|
+
if (el) {
|
|
129
|
+
el.setProperty(vars);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (tries++ < 30)
|
|
133
|
+
setTimeout(attempt, 16);
|
|
134
|
+
};
|
|
135
|
+
attempt();
|
|
136
|
+
};
|
|
137
|
+
varsEffect = effect(() => { pushInsets(); });
|
|
98
138
|
emitter = lynxObj?.getJSModule?.('GlobalEventEmitter');
|
|
99
139
|
if (!emitter)
|
|
100
140
|
return;
|
|
@@ -112,8 +152,11 @@ export const SafeAreaProvider = component(({ props, slots }) => {
|
|
|
112
152
|
onUnmounted(() => {
|
|
113
153
|
if (emitter && listener)
|
|
114
154
|
emitter.removeListener(SAFE_AREA_EVENT, listener);
|
|
155
|
+
varsEffect?.stop();
|
|
156
|
+
varsEffect = undefined;
|
|
157
|
+
++insetsGen; // cancel any pending setProperty retry
|
|
115
158
|
});
|
|
116
|
-
return () => (_jsx("view", { class: props.class, "main-thread:ref": elRef, style:
|
|
159
|
+
return () => (_jsx("view", { id: hostId, class: props.class, "main-thread:ref": elRef, style: rootStyle(props.style), children: slots.default?.() }));
|
|
117
160
|
});
|
|
118
161
|
function normaliseInsets(raw, fallback) {
|
|
119
162
|
if (!raw || typeof raw !== 'object')
|
|
@@ -132,22 +175,22 @@ function normaliseInsets(raw, fallback) {
|
|
|
132
175
|
function numOr(v, fallback) {
|
|
133
176
|
return typeof v === 'number' && Number.isFinite(v) ? v : fallback;
|
|
134
177
|
}
|
|
135
|
-
function
|
|
178
|
+
function rootStyle(user) {
|
|
136
179
|
// Defaults make the provider fill the device viewport and act as a
|
|
137
180
|
// flex-column ancestor. Without these, every Lynx app re-rolls inline
|
|
138
181
|
// `style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}`
|
|
139
182
|
// because `<view>` defaults to auto height and the lynx-tailwind
|
|
140
183
|
// preset (as of 0.4.0) doesn't ship an `h-screen` rule. Consumers can
|
|
141
184
|
// override any of these via `props.style`.
|
|
185
|
+
//
|
|
186
|
+
// The safe-area CSS variables (`--sat`/`--sar`/`--sab`/`--sal`/
|
|
187
|
+
// `--safe-area-keyboard`) are NOT set here: Lynx ignores custom properties
|
|
188
|
+
// declared via inline `style`. They're published via the runtime
|
|
189
|
+
// `setProperty` API in the provider's mount effect instead.
|
|
142
190
|
const base = {
|
|
143
191
|
height: '100vh',
|
|
144
192
|
display: 'flex',
|
|
145
193
|
flexDirection: 'column',
|
|
146
|
-
'--sat': `${i.top}px`,
|
|
147
|
-
'--sar': `${i.right}px`,
|
|
148
|
-
'--sab': `${i.bottom}px`,
|
|
149
|
-
'--sal': `${i.left}px`,
|
|
150
|
-
'--safe-area-keyboard': `${i.keyboard}px`,
|
|
151
194
|
};
|
|
152
195
|
return user ? { ...base, ...user } : base;
|
|
153
196
|
}
|
package/dist/provider.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.tsx"],"names":[],"mappings":";AAAA,OAAO,EACL,SAAS,EACT,aAAa,EACb,QAAQ,EACR,MAAM,EACN,SAAS,EACT,WAAW,EACX,cAAc,EACd,gBAAgB,EAChB,eAAe,GAIhB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAGlD;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sigx/lynx-safe-area",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"description": "Safe area insets (notch, home indicator, status bar, keyboard) for sigx-lynx",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -31,15 +31,15 @@
|
|
|
31
31
|
"license": "MIT",
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@sigx/reactivity": "^0.4.8",
|
|
34
|
-
"@sigx/lynx
|
|
35
|
-
"@sigx/lynx": "^0.4.
|
|
34
|
+
"@sigx/lynx": "^0.4.4",
|
|
35
|
+
"@sigx/lynx-runtime-internal": "^0.4.4"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@typescript/native-preview": "7.0.0-dev.20260521.1",
|
|
39
39
|
"typescript": "^6.0.3",
|
|
40
|
-
"@sigx/lynx-runtime-main": "^0.4.
|
|
41
|
-
"@sigx/lynx-testing": "^0.4.
|
|
42
|
-
"@sigx/lynx-plugin": "^0.4.
|
|
40
|
+
"@sigx/lynx-runtime-main": "^0.4.4",
|
|
41
|
+
"@sigx/lynx-testing": "^0.4.4",
|
|
42
|
+
"@sigx/lynx-plugin": "^0.4.4"
|
|
43
43
|
},
|
|
44
44
|
"repository": {
|
|
45
45
|
"type": "git",
|
package/src/provider.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
component,
|
|
3
|
+
effect,
|
|
3
4
|
defineProvide,
|
|
4
5
|
computed,
|
|
5
6
|
signal,
|
|
@@ -35,6 +36,9 @@ interface GlobalEventEmitterLike {
|
|
|
35
36
|
|
|
36
37
|
interface LynxLike {
|
|
37
38
|
getJSModule?: (name: string) => GlobalEventEmitterLike | undefined;
|
|
39
|
+
getElementById?: (
|
|
40
|
+
id: string,
|
|
41
|
+
) => { setProperty(props: Record<string, string>): void } | null;
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
// Closure-injected identifier provided by
|
|
@@ -43,6 +47,10 @@ interface LynxLike {
|
|
|
43
47
|
// have to depend on lynx-runtime-internal just for the ambient.
|
|
44
48
|
declare const lynx: unknown | undefined;
|
|
45
49
|
|
|
50
|
+
// Unique host id per provider instance so the runtime `setProperty` call can
|
|
51
|
+
// target this provider's own view.
|
|
52
|
+
let safeAreaIdSeq = 0;
|
|
53
|
+
|
|
46
54
|
export type SafeAreaProviderProps =
|
|
47
55
|
& Define.Prop<'class', string, false>
|
|
48
56
|
& Define.Prop<'style', Record<string, string | number>, false>
|
|
@@ -127,8 +135,13 @@ export const SafeAreaProvider = component<SafeAreaProviderProps>(({ props, slots
|
|
|
127
135
|
// via the published CSS variables. Not used internally for any MT writes.
|
|
128
136
|
const elRef = useMainThreadRef<MainThread.Element | null>(null);
|
|
129
137
|
|
|
138
|
+
// Host id for the runtime `setProperty` CSS-variable application (below).
|
|
139
|
+
const hostId = `safe-area-${++safeAreaIdSeq}`;
|
|
140
|
+
|
|
130
141
|
let listener: ((...a: unknown[]) => void) | undefined;
|
|
131
142
|
let emitter: GlobalEventEmitterLike | undefined;
|
|
143
|
+
let varsEffect: { stop: () => void } | undefined;
|
|
144
|
+
let insetsGen = 0;
|
|
132
145
|
|
|
133
146
|
onMounted(() => {
|
|
134
147
|
// `lynx` is a closure-injected identifier (provided by
|
|
@@ -138,6 +151,35 @@ export const SafeAreaProvider = component<SafeAreaProviderProps>(({ props, slots
|
|
|
138
151
|
const lynxObj: LynxLike | undefined = typeof lynx !== 'undefined'
|
|
139
152
|
? (lynx as unknown as LynxLike)
|
|
140
153
|
: undefined;
|
|
154
|
+
|
|
155
|
+
// Publish insets as real, inheritable CSS custom properties via the runtime
|
|
156
|
+
// `setProperty` API. Lynx does NOT honor custom properties declared through
|
|
157
|
+
// the inline `style` attribute, so `class="pt-[var(--sat)]"` consumers rely
|
|
158
|
+
// on this. On cold start the host view isn't queryable from the background
|
|
159
|
+
// thread the instant this runs, so retry on a short timer until it resolves
|
|
160
|
+
// (`insetsGen` drops a superseded retry). Reactive on `insets.value`.
|
|
161
|
+
const pushInsets = (): void => {
|
|
162
|
+
const i = insets.value;
|
|
163
|
+
if (!lynxObj?.getElementById) return;
|
|
164
|
+
const vars: Record<string, string> = {
|
|
165
|
+
'--sat': `${i.top}px`,
|
|
166
|
+
'--sar': `${i.right}px`,
|
|
167
|
+
'--sab': `${i.bottom}px`,
|
|
168
|
+
'--sal': `${i.left}px`,
|
|
169
|
+
'--safe-area-keyboard': `${i.keyboard}px`,
|
|
170
|
+
};
|
|
171
|
+
const gen = ++insetsGen;
|
|
172
|
+
let tries = 0;
|
|
173
|
+
const attempt = (): void => {
|
|
174
|
+
if (gen !== insetsGen) return;
|
|
175
|
+
const el = lynxObj!.getElementById!(hostId);
|
|
176
|
+
if (el) { el.setProperty(vars); return; }
|
|
177
|
+
if (tries++ < 30) setTimeout(attempt, 16);
|
|
178
|
+
};
|
|
179
|
+
attempt();
|
|
180
|
+
};
|
|
181
|
+
varsEffect = effect(() => { pushInsets(); });
|
|
182
|
+
|
|
141
183
|
emitter = lynxObj?.getJSModule?.('GlobalEventEmitter');
|
|
142
184
|
if (!emitter) return;
|
|
143
185
|
listener = (raw: unknown) => {
|
|
@@ -154,13 +196,17 @@ export const SafeAreaProvider = component<SafeAreaProviderProps>(({ props, slots
|
|
|
154
196
|
|
|
155
197
|
onUnmounted(() => {
|
|
156
198
|
if (emitter && listener) emitter.removeListener(SAFE_AREA_EVENT, listener);
|
|
199
|
+
varsEffect?.stop();
|
|
200
|
+
varsEffect = undefined;
|
|
201
|
+
++insetsGen; // cancel any pending setProperty retry
|
|
157
202
|
});
|
|
158
203
|
|
|
159
204
|
return () => (
|
|
160
205
|
<view
|
|
206
|
+
id={hostId}
|
|
161
207
|
class={props.class}
|
|
162
208
|
main-thread:ref={elRef}
|
|
163
|
-
style={
|
|
209
|
+
style={rootStyle(props.style)}
|
|
164
210
|
>
|
|
165
211
|
{slots.default?.()}
|
|
166
212
|
</view>
|
|
@@ -191,8 +237,7 @@ function numOr(v: unknown, fallback: number): number {
|
|
|
191
237
|
return typeof v === 'number' && Number.isFinite(v) ? v : fallback;
|
|
192
238
|
}
|
|
193
239
|
|
|
194
|
-
function
|
|
195
|
-
i: EdgeInsets,
|
|
240
|
+
function rootStyle(
|
|
196
241
|
user: Record<string, string | number> | undefined,
|
|
197
242
|
): Record<string, string | number> {
|
|
198
243
|
// Defaults make the provider fill the device viewport and act as a
|
|
@@ -201,15 +246,15 @@ function cssVarStyle(
|
|
|
201
246
|
// because `<view>` defaults to auto height and the lynx-tailwind
|
|
202
247
|
// preset (as of 0.4.0) doesn't ship an `h-screen` rule. Consumers can
|
|
203
248
|
// override any of these via `props.style`.
|
|
249
|
+
//
|
|
250
|
+
// The safe-area CSS variables (`--sat`/`--sar`/`--sab`/`--sal`/
|
|
251
|
+
// `--safe-area-keyboard`) are NOT set here: Lynx ignores custom properties
|
|
252
|
+
// declared via inline `style`. They're published via the runtime
|
|
253
|
+
// `setProperty` API in the provider's mount effect instead.
|
|
204
254
|
const base: Record<string, string | number> = {
|
|
205
255
|
height: '100vh',
|
|
206
256
|
display: 'flex',
|
|
207
257
|
flexDirection: 'column',
|
|
208
|
-
'--sat': `${i.top}px`,
|
|
209
|
-
'--sar': `${i.right}px`,
|
|
210
|
-
'--sab': `${i.bottom}px`,
|
|
211
|
-
'--sal': `${i.left}px`,
|
|
212
|
-
'--safe-area-keyboard': `${i.keyboard}px`,
|
|
213
258
|
};
|
|
214
259
|
return user ? { ...base, ...user } : base;
|
|
215
260
|
}
|