@sigx/lynx-safe-area 0.1.0 → 0.1.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 Andreas Ekdahl
3
+ Copyright (c) 2025-2026 Andreas Ekdahl
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/dist/globals.d.ts CHANGED
@@ -36,7 +36,7 @@ export interface RawSafeAreaProps {
36
36
  * The `lynx` symbol is a closure-injected identifier (provided by
37
37
  * `@lynx-js/runtime-wrapper-webpack-plugin`'s `__init_card_bundle__`
38
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`.
39
+ * with a `typeof` guard — same pattern used by `lynx-runtime/src/bg-bridge.ts`.
40
40
  */
41
41
  export declare function readGlobalSafeArea(): EdgeInsets;
42
42
  //# sourceMappingURL=globals.d.ts.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/injectable.ts","../src/types.ts","../src/globals.ts","../src/provider.tsx","../src/hooks.ts","../src/safe-area-view.tsx"],"sourcesContent":["import { defineInjectable } from '@sigx/lynx';\nimport type { SafeAreaContextValue } from './types.js';\n\n/**\n * The DI handle for the safe-area context.\n *\n * - Inside `<SafeAreaProvider>`'s setup we call `defineProvide(useSafeAreaContext, factory)`\n * to install a per-app instance.\n * - Anywhere downstream `useSafeAreaContext()` returns that instance, or\n * `null` if no provider is in scope. Hooks defined in `./hooks.ts` wrap\n * this with the null-check + signal subscription.\n *\n * The factory returns `null` at the global-singleton level so consumers\n * outside a `<SafeAreaProvider>` get a clear signal (vs. a phantom zero-\n * insets context that silently does nothing).\n */\nexport const useSafeAreaContext = defineInjectable<SafeAreaContextValue | null>(() => null);\n","/**\n * Per-edge inset values, in dp/pt (logical pixels). Top/right/bottom/left\n * follow CSS shorthand order. Keyboard, statusBar, navigationBar are\n * informational extras populated when the host platform exposes them — they\n * may be 0 if unknown.\n */\nexport interface EdgeInsets {\n top: number;\n right: number;\n bottom: number;\n left: number;\n /** IME (soft keyboard) height when visible, 0 when hidden. */\n keyboard: number;\n /** Status-bar height (top system bar). Often equal to `top`, but on\n * notched devices the safe-area top includes the notch and the status\n * bar is the smaller status-only inset. */\n statusBar: number;\n /** Navigation-bar height (Android gesture/3-button nav at bottom). */\n navigationBar: number;\n}\n\n/**\n * The four standard CSS edges. Subset to control which sides\n * `<SafeAreaView>` applies inset padding/margin to.\n */\nexport type Edge = 'top' | 'right' | 'bottom' | 'left';\n\n/** Whether `<SafeAreaView>` applies its insets as `padding` or `margin`. */\nexport type SafeAreaMode = 'padding' | 'margin';\n\n/**\n * The injectable shape exposed by `<SafeAreaProvider>`. Components that need\n * insets reactively read `insets.value` (BG signal) or, for MT-driven\n * layouts, subscribe to per-edge `SharedValue`s.\n */\nexport interface SafeAreaContextValue {\n /** BG-side reactive insets. Re-renders the consumer on change. */\n readonly insets: import('@sigx/reactivity').PrimitiveSignal<EdgeInsets>;\n /** Per-edge SharedValues for MT-driven `useAnimatedStyle` bindings. */\n readonly sv: {\n top: import('@sigx/lynx').SharedValue<number>;\n right: import('@sigx/lynx').SharedValue<number>;\n bottom: import('@sigx/lynx').SharedValue<number>;\n left: import('@sigx/lynx').SharedValue<number>;\n };\n}\n\nexport const ZERO_INSETS: EdgeInsets = {\n top: 0,\n right: 0,\n bottom: 0,\n left: 0,\n keyboard: 0,\n statusBar: 0,\n navigationBar: 0,\n};\n","import { type EdgeInsets, ZERO_INSETS } from './types.js';\n\n/**\n * The key under `lynx.__globalProps` where the native publisher writes the\n * inset map. Kept as a constant so iOS/Android publishers, the JS reader, and\n * tests all agree on a single string.\n */\nexport const GLOBAL_PROPS_KEY = 'safeArea';\n\n/**\n * Shape of the safe-area sub-object the native publishers write to\n * `lynx.__globalProps[GLOBAL_PROPS_KEY]`. Some fields may be absent on\n * platforms that don't expose them (e.g. Android pre-31 navigation-bar API);\n * the reader fills missing keys with 0.\n */\nexport interface RawSafeAreaProps {\n top?: number;\n right?: number;\n bottom?: number;\n left?: number;\n keyboard?: number;\n statusBar?: number;\n navigationBar?: number;\n}\n\ninterface LynxGlobalLike {\n __globalProps?: { [k: string]: unknown };\n}\n\n// Closure-injected identifier — see provider.tsx for context.\ndeclare const lynx: unknown | undefined;\n\n/**\n * Synchronously read the current safe-area insets from `lynx.__globalProps`.\n *\n * Returns `ZERO_INSETS` when the publisher hasn't populated yet, when the\n * package is bundled into a non-Lynx host (web preview, SSR), or when the\n * host runtime omits the global. All callers must be prepared for the\n * zero-fallback — it's the natural state during cold start before the native\n * publisher has fired its first `updateGlobalProps`.\n *\n * Safe to call from both the Background Thread (BG) and the Main Thread\n * (MT), since `lynx.__globalProps` is mirrored across both. Sync read on MT\n * is what gives us inset-aware first paint.\n *\n * The `lynx` symbol is a closure-injected identifier (provided by\n * `@lynx-js/runtime-wrapper-webpack-plugin`'s `__init_card_bundle__`\n * wrapper), NOT a property of `globalThis`. Access it as a bare identifier\n * with a `typeof` guard — same pattern used by `runtime-lynx/src/bg-bridge.ts`.\n */\nexport function readGlobalSafeArea(): EdgeInsets {\n const lynxObj: LynxGlobalLike | undefined = typeof lynx !== 'undefined'\n ? (lynx as unknown as LynxGlobalLike)\n : undefined;\n const raw = lynxObj?.__globalProps?.[GLOBAL_PROPS_KEY] as RawSafeAreaProps | undefined;\n if (!raw || typeof raw !== 'object') return ZERO_INSETS;\n return {\n top: numOr0(raw.top),\n right: numOr0(raw.right),\n bottom: numOr0(raw.bottom),\n left: numOr0(raw.left),\n keyboard: numOr0(raw.keyboard),\n statusBar: numOr0(raw.statusBar),\n navigationBar: numOr0(raw.navigationBar),\n };\n}\n\nfunction numOr0(v: unknown): number {\n return typeof v === 'number' && Number.isFinite(v) ? v : 0;\n}\n","import {\n component,\n defineProvide,\n computed,\n signal,\n onMounted,\n onUnmounted,\n useSharedValue,\n useMainThreadRef,\n runOnMainThread,\n type Define,\n type MainThread,\n type SharedValue,\n} from '@sigx/lynx';\nimport { useSafeAreaContext } from './injectable.js';\nimport { readGlobalSafeArea } from './globals.js';\nimport type { EdgeInsets, SafeAreaContextValue } from './types.js';\n\n/**\n * The native publisher (iOS `SafeAreaPublisher.swift`, Android\n * `SafeAreaPublisher.kt`) emits this event via `GlobalEventEmitter` every\n * time it republishes insets. Payload mirrors the same `RawSafeAreaProps`\n * shape stored under `lynx.__globalProps[GLOBAL_PROPS_KEY]`.\n *\n * We use a custom event rather than upstream's `onGlobalPropsChanged` so\n * the contract stays in our hands (upstream's event-name conventions have\n * churned across Lynx releases).\n */\nexport const SAFE_AREA_EVENT = 'safeAreaChanged';\n\ninterface GlobalEventEmitterLike {\n addListener: (name: string, fn: (...a: unknown[]) => void) => void;\n removeListener: (name: string, fn: (...a: unknown[]) => void) => void;\n}\n\ninterface LynxLike {\n getJSModule?: (name: string) => GlobalEventEmitterLike | undefined;\n}\n\n// Closure-injected identifier provided by\n// `@lynx-js/runtime-wrapper-webpack-plugin`. Same pattern as\n// `runtime-lynx/src/shims.d.ts`. Declared locally so this package doesn't\n// have to depend on runtime-lynx-internal just for the ambient.\ndeclare const lynx: unknown | undefined;\n\nexport type SafeAreaProviderProps =\n & Define.Prop<'class', string, false>\n & Define.Prop<'style', Record<string, string | number>, false>\n & Define.Slot<'default'>;\n\n/**\n * Mount once at the root of an app. Responsibilities:\n *\n * 1. **Seed insets synchronously** from `lynx.__globalProps[safeArea]`. The\n * native side populates this *before* the MT bundle evaluates, so the\n * seed is correct on first render — no flash of unsafe content.\n *\n * 2. **Provide a DI context** (`useSafeAreaContext`) holding:\n * - four per-edge `SharedValue<number>`s — the single source of truth,\n * writable on MT, observable from both threads.\n * - a derived BG `computed<EdgeInsets>` for re-render-driven consumers\n * (`useSafeAreaInsets()`).\n *\n * 3. **Subscribe to live updates** via `GlobalEventEmitter`. The native\n * publisher emits `'safeAreaChanged'` after each `updateGlobalProps`,\n * carrying the new inset map. We dispatch a `runOnMainThread` worklet\n * that writes the per-edge SVs on MT — the SharedValue diff/publish\n * bridge then propagates the new values back to the BG signal mirror,\n * which re-fires the `computed` and re-renders consumers.\n *\n * 4. **Apply CSS variables** (`--sat`, `--sar`, `--sab`, `--sal`,\n * `--safe-area-keyboard`) on the root `<view>` so utility-class\n * consumers can write `class=\"pt-[var(--sat)]\"` and have it work\n * uniformly across iOS and Android (upstream's\n * `env(safe-area-inset-*)` is iOS-only).\n */\nexport const SafeAreaProvider = component<SafeAreaProviderProps>(({ props, slots }) => {\n const initial = readGlobalSafeArea();\n\n const svTop = useSharedValue(initial.top);\n const svRight = useSharedValue(initial.right);\n const svBottom = useSharedValue(initial.bottom);\n const svLeft = useSharedValue(initial.left);\n\n // Reactive object signal for the non-SV extras (BG-only — keyboard,\n // statusBar, navigationBar don't drive MT-bound layout, so SV plumbing\n // isn't worth the cost). `signal({...})` returns a deeply reactive proxy;\n // access via `extras.keyboard` etc., replace via `extras.$set({...})`.\n const extras = signal<Extras>({\n keyboard: initial.keyboard,\n statusBar: initial.statusBar,\n navigationBar: initial.navigationBar,\n });\n\n // Single source of truth for BG consumers — derived reactively from the\n // four edge SVs (which live on MT) and the extras signal (which lives on\n // BG). Re-runs when MT publishes new SV values via the AvBridge OR when\n // the safeAreaChanged listener writes to `extras`.\n const insets = computed<EdgeInsets>(() => ({\n top: svTop.value,\n right: svRight.value,\n bottom: svBottom.value,\n left: svLeft.value,\n keyboard: extras.keyboard,\n statusBar: extras.statusBar,\n navigationBar: extras.navigationBar,\n }));\n\n const ctx: SafeAreaContextValue = {\n insets,\n sv: { top: svTop, right: svRight, bottom: svBottom, left: svLeft },\n };\n defineProvide(useSafeAreaContext, () => ctx);\n\n // Worklet that writes the four per-edge SVs on MT. Captured by `_c` at\n // build time — runOnMainThread ships the SV refs as `{_wvid, _initValue}`\n // placeholders that the MT runtime resolves to the live envelope.\n const writeOnMT = runOnMainThread((t: number, r: number, b: number, l: number) => {\n 'main thread';\n svTop.current.value = t;\n svRight.current.value = r;\n svBottom.current.value = b;\n svLeft.current.value = l;\n });\n\n // Hold the elRef purely so consumers can extend the provider's host view\n // via the published CSS variables. Not used internally for any MT writes.\n const elRef = useMainThreadRef<MainThread.Element | null>(null);\n\n let listener: ((...a: unknown[]) => void) | undefined;\n let emitter: GlobalEventEmitterLike | undefined;\n\n onMounted(() => {\n // `lynx` is a closure-injected identifier (provided by\n // `@lynx-js/runtime-wrapper-webpack-plugin`'s `__init_card_bundle__`\n // wrapper), NOT a property of `globalThis`. Access as a bare identifier\n // with `typeof` guard — same pattern as `runtime-lynx/src/bg-bridge.ts`.\n const lynxObj: LynxLike | undefined = typeof lynx !== 'undefined'\n ? (lynx as unknown as LynxLike)\n : undefined;\n emitter = lynxObj?.getJSModule?.('GlobalEventEmitter');\n if (!emitter) return;\n listener = (raw: unknown) => {\n const next = normaliseInsets(raw, insets.value);\n extras.$set({\n keyboard: next.keyboard,\n statusBar: next.statusBar,\n navigationBar: next.navigationBar,\n });\n void writeOnMT(next.top, next.right, next.bottom, next.left);\n };\n emitter.addListener(SAFE_AREA_EVENT, listener);\n });\n\n onUnmounted(() => {\n if (emitter && listener) emitter.removeListener(SAFE_AREA_EVENT, listener);\n });\n\n return () => (\n <view\n class={props.class}\n main-thread:ref={elRef}\n style={cssVarStyle(insets.value, props.style)}\n >\n {slots.default?.()}\n </view>\n );\n});\n\ninterface Extras {\n keyboard: number;\n statusBar: number;\n navigationBar: number;\n}\n\nfunction normaliseInsets(raw: unknown, fallback: EdgeInsets): EdgeInsets {\n if (!raw || typeof raw !== 'object') return fallback;\n const o = raw as Record<string, unknown>;\n return {\n top: numOr(o['top'], fallback.top),\n right: numOr(o['right'], fallback.right),\n bottom: numOr(o['bottom'], fallback.bottom),\n left: numOr(o['left'], fallback.left),\n keyboard: numOr(o['keyboard'], fallback.keyboard),\n statusBar: numOr(o['statusBar'], fallback.statusBar),\n navigationBar: numOr(o['navigationBar'], fallback.navigationBar),\n };\n}\n\nfunction numOr(v: unknown, fallback: number): number {\n return typeof v === 'number' && Number.isFinite(v) ? v : fallback;\n}\n\nfunction cssVarStyle(\n i: EdgeInsets,\n user: Record<string, string | number> | undefined,\n): Record<string, string | number> {\n const base: Record<string, string | number> = {\n '--sat': `${i.top}px`,\n '--sar': `${i.right}px`,\n '--sab': `${i.bottom}px`,\n '--sal': `${i.left}px`,\n '--safe-area-keyboard': `${i.keyboard}px`,\n };\n return user ? { ...base, ...user } : base;\n}\n\n// re-export so users only need `@sigx/lynx-safe-area`\nexport type { SharedValue };\n","import { computed, type Computed, type PrimitiveSignal } from '@sigx/reactivity';\nimport { useSafeAreaContext } from './injectable.js';\nimport { readGlobalSafeArea } from './globals.js';\nimport { ZERO_INSETS, type EdgeInsets, type SafeAreaContextValue } from './types.js';\n\ntype InsetsRead = PrimitiveSignal<EdgeInsets> | Computed<EdgeInsets>;\n\n/**\n * BG-side reactive read of current safe-area insets. Returns the live signal\n * — components calling this re-render on inset change (rotation, keyboard,\n * split-view).\n *\n * If no `<SafeAreaProvider>` is in scope, returns a signal seeded with\n * `ZERO_INSETS` and warns in dev. (We don't throw because mounting an app\n * fragment for tests/storybook without a provider is convenient and the\n * zero fallback degrades gracefully.)\n *\n * @example\n * ```tsx\n * const insets = useSafeAreaInsets();\n * return () => <view style={{ paddingTop: insets.value.top }} />;\n * ```\n */\nexport function useSafeAreaInsets(): InsetsRead {\n const ctx = useSafeAreaContext();\n if (!ctx) return fallbackSignal();\n return ctx.insets;\n}\n\n/**\n * Per-edge SharedValues for MT-driven `useAnimatedStyle` bindings — the\n * recommended path for `<SafeAreaView>`-style layouts that need padding to\n * track insets without a BG re-render. See `safe-area-view.tsx` for the\n * canonical consumer.\n */\nexport function useSafeAreaSharedValues(): SafeAreaContextValue['sv'] | null {\n const ctx = useSafeAreaContext();\n return ctx?.sv ?? null;\n}\n\n/**\n * Computed signal of the inner safe frame (origin + size) in dp/pt.\n *\n * Useful for absolute-positioned overlays, modal bounds, and layout math\n * that needs to know \"the visible content rect\" rather than just the inset\n * deltas. The frame size is computed from the host viewport — you must pass\n * `viewportWidth`/`viewportHeight` (typically read once via\n * `@sigx/lynx-device-info`) since the safe-area module deliberately avoids\n * pulling that whole dependency.\n *\n * @example\n * ```tsx\n * const { screenWidth, screenHeight } = await DeviceInfo.getInfo();\n * const frame = useSafeAreaFrame(screenWidth, screenHeight);\n * return () => <view style={{\n * position: 'absolute',\n * top: frame.value.y, left: frame.value.x,\n * width: frame.value.width, height: frame.value.height,\n * }} />;\n * ```\n */\nexport function useSafeAreaFrame(\n viewportWidth: number,\n viewportHeight: number,\n): Computed<{ x: number; y: number; width: number; height: number }> {\n const insets = useSafeAreaInsets();\n return computed(() => {\n const i = insets.value;\n return {\n x: i.left,\n y: i.top,\n width: Math.max(0, viewportWidth - i.left - i.right),\n height: Math.max(0, viewportHeight - i.top - i.bottom - i.keyboard),\n };\n });\n}\n\n/**\n * **MT-thread** synchronous read of the current safe-area insets. For use\n * inside `'main thread'`-marked worklet bodies. Reads `lynx.__globalProps`\n * directly — there's no signal subscription, so callers re-evaluate per\n * worklet invocation rather than reactively.\n *\n * For declarative MT-driven layout (the common case), prefer\n * `<SafeAreaView edges={…}>`, which composes `useSafeAreaSharedValues()`\n * with `useAnimatedStyle` — that path is reactive and applies on flush.\n */\nexport function useSafeAreaInsetsMT(): EdgeInsets {\n return readGlobalSafeArea();\n}\n\nlet _fallback: Computed<EdgeInsets> | undefined;\nfunction fallbackSignal(): Computed<EdgeInsets> {\n if (!_fallback) {\n const env = (globalThis as { process?: { env?: Record<string, string | undefined> } })\n .process?.env?.['NODE_ENV'];\n if (env !== 'production') {\n console.warn(\n '[sigx-safe-area] useSafeAreaInsets() called outside <SafeAreaProvider>. ' +\n 'Returning ZERO_INSETS. Wrap your app in <SafeAreaProvider> to receive ' +\n 'live device insets.',\n );\n }\n _fallback = computed(() => ZERO_INSETS);\n }\n return _fallback;\n}\n","import { component, type Define } from '@sigx/lynx';\nimport { useSafeAreaInsets } from './hooks.js';\nimport type { Edge, SafeAreaMode } from './types.js';\n\nexport type SafeAreaViewProps =\n & Define.Prop<'edges', Edge[], false>\n & Define.Prop<'mode', SafeAreaMode, false>\n & Define.Prop<'class', string, false>\n & Define.Prop<'style', Record<string, string | number>, false>\n & Define.Slot<'default'>;\n\nconst ALL_EDGES: Edge[] = ['top', 'right', 'bottom', 'left'];\n\n/**\n * Drop-in container that applies the current safe-area insets as padding\n * (default) or margin on the configured edges.\n *\n * Implementation: BG signal + inline style. Sigx auto-tracks `insets.value`\n * access in the render function, so the inset values land in the FIRST\n * layout pass and re-apply reactively on every `safeAreaChanged` event.\n *\n * The previous implementation used `useAnimatedStyle` to drive padding via\n * the MT bridge — but `setStyleProperties` writes that affect layout fire\n * AFTER the first layout pass, and child elements that have already laid\n * out (notably `<scroll-view>`, which captures its frame eagerly) don't\n * reflow. Inline style avoids that timing trap entirely.\n *\n * `edges` defaults to all four sides. Pass a subset (e.g. `['top']`) to\n * leave the unspecified sides unaffected.\n *\n * Must be a descendant of `<SafeAreaProvider>`. If no provider is in scope\n * (test/storybook), `useSafeAreaInsets()` returns `ZERO_INSETS` with a\n * dev-mode warning and SafeAreaView passes through unchanged.\n *\n * @example\n * ```tsx\n * <SafeAreaProvider>\n * <SafeAreaView edges={['top', 'bottom']} class=\"bg-base-100 flex-1\">\n * <PageContent />\n * </SafeAreaView>\n * </SafeAreaProvider>\n * ```\n */\nexport const SafeAreaView = component<SafeAreaViewProps>(({ props, slots }) => {\n const insets = useSafeAreaInsets();\n const edges = props.edges ?? ALL_EDGES;\n const mode = props.mode ?? 'padding';\n\n return () => {\n const i = insets.value;\n const insetStyle: Record<string, string | number> = {};\n if (edges.includes('top')) {\n insetStyle[mode === 'padding' ? 'paddingTop' : 'marginTop'] = `${i.top}px`;\n }\n if (edges.includes('right')) {\n insetStyle[mode === 'padding' ? 'paddingRight' : 'marginRight'] = `${i.right}px`;\n }\n if (edges.includes('bottom')) {\n insetStyle[mode === 'padding' ? 'paddingBottom' : 'marginBottom'] = `${i.bottom}px`;\n }\n if (edges.includes('left')) {\n insetStyle[mode === 'padding' ? 'paddingLeft' : 'marginLeft'] = `${i.left}px`;\n }\n return (\n <view\n class={props.class}\n style={props.style ? { ...props.style, ...insetStyle } : insetStyle}\n >\n {slots.default?.()}\n </view>\n );\n };\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/injectable.ts","../src/types.ts","../src/globals.ts","../src/provider.tsx","../src/hooks.ts","../src/safe-area-view.tsx"],"sourcesContent":["import { defineInjectable } from '@sigx/lynx';\nimport type { SafeAreaContextValue } from './types.js';\n\n/**\n * The DI handle for the safe-area context.\n *\n * - Inside `<SafeAreaProvider>`'s setup we call `defineProvide(useSafeAreaContext, factory)`\n * to install a per-app instance.\n * - Anywhere downstream `useSafeAreaContext()` returns that instance, or\n * `null` if no provider is in scope. Hooks defined in `./hooks.ts` wrap\n * this with the null-check + signal subscription.\n *\n * The factory returns `null` at the global-singleton level so consumers\n * outside a `<SafeAreaProvider>` get a clear signal (vs. a phantom zero-\n * insets context that silently does nothing).\n */\nexport const useSafeAreaContext = defineInjectable<SafeAreaContextValue | null>(() => null);\n","/**\n * Per-edge inset values, in dp/pt (logical pixels). Top/right/bottom/left\n * follow CSS shorthand order. Keyboard, statusBar, navigationBar are\n * informational extras populated when the host platform exposes them — they\n * may be 0 if unknown.\n */\nexport interface EdgeInsets {\n top: number;\n right: number;\n bottom: number;\n left: number;\n /** IME (soft keyboard) height when visible, 0 when hidden. */\n keyboard: number;\n /** Status-bar height (top system bar). Often equal to `top`, but on\n * notched devices the safe-area top includes the notch and the status\n * bar is the smaller status-only inset. */\n statusBar: number;\n /** Navigation-bar height (Android gesture/3-button nav at bottom). */\n navigationBar: number;\n}\n\n/**\n * The four standard CSS edges. Subset to control which sides\n * `<SafeAreaView>` applies inset padding/margin to.\n */\nexport type Edge = 'top' | 'right' | 'bottom' | 'left';\n\n/** Whether `<SafeAreaView>` applies its insets as `padding` or `margin`. */\nexport type SafeAreaMode = 'padding' | 'margin';\n\n/**\n * The injectable shape exposed by `<SafeAreaProvider>`. Components that need\n * insets reactively read `insets.value` (BG signal) or, for MT-driven\n * layouts, subscribe to per-edge `SharedValue`s.\n */\nexport interface SafeAreaContextValue {\n /** BG-side reactive insets. Re-renders the consumer on change. */\n readonly insets: import('@sigx/reactivity').PrimitiveSignal<EdgeInsets>;\n /** Per-edge SharedValues for MT-driven `useAnimatedStyle` bindings. */\n readonly sv: {\n top: import('@sigx/lynx').SharedValue<number>;\n right: import('@sigx/lynx').SharedValue<number>;\n bottom: import('@sigx/lynx').SharedValue<number>;\n left: import('@sigx/lynx').SharedValue<number>;\n };\n}\n\nexport const ZERO_INSETS: EdgeInsets = {\n top: 0,\n right: 0,\n bottom: 0,\n left: 0,\n keyboard: 0,\n statusBar: 0,\n navigationBar: 0,\n};\n","import { type EdgeInsets, ZERO_INSETS } from './types.js';\n\n/**\n * The key under `lynx.__globalProps` where the native publisher writes the\n * inset map. Kept as a constant so iOS/Android publishers, the JS reader, and\n * tests all agree on a single string.\n */\nexport const GLOBAL_PROPS_KEY = 'safeArea';\n\n/**\n * Shape of the safe-area sub-object the native publishers write to\n * `lynx.__globalProps[GLOBAL_PROPS_KEY]`. Some fields may be absent on\n * platforms that don't expose them (e.g. Android pre-31 navigation-bar API);\n * the reader fills missing keys with 0.\n */\nexport interface RawSafeAreaProps {\n top?: number;\n right?: number;\n bottom?: number;\n left?: number;\n keyboard?: number;\n statusBar?: number;\n navigationBar?: number;\n}\n\ninterface LynxGlobalLike {\n __globalProps?: { [k: string]: unknown };\n}\n\n// Closure-injected identifier — see provider.tsx for context.\ndeclare const lynx: unknown | undefined;\n\n/**\n * Synchronously read the current safe-area insets from `lynx.__globalProps`.\n *\n * Returns `ZERO_INSETS` when the publisher hasn't populated yet, when the\n * package is bundled into a non-Lynx host (web preview, SSR), or when the\n * host runtime omits the global. All callers must be prepared for the\n * zero-fallback — it's the natural state during cold start before the native\n * publisher has fired its first `updateGlobalProps`.\n *\n * Safe to call from both the Background Thread (BG) and the Main Thread\n * (MT), since `lynx.__globalProps` is mirrored across both. Sync read on MT\n * is what gives us inset-aware first paint.\n *\n * The `lynx` symbol is a closure-injected identifier (provided by\n * `@lynx-js/runtime-wrapper-webpack-plugin`'s `__init_card_bundle__`\n * wrapper), NOT a property of `globalThis`. Access it as a bare identifier\n * with a `typeof` guard — same pattern used by `lynx-runtime/src/bg-bridge.ts`.\n */\nexport function readGlobalSafeArea(): EdgeInsets {\n const lynxObj: LynxGlobalLike | undefined = typeof lynx !== 'undefined'\n ? (lynx as unknown as LynxGlobalLike)\n : undefined;\n const raw = lynxObj?.__globalProps?.[GLOBAL_PROPS_KEY] as RawSafeAreaProps | undefined;\n if (!raw || typeof raw !== 'object') return ZERO_INSETS;\n return {\n top: numOr0(raw.top),\n right: numOr0(raw.right),\n bottom: numOr0(raw.bottom),\n left: numOr0(raw.left),\n keyboard: numOr0(raw.keyboard),\n statusBar: numOr0(raw.statusBar),\n navigationBar: numOr0(raw.navigationBar),\n };\n}\n\nfunction numOr0(v: unknown): number {\n return typeof v === 'number' && Number.isFinite(v) ? v : 0;\n}\n","import {\n component,\n defineProvide,\n computed,\n signal,\n onMounted,\n onUnmounted,\n useSharedValue,\n useMainThreadRef,\n runOnMainThread,\n type Define,\n type MainThread,\n type SharedValue,\n} from '@sigx/lynx';\nimport { useSafeAreaContext } from './injectable.js';\nimport { readGlobalSafeArea } from './globals.js';\nimport type { EdgeInsets, SafeAreaContextValue } from './types.js';\n\n/**\n * The native publisher (iOS `SafeAreaPublisher.swift`, Android\n * `SafeAreaPublisher.kt`) emits this event via `GlobalEventEmitter` every\n * time it republishes insets. Payload mirrors the same `RawSafeAreaProps`\n * shape stored under `lynx.__globalProps[GLOBAL_PROPS_KEY]`.\n *\n * We use a custom event rather than upstream's `onGlobalPropsChanged` so\n * the contract stays in our hands (upstream's event-name conventions have\n * churned across Lynx releases).\n */\nexport const SAFE_AREA_EVENT = 'safeAreaChanged';\n\ninterface GlobalEventEmitterLike {\n addListener: (name: string, fn: (...a: unknown[]) => void) => void;\n removeListener: (name: string, fn: (...a: unknown[]) => void) => void;\n}\n\ninterface LynxLike {\n getJSModule?: (name: string) => GlobalEventEmitterLike | undefined;\n}\n\n// Closure-injected identifier provided by\n// `@lynx-js/runtime-wrapper-webpack-plugin`. Same pattern as\n// `lynx-runtime/src/shims.d.ts`. Declared locally so this package doesn't\n// have to depend on lynx-runtime-internal just for the ambient.\ndeclare const lynx: unknown | undefined;\n\nexport type SafeAreaProviderProps =\n & Define.Prop<'class', string, false>\n & Define.Prop<'style', Record<string, string | number>, false>\n & Define.Slot<'default'>;\n\n/**\n * Mount once at the root of an app. Responsibilities:\n *\n * 1. **Seed insets synchronously** from `lynx.__globalProps[safeArea]`. The\n * native side populates this *before* the MT bundle evaluates, so the\n * seed is correct on first render — no flash of unsafe content.\n *\n * 2. **Provide a DI context** (`useSafeAreaContext`) holding:\n * - four per-edge `SharedValue<number>`s — the single source of truth,\n * writable on MT, observable from both threads.\n * - a derived BG `computed<EdgeInsets>` for re-render-driven consumers\n * (`useSafeAreaInsets()`).\n *\n * 3. **Subscribe to live updates** via `GlobalEventEmitter`. The native\n * publisher emits `'safeAreaChanged'` after each `updateGlobalProps`,\n * carrying the new inset map. We dispatch a `runOnMainThread` worklet\n * that writes the per-edge SVs on MT — the SharedValue diff/publish\n * bridge then propagates the new values back to the BG signal mirror,\n * which re-fires the `computed` and re-renders consumers.\n *\n * 4. **Apply CSS variables** (`--sat`, `--sar`, `--sab`, `--sal`,\n * `--safe-area-keyboard`) on the root `<view>` so utility-class\n * consumers can write `class=\"pt-[var(--sat)]\"` and have it work\n * uniformly across iOS and Android (upstream's\n * `env(safe-area-inset-*)` is iOS-only).\n */\nexport const SafeAreaProvider = component<SafeAreaProviderProps>(({ props, slots }) => {\n const initial = readGlobalSafeArea();\n\n const svTop = useSharedValue(initial.top);\n const svRight = useSharedValue(initial.right);\n const svBottom = useSharedValue(initial.bottom);\n const svLeft = useSharedValue(initial.left);\n\n // Reactive object signal for the non-SV extras (BG-only — keyboard,\n // statusBar, navigationBar don't drive MT-bound layout, so SV plumbing\n // isn't worth the cost). `signal({...})` returns a deeply reactive proxy;\n // access via `extras.keyboard` etc., replace via `extras.$set({...})`.\n const extras = signal<Extras>({\n keyboard: initial.keyboard,\n statusBar: initial.statusBar,\n navigationBar: initial.navigationBar,\n });\n\n // Single source of truth for BG consumers — derived reactively from the\n // four edge SVs (which live on MT) and the extras signal (which lives on\n // BG). Re-runs when MT publishes new SV values via the AvBridge OR when\n // the safeAreaChanged listener writes to `extras`.\n const insets = computed<EdgeInsets>(() => ({\n top: svTop.value,\n right: svRight.value,\n bottom: svBottom.value,\n left: svLeft.value,\n keyboard: extras.keyboard,\n statusBar: extras.statusBar,\n navigationBar: extras.navigationBar,\n }));\n\n const ctx: SafeAreaContextValue = {\n insets,\n sv: { top: svTop, right: svRight, bottom: svBottom, left: svLeft },\n };\n defineProvide(useSafeAreaContext, () => ctx);\n\n // Worklet that writes the four per-edge SVs on MT. Captured by `_c` at\n // build time — runOnMainThread ships the SV refs as `{_wvid, _initValue}`\n // placeholders that the MT runtime resolves to the live envelope.\n const writeOnMT = runOnMainThread((t: number, r: number, b: number, l: number) => {\n 'main thread';\n svTop.current.value = t;\n svRight.current.value = r;\n svBottom.current.value = b;\n svLeft.current.value = l;\n });\n\n // Hold the elRef purely so consumers can extend the provider's host view\n // via the published CSS variables. Not used internally for any MT writes.\n const elRef = useMainThreadRef<MainThread.Element | null>(null);\n\n let listener: ((...a: unknown[]) => void) | undefined;\n let emitter: GlobalEventEmitterLike | undefined;\n\n onMounted(() => {\n // `lynx` is a closure-injected identifier (provided by\n // `@lynx-js/runtime-wrapper-webpack-plugin`'s `__init_card_bundle__`\n // wrapper), NOT a property of `globalThis`. Access as a bare identifier\n // with `typeof` guard — same pattern as `lynx-runtime/src/bg-bridge.ts`.\n const lynxObj: LynxLike | undefined = typeof lynx !== 'undefined'\n ? (lynx as unknown as LynxLike)\n : undefined;\n emitter = lynxObj?.getJSModule?.('GlobalEventEmitter');\n if (!emitter) return;\n listener = (raw: unknown) => {\n const next = normaliseInsets(raw, insets.value);\n extras.$set({\n keyboard: next.keyboard,\n statusBar: next.statusBar,\n navigationBar: next.navigationBar,\n });\n void writeOnMT(next.top, next.right, next.bottom, next.left);\n };\n emitter.addListener(SAFE_AREA_EVENT, listener);\n });\n\n onUnmounted(() => {\n if (emitter && listener) emitter.removeListener(SAFE_AREA_EVENT, listener);\n });\n\n return () => (\n <view\n class={props.class}\n main-thread:ref={elRef}\n style={cssVarStyle(insets.value, props.style)}\n >\n {slots.default?.()}\n </view>\n );\n});\n\ninterface Extras {\n keyboard: number;\n statusBar: number;\n navigationBar: number;\n}\n\nfunction normaliseInsets(raw: unknown, fallback: EdgeInsets): EdgeInsets {\n if (!raw || typeof raw !== 'object') return fallback;\n const o = raw as Record<string, unknown>;\n return {\n top: numOr(o['top'], fallback.top),\n right: numOr(o['right'], fallback.right),\n bottom: numOr(o['bottom'], fallback.bottom),\n left: numOr(o['left'], fallback.left),\n keyboard: numOr(o['keyboard'], fallback.keyboard),\n statusBar: numOr(o['statusBar'], fallback.statusBar),\n navigationBar: numOr(o['navigationBar'], fallback.navigationBar),\n };\n}\n\nfunction numOr(v: unknown, fallback: number): number {\n return typeof v === 'number' && Number.isFinite(v) ? v : fallback;\n}\n\nfunction cssVarStyle(\n i: EdgeInsets,\n user: Record<string, string | number> | undefined,\n): Record<string, string | number> {\n const base: Record<string, string | number> = {\n '--sat': `${i.top}px`,\n '--sar': `${i.right}px`,\n '--sab': `${i.bottom}px`,\n '--sal': `${i.left}px`,\n '--safe-area-keyboard': `${i.keyboard}px`,\n };\n return user ? { ...base, ...user } : base;\n}\n\n// re-export so users only need `@sigx/lynx-safe-area`\nexport type { SharedValue };\n","import { computed, type Computed, type PrimitiveSignal } from '@sigx/reactivity';\nimport { useSafeAreaContext } from './injectable.js';\nimport { readGlobalSafeArea } from './globals.js';\nimport { ZERO_INSETS, type EdgeInsets, type SafeAreaContextValue } from './types.js';\n\ntype InsetsRead = PrimitiveSignal<EdgeInsets> | Computed<EdgeInsets>;\n\n/**\n * BG-side reactive read of current safe-area insets. Returns the live signal\n * — components calling this re-render on inset change (rotation, keyboard,\n * split-view).\n *\n * If no `<SafeAreaProvider>` is in scope, returns a signal seeded with\n * `ZERO_INSETS` and warns in dev. (We don't throw because mounting an app\n * fragment for tests/storybook without a provider is convenient and the\n * zero fallback degrades gracefully.)\n *\n * @example\n * ```tsx\n * const insets = useSafeAreaInsets();\n * return () => <view style={{ paddingTop: insets.value.top }} />;\n * ```\n */\nexport function useSafeAreaInsets(): InsetsRead {\n const ctx = useSafeAreaContext();\n if (!ctx) return fallbackSignal();\n return ctx.insets;\n}\n\n/**\n * Per-edge SharedValues for MT-driven `useAnimatedStyle` bindings — the\n * recommended path for `<SafeAreaView>`-style layouts that need padding to\n * track insets without a BG re-render. See `safe-area-view.tsx` for the\n * canonical consumer.\n */\nexport function useSafeAreaSharedValues(): SafeAreaContextValue['sv'] | null {\n const ctx = useSafeAreaContext();\n return ctx?.sv ?? null;\n}\n\n/**\n * Computed signal of the inner safe frame (origin + size) in dp/pt.\n *\n * Useful for absolute-positioned overlays, modal bounds, and layout math\n * that needs to know \"the visible content rect\" rather than just the inset\n * deltas. The frame size is computed from the host viewport — you must pass\n * `viewportWidth`/`viewportHeight` (typically read once via\n * `@sigx/lynx-device-info`) since the safe-area module deliberately avoids\n * pulling that whole dependency.\n *\n * @example\n * ```tsx\n * const { screenWidth, screenHeight } = await DeviceInfo.getInfo();\n * const frame = useSafeAreaFrame(screenWidth, screenHeight);\n * return () => <view style={{\n * position: 'absolute',\n * top: frame.value.y, left: frame.value.x,\n * width: frame.value.width, height: frame.value.height,\n * }} />;\n * ```\n */\nexport function useSafeAreaFrame(\n viewportWidth: number,\n viewportHeight: number,\n): Computed<{ x: number; y: number; width: number; height: number }> {\n const insets = useSafeAreaInsets();\n return computed(() => {\n const i = insets.value;\n return {\n x: i.left,\n y: i.top,\n width: Math.max(0, viewportWidth - i.left - i.right),\n height: Math.max(0, viewportHeight - i.top - i.bottom - i.keyboard),\n };\n });\n}\n\n/**\n * **MT-thread** synchronous read of the current safe-area insets. For use\n * inside `'main thread'`-marked worklet bodies. Reads `lynx.__globalProps`\n * directly — there's no signal subscription, so callers re-evaluate per\n * worklet invocation rather than reactively.\n *\n * For declarative MT-driven layout (the common case), prefer\n * `<SafeAreaView edges={…}>`, which composes `useSafeAreaSharedValues()`\n * with `useAnimatedStyle` — that path is reactive and applies on flush.\n */\nexport function useSafeAreaInsetsMT(): EdgeInsets {\n return readGlobalSafeArea();\n}\n\nlet _fallback: Computed<EdgeInsets> | undefined;\nfunction fallbackSignal(): Computed<EdgeInsets> {\n if (!_fallback) {\n const env = (globalThis as { process?: { env?: Record<string, string | undefined> } })\n .process?.env?.['NODE_ENV'];\n if (env !== 'production') {\n console.warn(\n '[sigx-safe-area] useSafeAreaInsets() called outside <SafeAreaProvider>. ' +\n 'Returning ZERO_INSETS. Wrap your app in <SafeAreaProvider> to receive ' +\n 'live device insets.',\n );\n }\n _fallback = computed(() => ZERO_INSETS);\n }\n return _fallback;\n}\n","import { component, type Define } from '@sigx/lynx';\nimport { useSafeAreaInsets } from './hooks.js';\nimport type { Edge, SafeAreaMode } from './types.js';\n\nexport type SafeAreaViewProps =\n & Define.Prop<'edges', Edge[], false>\n & Define.Prop<'mode', SafeAreaMode, false>\n & Define.Prop<'class', string, false>\n & Define.Prop<'style', Record<string, string | number>, false>\n & Define.Slot<'default'>;\n\nconst ALL_EDGES: Edge[] = ['top', 'right', 'bottom', 'left'];\n\n/**\n * Drop-in container that applies the current safe-area insets as padding\n * (default) or margin on the configured edges.\n *\n * Implementation: BG signal + inline style. Sigx auto-tracks `insets.value`\n * access in the render function, so the inset values land in the FIRST\n * layout pass and re-apply reactively on every `safeAreaChanged` event.\n *\n * The previous implementation used `useAnimatedStyle` to drive padding via\n * the MT bridge — but `setStyleProperties` writes that affect layout fire\n * AFTER the first layout pass, and child elements that have already laid\n * out (notably `<scroll-view>`, which captures its frame eagerly) don't\n * reflow. Inline style avoids that timing trap entirely.\n *\n * `edges` defaults to all four sides. Pass a subset (e.g. `['top']`) to\n * leave the unspecified sides unaffected.\n *\n * Must be a descendant of `<SafeAreaProvider>`. If no provider is in scope\n * (test/storybook), `useSafeAreaInsets()` returns `ZERO_INSETS` with a\n * dev-mode warning and SafeAreaView passes through unchanged.\n *\n * @example\n * ```tsx\n * <SafeAreaProvider>\n * <SafeAreaView edges={['top', 'bottom']} class=\"bg-base-100 flex-1\">\n * <PageContent />\n * </SafeAreaView>\n * </SafeAreaProvider>\n * ```\n */\nexport const SafeAreaView = component<SafeAreaViewProps>(({ props, slots }) => {\n const insets = useSafeAreaInsets();\n const edges = props.edges ?? ALL_EDGES;\n const mode = props.mode ?? 'padding';\n\n return () => {\n const i = insets.value;\n const insetStyle: Record<string, string | number> = {};\n if (edges.includes('top')) {\n insetStyle[mode === 'padding' ? 'paddingTop' : 'marginTop'] = `${i.top}px`;\n }\n if (edges.includes('right')) {\n insetStyle[mode === 'padding' ? 'paddingRight' : 'marginRight'] = `${i.right}px`;\n }\n if (edges.includes('bottom')) {\n insetStyle[mode === 'padding' ? 'paddingBottom' : 'marginBottom'] = `${i.bottom}px`;\n }\n if (edges.includes('left')) {\n insetStyle[mode === 'padding' ? 'paddingLeft' : 'marginLeft'] = `${i.left}px`;\n }\n return (\n <view\n class={props.class}\n style={props.style ? { ...props.style, ...insetStyle } : insetStyle}\n >\n {slots.default?.()}\n </view>\n );\n };\n});\n"],"mappings":"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"}
