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/capacitor

v0.3.0

Published

Capacitor plugin for Lumen — real native screenshots, screen recordings, shake-to-open and keyboard handling for the feedback widget.

Downloads

1,055

Readme

@lumen-stack/capacitor

Native Capacitor plugin for the Lumen feedback widget. Inside a Capacitor app it replaces the DOM screenshot (html2canvas) with real screenshots and adds native screen recording, shake-to-open and keyboard handling on iOS and Android.

| | iOS | Android | |---|---|---| | Screenshot | drawHierarchy of every visible window in the scene (WebView, native views, video, keyboard, alerts) | PixelCopy of the window surface (API 26+), View.draw fallback | | Screen recording | ReplayKit in-app capture → H.264 MP4 (no broadcast extension); the keyboard is drawn in, see below | MediaProjection + foreground service → H.264 MP4 | | Consent | iOS system prompt, once per app | System dialog every recording (Android 14+: "single app" option) | | Shake to open | CoreMotion, only while enabled | Accelerometer, only while enabled and app in foreground |

Requires Capacitor 6, 7 or 8.

What a recording contains

iOS draws the system keyboard in its own window, and in-app ReplayKit capture only gets the app's windows, so the real keyboard is never in the clip. The plugin tracks the keyboard frame and paints a dark KEYBOARD panel at that position into every frame, so "the keyboard slides over the input" is visible. On Android the keyboard is captured as is.

On both platforms the widget also logs taps, focus, typing (never the value), the keyboard, scrolling and page changes while recording. Taps show up in the clip as a short orange ring. The log is sent with the clip and used by the video analysis.

Install

npm install @lumen-stack/capacitor @lumen-stack/react
npx cap sync

That's it. <LumenProvider> detects the plugin at runtime and routes capture, recording, shake and keyboard through it. No extra code in your app:

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

<LumenProvider apiKey="lk_pub_…">{children}</LumenProvider>

In the browser (and npx cap run on the web platform) the same code keeps using DOM screenshots and getDisplayMedia.

Or let the CLI do it: npx @lumen-stack/cli init detects Capacitor, installs both packages and runs npx cap sync.

Updating

Update the Lumen packages together, sync, and rebuild the native app:

npm install @lumen-stack/react@latest @lumen-stack/capacitor@latest --prefer-online
npx cap sync
npx cap run ios   # and/or android — or build from Xcode / Android Studio

(npx @lumen-stack/cli init does the install + sync for you; re-running it upgrades.) A live-update / OTA bundle only carries the JS: the plugin's native code reaches users with a new native build. If the plugin is missing from the native build, Lumen falls back to DOM screenshots and logs [lumen] Native Capacitor … app detected, but the @lumen-stack/capacitor plugin is missing — the feedback's capture warnings in the dashboard say the same.

Allowed origins

Add your app's WebView origin under Settings → Allowed origins in the Lumen dashboard:

  • iOS: capacitor://localhost
  • Android: https://localhost

(Use your own values if you changed server.hostname / server.iosScheme / server.androidScheme.)

iOS

  • CocoaPods and Swift Package Manager are both supported; npx cap sync picks whichever your project uses.
  • For recordings with microphone audio, add NSMicrophoneUsageDescription to Info.plist.

Android

The plugin's manifest adds what MediaProjection needs (merged automatically):

  • FOREGROUND_SERVICE, FOREGROUND_SERVICE_MEDIA_PROJECTION
  • a mediaProjection foreground service that shows a "Recording screen" notification while recording
  • RECORD_AUDIO (voice notes, optional mic in recordings)

Recording stops automatically when the app goes to the background, so nothing outside your app ends up in the clip.

Google Play: apps using the mediaProjection foreground service type must declare it in the Play Console (App content → Foreground service permissions). Describe it as "user-initiated screen recording to attach to a bug report".

Options

Everything is automatic. To opt out and keep web-only behavior:

<LumenProvider apiKey="…" nativeBridge={false} />

To tune the native host, pass the detectCapacitorHost options (below) instead of true:

<LumenProvider apiKey="…" nativeBridge={{ screenshotStatusBar: "synthetic" }} />

An explicit capture={{ provider }}, capture={{ mode: "dom" }} or record={{ provider }} also wins over the plugin.

For direct access (e.g. your own UI):

import { LumenCapacitor, detectCapacitorHost } from "@lumen-stack/capacitor";

const host = detectCapacitorHost({ screenshotFormat: "png", withMicrophone: true });
<LumenProvider capture={{ mode: "custom", provider: host?.captureProvider }} record={host?.record} />

detectCapacitorHost(options):

| Option | Default | | |---|---|---| | screenshotFormat | "jpeg" | "png" for lossless; JPEG is ~5–10× smaller and opens faster | | screenshotQuality | 0.9 | JPEG quality 0–1 | | withMicrophone | false | Record mic audio with the screen (Android: only if RECORD_AUDIO is already granted) | | screenshotStatusBar | "none" | iOS only. "synthetic" paints a reconstructed status bar (time, battery) into screenshots. See below |

iOS status bar in screenshots

iOS draws the status bar (time, battery, notch area) outside the app's windows, and no public API lets an app capture it. Screenshots therefore show the app with an empty strip at the top. With screenshotStatusBar: "synthetic", Lumen paints the current time and battery level there, in the status bar's light/dark style, so the screenshot looks like what the user saw. It is a reconstruction, not a capture, and the feedback's capture notes in the dashboard say so. Signal and Wi-Fi are left out, because the app can't read them reliably.

A single ReplayKit frame (RPScreenRecorder.startCapture) would need the recording consent prompt for every screenshot, so Lumen does not use it for screenshots.

How recording transfers

The native side records into a temp file. When the recording stops, JS pulls the file in 512 KB slices (readRecording) and then deletes it (discardRecording). There is no single multi-MB message over the bridge and no event that has to arrive in time. Leftover files from a crash are removed on the next app start.

Native plugin API

| Method | Returns | |---|---| | captureScreen({ format?, quality?, statusBar? }) | { dataUrl, pixelRatio, statusBar? }. statusBar: "synthetic" (iOS) paints a reconstructed status bar; the result says whether it was drawn | | isRecordingAvailable() | { available } | | startRecording({ maxDurationMs, withMicrophone }) | { recordingId }, after consent and once frames flow. Rejects with code: permission_denied, already_recording, unavailable, start_failed | | stopRecording({ recordingId }) | { recordingId, size, durationMs, mimeType, posterDataUrl? }. iOS adds a natively decoded first frame (JPEG) that the widget uses as the preview | | readRecording({ recordingId, offset, length }) | { data } (base64) | | discardRecording({ recordingId }) / cancelRecording({ recordingId }) | – | | setShakeEnabled({ enabled }) | – |

| Event | Payload | |---|---| | lumenRecordingStopped | { recordingId, reason: "maxDuration" \| "background" \| "system" } | | lumenShake | {} | | lumenKeyboardChange | { height, keyboardHeight, visible }: height is the CSS px of the WebView covered by the keyboard (0 if the WebView resizes), keyboardHeight the full keyboard height in CSS px |