/** * `` — keeps the device status-bar (and Android's * navigation-bar) tint legible against the active theme. * * Reads the current theme via `useTheme()`, looks up its variant in the * theme registry, and pushes the appropriate tint to the OS via * `@sigx/lynx-appearance`: * * light theme → dark status-bar icons (legible against light bg) * dark theme → light status-bar icons (legible against dark bg) * * Mount once, inside ``: * * ```tsx * * * * * ``` * * Renders nothing — it's a side-effect-only component that drives a * reactive `effect()` reading `theme.name`. The `matchBackground` prop is * reserved for a follow-up that pushes the active theme's * `--color-base-100` as the Android system-bar background; today it's a * declared no-op so the API surface is stable across the rev that wires * CSS-var resolution. */ import { component, effect, onMounted, onUnmounted, type Define } from '@sigx/lynx'; import { isAvailable, setSystemBarsStyle } from '@sigx/lynx-appearance'; import type { SystemBarStyle } from '@sigx/lynx-appearance'; import { themeController } from './theme-state.js'; import { variantOf } from './registry.js'; export type StatusBarSyncProps = /** * Reserved — will (in a follow-up) push the active theme's * `--color-base-100` as the Android status- and navigation-bar * background. Currently a no-op; the prop ships so consumers can opt * in without an API break later. iOS and Android 15+ ignore the * background regardless (no equivalent on iOS; edge-to-edge on * Android 15+). */ & Define.Prop<'matchBackground', boolean, false>; export const StatusBarSync = component(({ props }) => { // Bind to the *global* theme — not `useTheme()` — so the OS bars always // track the app/screen theme and can't be hijacked by a content sub-scope // (a nested `` recolors its subtree but leaves the bars put). const theme = themeController; let lastApplied: string | null = null; let runner: { stop: () => void } | undefined; function apply(name: string): void { if (name === lastApplied) return; lastApplied = name; const variant = variantOf(name); // For unregistered themes we can't infer a variant — leave the // system bars alone. Consumers can register their custom theme via // `registerTheme()` to opt in. if (!variant) return; const style: SystemBarStyle = variant === 'dark' ? 'light' : 'dark'; // Fire-and-forget: `setSystemBarsStyle` is non-throwing (it // resolves `{ ok: false, reason: 'unsupported' }` when the native // module isn't registered, and silently filters per-leg // `unsupported` results — e.g. nav-bar on iOS — so an aggregate // `ok: true` is still reachable on partial platforms). Either way, // void-discarding the promise here can't surface as an unhandled // rejection. void setSystemBarsStyle({ statusBar: style, navigationBar: { style }, }); } onMounted(() => { if (!isAvailable()) return; // A reactive effect that reads `theme.name` so the effect re-runs // whenever the theme controller's underlying signal changes — // including the live system-flip path inside ThemeProvider. No // side effects in render; nothing to subscribe/unsubscribe by // hand. runner = effect(() => { apply(theme.name); }); }); onUnmounted(() => { runner?.stop(); runner = undefined; }); // Reference the prop so the type checker doesn't flag it as unused // while it's still reserved. Drop this when the matchBackground // implementation lands. void props.matchBackground; // Zero-size, out-of-flow placeholder. Avoids `display: none` — // Lynx can leak unstyled text paint through display:none overlays in // some builds (see lynx-display-none caveat); zero-size + absolute is // the safer shape. return () => ( ); });