npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@lumen-stack/expo

v0.2.0

Published

Expo / React Native helper for Lumen — answers screenshot requests with a real native screen capture.

Downloads

371

Readme

@lumen-stack/expo

Native screenshot bridge for running Lumen inside an Expo / React Native app.

The Lumen web SDK captures screenshots with html2canvas, a DOM re-renderer — it can't match native rendering and drops native overlays plus the live text typed into native <TextInput>s. This package lets the native side take a real React Native snapshot and hand the pixels back to Lumen over the WebView bridge.

Install

npx expo install @lumen-stack/expo react-native-webview react-native-view-shot

react, react-native, react-native-webview, and react-native-view-shot are peer dependencies.

Use the drop-in LumenWebView

Render LumenWebView where you'd render a <WebView>. It intercepts Lumen capture requests and answers them with a native screenshot of the WebView by default; your own onMessage still fires for everything else.

import { LumenWebView } from "@lumen-stack/expo";

export function Feedback() {
  return <LumenWebView source={{ uri: "https://your-app.example" }} />;
}

Inside the web app that loads in the WebView, force the native path so it never silently falls back to the (DOM) html2canvas renderer:

import { LumenProvider, createNativeCaptureProvider } from "@lumen-stack/react";

<LumenProvider
  apiKey="lk_pub_…"
  capture={{ mode: "custom", provider: createNativeCaptureProvider() }}
>
  {/* …your app… */}
</LumenProvider>;

iOS native navigation/tab bars

On iOS, full key-window capture can force UIKit to commit pending screen updates. With native chrome such as UITabBarController, UINavigationBar, or Liquid Glass UIVisualEffectView, that can blank the real native bar until the next UIKit relayout and can interrupt in-flight sheet animations. This is a react-native-view-shot captureScreen() caveat: it snapshots the whole key window with afterScreenUpdates: true.

LumenWebView uses its own WebView ref as the default capture target, which keeps native tab/navigation chrome out of the render pass. If you need to include more React Native UI around the WebView, pass a React Native root or container ref as the capture target. Lumen will use captureRef(target, …) instead of key-window captureScreen(), so UIKit does not re-render native chrome outside that subtree. The screenshot will include the targeted subtree, and will exclude native chrome outside it.

import { useRef } from "react";
import { View } from "react-native";
import { LumenWebView } from "@lumen-stack/expo";

export function FeedbackScreen() {
  const captureRootRef = useRef<View>(null);

  return (
    <View ref={captureRootRef} collapsable={false} style={{ flex: 1 }}>
      <LumenWebView
        captureTarget={captureRootRef}
        capture={{ settleDelayMs: 120 }}
        source={{ uri: "https://your-app.example" }}
      />
    </View>
  );
}

collapsable={false} is important because React Native may otherwise remove a purely layout-only wrapper from the native view hierarchy, leaving captureRef without a real native view to snapshot.

Bring your own WebView

If you manage the WebView yourself, call handleLumenMessage from onMessage:

import { useRef } from "react";
import { WebView } from "react-native-webview";
import { handleLumenMessage } from "@lumen-stack/expo";

const ref = useRef<WebView>(null);

<WebView
  ref={ref}
  source={{ uri: "https://your-app.example" }}
  onMessage={(e) => {
    if (handleLumenMessage(ref.current, e.nativeEvent.data)) return;
    // …your own message handling…
  }}
/>;

To use a targeted capture with your own WebView, pass the same options to handleLumenMessage. For Lumen messages it returns the capture promise; for non-Lumen messages it returns false.

const handled = handleLumenMessage(ref.current, e.nativeEvent.data, {
  captureTarget: captureRootRef,
  settleDelayMs: 120,
});
if (handled) {
  void handled.finally(() => {
    // Optional: trigger any host relayout/repair work after native capture settles.
  });
  return;
}

Capture options

| Option | Purpose | | ---------------------- | ----------------------------------------------------------------------- | | format | Screenshot format, "png" or "jpg". Default "png". | | quality | JPG quality from 0 to 1. Default 1. | | captureTarget | React Native ref, React instance, or react tag captured via captureRef. | | settleDelayMs | Extra delay after two animation frames before capture; clamped 0–500. | | afterScreenUpdates | iOS hint threaded to capture. false avoids forcing a screen-update commit when a compatible native snapshotter is installed. |

The web SDK may also request afterScreenUpdates: false; @lumen-stack/expo now honors that value. On iOS, the package ships a small autolinked LumenCapture native module that uses drawViewHierarchyInRect:afterScreenUpdates:NO for key-window capture when that flag is false. Native navigation apps should still use LumenWebView's default targeted capture or pass a captureTarget root/container ref, because targeted capture keeps native chrome outside the subtree out of the render pass entirely. Low-level respondToCaptureRequest calls with no target fall back to react-native-view-shot key-window captureScreen() when the native module is unavailable.

Shake to open

Wire any shake detector to postLumenShake; the web SDK opens the sheet when shake-to-open is enabled:

import { Accelerometer } from "expo-sensors";
import { postLumenShake } from "@lumen-stack/expo";

Accelerometer.addListener(({ x, y, z }) => {
  if (Math.abs(x) + Math.abs(y) + Math.abs(z) > 2.5) postLumenShake(ref.current);
});

Keyboard inset

Inside an iOS WKWebView window.visualViewport does not shrink when the soft keyboard opens, so the web SDK can't keep the floating trigger and the feedback input above the keyboard on its own. LumenWebView forwards the native keyboard height automatically (forwardKeyboardInset, default true); Lumen then lifts its trigger and modal input above it.

Bringing your own WebView? Push the height yourself:

import { Keyboard } from "react-native";
import { postLumenKeyboardInset } from "@lumen-stack/expo";

Keyboard.addListener("keyboardDidShow", (e) =>
  postLumenKeyboardInset(ref.current, e.endCoordinates.height),
);
Keyboard.addListener("keyboardDidHide", () =>
  postLumenKeyboardInset(ref.current, 0),
);

API

| Export | Purpose | | ----------------------------- | ----------------------------------------------------------------- | | LumenWebView | Drop-in WebView that auto-answers capture requests + forwards the keyboard height. | | handleLumenMessage(w, d) | Handle a WebView message; returns the capture promise or false. | | respondToCaptureRequest | Low-level: capture + reply to a specific request id. | | postLumenShake(w) | Open the Lumen sheet by simulating a shake in the WebView. | | postLumenKeyboardInset(w, px) | Forward the soft-keyboard height so Lumen lifts above it. | | parseCaptureRequest(d) | Pure parser for capture-request messages. |