···11+// sticky headers need top offset for safe area on iOS PWA
22+import {useSafeAreaInsets} from 'react-native-safe-area-context'
33+44+import {atoms as a, web} from '#/alf'
55+66+export function useStickyTop() {
77+ const {top} = useSafeAreaInsets()
88+ return web([a.sticky, {top}, a.z_10])
99+}
+73
src/lib/pwa-safe-area.tsx
···11+// measure safe area insets via getBoundingClientRect() on elements
22+// with height: env(safe-area-inset-*). Safari WebKit returns 0 for env() via
33+// getComputedStyle() in standalone PWA mode, but CSS rendering works — so we
44+// create elements sized by env() and measure their rendered dimensions.
55+//
66+// Usage: wrap children of SafeAreaProvider with <SafeAreaOverride> to replace
77+// the broken insets with our measured values.
88+99+import {type PropsWithChildren, useEffect, useState} from 'react'
1010+import {type EdgeInsets} from 'react-native-safe-area-context'
1111+1212+// Access the context — exported as SafeAreaContext (alias for SafeAreaInsetsContext)
1313+const {SafeAreaContext} = require('react-native-safe-area-context')
1414+1515+function createMeasureEl(id: string, heightEnv: string): HTMLElement {
1616+ let el = document.getElementById(id)
1717+ if (!el) {
1818+ el = document.createElement('div')
1919+ el.id = id
2020+ el.style.cssText = [
2121+ 'position:fixed',
2222+ 'left:0',
2323+ 'top:0',
2424+ 'width:1px',
2525+ 'visibility:hidden',
2626+ 'pointer-events:none',
2727+ `height:${heightEnv}`,
2828+ ].join(';')
2929+ document.documentElement.appendChild(el)
3030+ }
3131+ return el
3232+}
3333+3434+function measure(): EdgeInsets {
3535+ if (typeof document === 'undefined') {
3636+ return {top: 0, bottom: 0, left: 0, right: 0}
3737+ }
3838+3939+ const topEl = createMeasureEl(
4040+ 'safe-area-measure-top',
4141+ 'env(safe-area-inset-top, 0px)',
4242+ )
4343+ const bottomEl = createMeasureEl(
4444+ 'safe-area-measure-bottom',
4545+ 'env(safe-area-inset-bottom, 0px)',
4646+ )
4747+4848+ return {
4949+ top: topEl.getBoundingClientRect().height,
5050+ bottom: bottomEl.getBoundingClientRect().height,
5151+ left: 0,
5252+ right: 0,
5353+ }
5454+}
5555+5656+export function SafeAreaOverride({children}: PropsWithChildren) {
5757+ const [insets, setInsets] = useState<EdgeInsets>({
5858+ top: 0,
5959+ bottom: 0,
6060+ left: 0,
6161+ right: 0,
6262+ })
6363+6464+ useEffect(() => {
6565+ // Measure after mount + a frame to ensure CSS env() has been resolved
6666+ requestAnimationFrame(() => {
6767+ setInsets(measure())
6868+ })
6969+ }, [])
7070+7171+ const Provider = SafeAreaContext.Provider
7272+ return <Provider value={insets}>{children}</Provider>
7373+}