@@ -38,7 +38,7 @@ export type SafeAreaProviderProps = Define.Prop<'class', string, false> & Define
38
38
  * `env(safe-area-inset-*)` is iOS-only).
39
39
  */
40
40
  export declare const SafeAreaProvider: import("@sigx/runtime-core").ComponentFactory<SafeAreaProviderProps, void, {
41
- default: () => import("@sigx/runtime-core").JSXElement[] | import("@sigx/runtime-core").JSXElement;
41
+ default: () => import("@sigx/runtime-core").JSXElement | import("@sigx/runtime-core").JSXElement[] | null;
42
42
  }>;
43
43
  export type { SharedValue };
44
44
  //# sourceMappingURL=provider.d.ts.map
@@ -32,6 +32,6 @@ export type SafeAreaViewProps = Define.Prop<'edges', Edge[], false> & Define.Pro
32
32
  * ```
33
33
  */
34
34
  export declare const SafeAreaView: import("@sigx/runtime-core").ComponentFactory<SafeAreaViewProps, void, {
35
- default: () => import("@sigx/runtime-core").JSXElement[] | import("@sigx/runtime-core").JSXElement;
35
+ default: () => import("@sigx/runtime-core").JSXElement | import("@sigx/runtime-core").JSXElement[] | null;
36
36
  }>;
37
37
  //# sourceMappingURL=safe-area-view.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sigx/lynx-safe-area",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Safe area insets (notch, home indicator, status bar, keyboard) for sigx-lynx",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -27,15 +27,28 @@
27
27
  "author": "Andreas Ekdahl",
28
28
  "license": "MIT",
29
29
  "dependencies": {
30
- "@sigx/lynx": "^0.1.0",
31
- "@sigx/runtime-lynx-internal": "^0.2.4"
30
+ "@sigx/reactivity": "^0.4.3",
31
+ "@sigx/lynx": "^0.1.4",
32
+ "@sigx/lynx-runtime-internal": "^0.2.6"
32
33
  },
33
34
  "devDependencies": {
34
- "typescript": "^5.9.3",
35
- "vite": "^8.0.3",
36
- "@sigx/runtime-lynx-main": "^0.2.4",
37
- "@sigx/lynx-plugin": "^0.2.4",
38
- "@sigx/testing-lynx": "^0.2.4"
35
+ "typescript": "^6.0.3",
36
+ "vite": "^8.0.12",
37
+ "@sigx/lynx-testing": "^0.2.6",
38
+ "@sigx/lynx-runtime-main": "^0.2.7",
39
+ "@sigx/lynx-plugin": "^0.2.7"
40
+ },
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "git+https://github.com/signalxjs/lynx.git",
44
+ "directory": "packages/lynx-safe-area"
45
+ },
46
+ "homepage": "https://github.com/signalxjs/lynx/tree/main/packages/lynx-safe-area",
47
+ "bugs": {
48
+ "url": "https://github.com/signalxjs/lynx/issues"
49
+ },
50
+ "publishConfig": {
51
+ "access": "public"
39
52
  },
40
53
  "scripts": {
41
54
  "build": "vite build && tsgo --emitDeclarationOnly",
package/src/globals.ts CHANGED
@@ -46,7 +46,7 @@ declare const lynx: unknown | undefined;
46
46
  * The `lynx` symbol is a closure-injected identifier (provided by
47
47
  * `@lynx-js/runtime-wrapper-webpack-plugin`'s `__init_card_bundle__`
48
48
  * wrapper), NOT a property of `globalThis`. Access it as a bare identifier
49
- * with a `typeof` guard — same pattern used by `runtime-lynx/src/bg-bridge.ts`.
49
+ * with a `typeof` guard — same pattern used by `lynx-runtime/src/bg-bridge.ts`.
50
50
  */
51
51
  export function readGlobalSafeArea(): EdgeInsets {
52
52
  const lynxObj: LynxGlobalLike | undefined = typeof lynx !== 'undefined'
package/src/provider.tsx CHANGED
@@ -39,8 +39,8 @@ interface LynxLike {
39
39
 
40
40
  // Closure-injected identifier provided by
41
41
  // `@lynx-js/runtime-wrapper-webpack-plugin`. Same pattern as
42
- // `runtime-lynx/src/shims.d.ts`. Declared locally so this package doesn't
43
- // have to depend on runtime-lynx-internal just for the ambient.
42
+ // `lynx-runtime/src/shims.d.ts`. Declared locally so this package doesn't
43
+ // have to depend on lynx-runtime-internal just for the ambient.
44
44
  declare const lynx: unknown | undefined;
45
45
 
46
46
  export type SafeAreaProviderProps =
@@ -134,7 +134,7 @@ export const SafeAreaProvider = component<SafeAreaProviderProps>(({ props, slots
134
134
  // `lynx` is a closure-injected identifier (provided by
135
135
  // `@lynx-js/runtime-wrapper-webpack-plugin`'s `__init_card_bundle__`
136
136
  // wrapper), NOT a property of `globalThis`. Access as a bare identifier
137
- // with `typeof` guard — same pattern as `runtime-lynx/src/bg-bridge.ts`.
137
+ // with `typeof` guard — same pattern as `lynx-runtime/src/bg-bridge.ts`.
138
138
  const lynxObj: LynxLike | undefined = typeof lynx !== 'undefined'
139
139
  ? (lynx as unknown as LynxLike)
140
140
  : undefined;