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

@pnlight/sdk-react

v0.7.0

Published

PNLight Remote UI React component powered by DivKit

Readme

@pnlight/sdk-react

A thin React wrapper around the official DivKit Web renderer with PNLight Remote UI behavior installed automatically.

Install

npm install @pnlight/sdk-react

Use

import { RemoteUi } from "@pnlight/sdk-react";
import "@pnlight/sdk-react/styles.css";

export function Screen({ config }: { config: object }) {
  return (
    <RemoteUi
      id="home"
      json={config}
      theme="system"
      onCustomAction={(action) => {
        console.log(action.url);
      }}
      onError={({ error }) => {
        console.error(error);
      }}
    />
  );
}

Like the official @divkitframework/react package, the component accepts the DivKit client render options except target and hydrate. PNLight supplies its own target, custom components, global scaling/safe-area variables, Lottie extension, and protocol handlers.

The package also exports PNLightDivKit as an alias for RemoteUi.

The registered custom components include pnlight.animated_prepend_list. It uses the same props as the iOS component: increasing count by one smoothly prepends the next discovery-ordered item, while larger jumps or resets apply immediately. Rows use intrinsic content height, and instance_id preserves the displayed count if DivKit replaces the underlying custom element.

On the web, DivKit does not make expressions inside custom_props reactive. For a variable-driven list, keep count as a literal initial value and pass the variable name through count_variable:

{
  "custom_props": {
    "count": 0,
    "count_variable": "visible_item_count",
    "items": []
  }
}

The component subscribes to that DivKit variable and applies the same prepend animation for each increment.

Progress bar and animated number

schemaVersion: 3 documents can also use pnlight.progress_bar and pnlight.animated_number, with the same props as the iOS components. The bar animates from its current fill to a new progress, and the label counts through every value between the displayed number and a new value, so markup does not have to drive either with DivKit animations.

The same reactivity rule applies: keep progress / value as the literal initial value and name the variable in progress_variable / value_variable.

{
  "type": "custom",
  "custom_type": "pnlight.progress_bar",
  "width": { "type": "match_parent" },
  "height": { "type": "fixed", "value": 10 },
  "custom_props": {
    "instance_id": "scan",
    "progress": 0,
    "progress_variable": "scan_progress"
  }
}

Numbers are formatted with Intl.NumberFormat for the browser's locale, the web bar uses a CSS transition, and both components respect prefers-reduced-motion. On the web track_height is also the bar's minimum thickness, so give it the same value as any DivKit height thinner than its 8 default. In a schemaVersion 1 or 2 document neither custom_type is registered, matching the native version gate.

PNLight actions

Navigation, dialogs, and haptics are consumed inside the component for schemaVersion: 2 and newer documents. Observe them when needed:

<RemoteUi
  id="offer"
  json={config}
  onPNLightAction={({ kind, url, logId, message }) => {
    console.log(kind, url, logId, message);
  }}
/>

Unconsumed custom URLs continue through DivKit's standard onCustomAction callback.

Close action

A schemaVersion: 5 document asks the host to close it with the plain URL action pnlight://close. On iOS the SDK only notifies the app, which dismisses the screen itself. A browser has no app to do that, so the component covers the rendered document with a full-view "Screen is closed" placeholder and then calls onClosed:

<RemoteUi
  id="offer"
  json={config}
  onClosed={() => {
    // Optional: unmount the component to show your own UI instead.
  }}
/>

The same action is reported through onPNLightAction with kind: "close". Right after onClosed, the component also replays the legacy custom action { url: "myapp://close", log_id: "close_button" } through onCustomAction, matching the native SDKs, so hosts that still close from that callback keep working. Mounting the component again (for example with a new id) renders the document from scratch.

Safe-area insets

Documents that declare a safe_area read the phone's insets from the safe_area_top, safe_area_bottom, safe_area_left and safe_area_right variables (mode content) or get them as padding (mode inset). A browser has no phone, so the component simulates a fixed one: 24 on top and 16 at the bottom. A host that draws a particular phone around the document passes that phone's insets instead, in CSS pixels:

<RemoteUi
  id="preview"
  json={config}
  safeAreaInsets={{ top: 59, bottom: 34 }}
/>

Edges left out keep the simulated values. Pass a stable object: a new one on every render remounts the document.

Restore action

A schemaVersion: 6 document restores purchases with the typed custom action pnlight.restore. params and both follow-up lists are optional:

{
  "log_id": "restore",
  "typed": { "type": "custom" },
  "payload": {
    "id": "pnlight.restore",
    "params": { "on_success": [], "on_fail": [] }
  }
}

On iOS the SDK syncs App Store purchases, then runs on_success, or on_fail on error. On the web the store simulator opens a "Restore purchases" dialog: Success executes on_success, Fail executes on_fail, and dismissing the dialog runs nothing. Only one purchase or restore dialog is open at a time.

Products

A schemaVersion: 6 document (single card or flow) can declare up to 16 products at its root. An entry is a product id, or an object with id, optional selected, and an optional fallback shown until store data arrives:

"products": [
  "com.app.monthly",
  { "id": "com.app.yearly", "selected": true,
    "fallback": { "price": "$39.99", "period": "year", "period_count": 1,
      "price_per_month": "$3.33", "price_per_week": "$0.77",
      "has_trial": true, "trial_count": 3, "trial_unit": "day",
      "intro_price": "" } }
]

Fallback keys are the strings name (what the document calls the plan; the store has no such name), price, period, price_per_month, price_per_week, trial_unit, intro_price, the integers period_count, trial_count, and the boolean has_trial; unknown keys and wrong types are ignored. Empty or duplicate ids, or a malformed entry, fail the load through onError with an Invalid PNLight products: ... message.

Before the card renders, three global variables are published to every route:

| Variable | Type | Value | | --- | --- | --- | | products | array | One dict per product in document order. Every dict has all keys: id, the fallback keys above (defaults "", 0, false), is_selected, and is_loaded (store data exists for the id). | | selected_product | string | The selected id: the first entry with selected: true, else the first entry. | | selected | dict | The products entry of the selected product. |

Each dict is the defaults, overlaid by fallback, overlaid by store data. A document selects a product with an ordinary set_variable action on selected_product; products and selected are republished immediately. Render plans with DivKit's item_builder, and purchase the selection by passing "@{selected_product}" as the pnlight.purchase product_id:

{
  "type": "container",
  "items": [
    {
      "type": "container",
      "item_builder": {
        "data": "@{products}",
        "data_element_name": "product",
        "prototypes": [{
          "div": {
            "type": "text",
            "text": "@{product.getString('price')} / @{product.getString('period')}",
            "paddings": { "left": 16, "right": 16, "top": 14, "bottom": 14 },
            "margins": { "bottom": 8 },
            "border": {
              "corner_radius": 12,
              "stroke": {
                "width": 2,
                "color": "@{product.getBoolean('is_selected') ? '#0A84FF' : '#D1D1D6'}"
              }
            },
            "actions": [{
              "log_id": "select_product",
              "typed": {
                "type": "set_variable",
                "variable_name": "selected_product",
                "value": { "type": "string", "value": "@{product.getString('id')}" }
              }
            }]
          }
        }]
      }
    },
    {
      "type": "text",
      "text": "@{selected.getInteger('trial_count')}-@{selected.getString('trial_unit')} free trial",
      "visibility": "@{selected.getBoolean('has_trial') ? 'visible' : 'gone'}"
    },
    {
      "type": "text",
      "text": "Continue for @{selected.getString('price')}",
      "actions": [{
        "log_id": "buy",
        "typed": { "type": "custom" },
        "payload": {
          "id": "pnlight.purchase",
          "params": { "product_id": "@{selected_product}" }
        }
      }]
    }
  ]
}

Supply store data keyed by product id; every field is optional:

<RemoteUi
  id="offer"
  json={config}
  storeProducts={{
    "com.app.yearly": { price: "€35.99", period: "year", has_trial: true,
      trial_count: 3, trial_unit: "day" }
  }}
/>

Changing storeProducts republishes the variables live without remounting the document and keeps the current selection. The web purchase simulator resolves @{selected_product}, so its dialog title shows the real product id.

Document files (schema v7)

A schemaVersion: 7 document built by the PNLight console reads its pictures from storage and lists them under the root files array (id, url, contentType, bytes). The component downloads and decodes every listed file before it renders the first screen, waiting up to four seconds, so the screen appears whole; a file that fails or arrives late loads when it is drawn.

Where the web renderer is filled in (schema v7)

DivKit for the web leaves two things out that native DivKit plays, so the component plays them itself; documents stay as written for the apps.

  • Expressions in custom_props. A pnlight.cta_button whose title is "@{selected.getString('price')}" (or any custom component prop holding @{…}) shows the live value and follows the variables it reads, instead of the raw expression. The *_variable props of the progress bar and the animated number keep working as before.
  • Entrances around item_builder. A transition_in on a list built from item_builder, on a prototype inside it, or on any box above it makes DivKit for the web fail the card ("Expression execution error") or empty its texts. The component takes those transitions off the document and plays them with the Web Animations API when the element becomes visible: fade (from alpha), slide (from edge by distance), scale, and a set of them, with duration, start_delay and interpolator.

Styling

Import @pnlight/sdk-react/styles.css once. Give the component or its parent an explicit height when the document uses match_parent:

<RemoteUi id="full-screen" json={config} style={{ minHeight: "100dvh" }} />

Compatibility

  • Missing schemaVersion, or version 1, keeps legacy edge-to-edge behavior.
  • schemaVersion: 2 enables PNLight flows, safe areas, scaling variables, dialogs, and haptics.
  • schemaVersion: 3 preserves all v2 behavior, accepts SDK-owned typed actions such as pnlight.purchase, and registers the pnlight.progress_bar and pnlight.animated_number components. Its purchase simulator preserves the original success-only contract: Success executes on_success, while Fail only closes the simulator.
  • schemaVersion: 4 preserves all v3 behavior and adds on_fail and on_cancel to pnlight.purchase. The simulator adds a Cancel button; Fail executes on_fail, while Cancel or dismissing the simulator executes on_cancel. No real StoreKit transaction occurs.
  • schemaVersion: 5 preserves all v4 behavior and consumes pnlight://close: the component shows its "Screen is closed" view, calls onClosed, and replays the legacy myapp://close custom action. In older schemas the URL reaches onCustomAction untouched.
  • schemaVersion: 6 preserves all v5 behavior, consumes the pnlight.restore typed action, publishes root-level products as the products, selected_product, and selected variables fed by the storeProducts prop, and accepts "@{selected_product}" as a pnlight.purchase product_id. In older schemas pnlight.restore reaches onCustomAction untouched and products is ignored.
  • schemaVersion: 7 preserves all v6 behavior, prefetches and decodes root files before first render (up to four seconds), resolves expressions in custom component props, and plays otherwise unsupported entrances around item_builder on the web. Versions 1–6 retain their previous immediate rendering and unmodified DivKit document behavior.
  • schemaVersion: 8 preserves v7 document behavior. The native SDK can also honor the backend UI response's deferHomeGesture flag while Remote UI is onscreen. The web preview has no equivalent system gesture.
  • CTA, icon button, circular loader, animated prepend list, and Lottie support are additive.

This initial package is client-side only. Use it from a client component in SSR frameworks.