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

pandora-box-react

v0.4.1

Published

Puck-free, framework-agnostic runtime that renders low-code builder JSON with your own components (React web + React Native).

Downloads

87

Readme

pandora-box

A Puck-free, framework-agnostic runtime that renders low-code builder JSON with your own components. Give it the page document + a component registry + the manifest; it walks the JSON and renders. The same document renders on React (web) and React Native — only the registry differs. No editor, no Puck, no design-system lock-in.

┌──────────────────────────┐      page document (JSON)      ┌──────────────────────────────┐
│  Builder (Contentful app │  ──────────────────────────▶   │  Your app                    │
│  / cross-platform-low-   │   { root, content, zones }     │  pandora-box-react renders   │
│  code-engine)            │                                │  the doc with YOUR registry  │
└──────────────────────────┘                                └──────────────────────────────┘
        ▲                                                            ▲
        │ manifest.json (prop schema, generated by the extractor)    │
        └──────────────────── pandora-box-manifest ──────────────────┘

The package family

| package | what it is | when you need it | | --- | --- | --- | | pandora-box-react (this repo) | The runtime: PageRuntime / createRuntime / Render. Walks the document, resolves i18n text, $media refs, actions, slots/arrays; renders via your registry. | Always. | | pandora-box-manifest | The published prop schema (manifest.json + types). Tells the runtime which props are slots / arrays / text / images. Regenerated by the builder repo's extractor — never hand-edited. | Always (pass it to the runtime). | | pandora-box-layout | The engine components: free-layout primitives (Overlay, Positioned, Flex, Card), Swiper, Typography, and the templates (HeroOverview, WhatsNew, UpcomingList, ServiceList). Styled parts read dp-design live tokens (white-label). | When your documents use engine components/templates (the standard setup does). | | pandora-box-dp | The dp-design adapter: dpRegistry (the full @dragonpass/atom-ui-mobile component map, keyed by manifest type ids) + DpProvider (ConfigProvider wrapper) + DpConfig (locale context). | When your documents use dp-design components (the standard setup does). | | cross-platform-low-code-engine | The builder monorepo: Puck editor, Contentful app, extractor (regenerates the manifest), playground. Ops author documents here. | You deploy it; app code never imports it. |

Version pairing (keep these in lockstep — the manifest describes what the components expose):

pandora-box-react ^0.3.0 · pandora-box-manifest ^0.2.8 · pandora-box-layout ^0.1.17 · pandora-box-dp ^0.2.1
npm install pandora-box-react pandora-box-manifest pandora-box-layout pandora-box-dp

Peers: react >= 18; pandora-box-dp / pandora-box-layout expect the host app to provide @dragonpass/atom-ui-mobile (>= 1.1.0-beta.23, from the private registry) and its CSS — your app is already a dp-design app, so nothing is bundled twice.

Full integration (the standard dp-design setup)

This is the wiring used in production (@dragonpass/benefit-common), end to end.

1. Assemble the runtime once — dp components + engine components, the published manifest, DpProvider as the wrapper, and your action dispatcher:

// pandora-page.tsx
import { createRuntime, type ComponentRegistry, type Manifest } from 'pandora-box-react';
import { manifest } from 'pandora-box-manifest';
import { dpRegistry, DpProvider } from 'pandora-box-dp';
import {
    Flex, Card, Overlay, Positioned, Swiper, Typography,
    MediaCaption, MediaCarousel, HeroOverview, WhatsNew, UpcomingList, ServiceList,
} from 'pandora-box-layout';
import { openWebview } from '@dragonpass/miniapp-jsbridge';

// registry = every type id the manifest knows: dp-design components + engine components.
// NB: `Swiper` comes from pandora-box-layout (full-bleed image + `dots` toggle) — it is
// NOT in dpRegistry since pandora-box-dp 0.2.1.
const registry = {
    ...dpRegistry,
    Flex, Card, Overlay, Positioned, Swiper, Typography,
    MediaCaption, MediaCarousel, HeroOverview, WhatsNew, UpcomingList, ServiceList,
} as unknown as ComponentRegistry;

export const PandoraPageRuntime = createRuntime({
    registry,
    manifest: manifest as unknown as Manifest,
    wrapper: DpProvider,          // dp-design ConfigProvider; reads locale from DpConfig
    fallbackLocale: 'en',
    onAction: (action) => {
        if (action.type === 'navigate' && action.href) openWebview(action.href, {});
    },
});

2. Inject the active locale at your app root (DpProvider reads it):

import { DpConfig } from 'pandora-box-dp';

<DpConfig.Provider value={{ locale: currentLocale }}>
    <App />
</DpConfig.Provider>

3. Fetch the document and render. Documents live wherever your builder publishes them (ours: a Contentful entry's document field):

import { type DocData } from 'pandora-box-react';

export const PandoraSection = ({ entryId }: { entryId: string }) => {
    const { currentLocale } = useLang('common');
    const [doc, setDoc] = useState<DocData | null>(null);
    useEffect(() => { fetchPandoraDoc(entryId).then((d) => d && setDoc(d)); }, [entryId]);
    if (!doc) return null;
    // builder docs key localized text by SHORT codes ({en, zh}) — fall back accordingly
    const short = (currentLocale || 'en').split('-')[0];
    return <PandoraPageRuntime doc={doc} locale={currentLocale} fallbackLocale={short} />;
};

4. Sizing (rem). Engine components emit inline sizes as rem against a 375px design base (toRem(px), rootValue 37.5). If your app uses amfe-flexible (html font-size = viewport/10), everything lines up out of the box. A different rem system? Configure once at startup:

import { configureRem } from 'pandora-box-layout';
configureRem({ rootValue: 16 });            // match your postcss-pxtorem rootValue
// or keep plain px: configureRem({ convert: (px) => `${px}px` });

That's it. Anything ops publish from the builder now renders in your app.

Keeping registry ↔ manifest in sync

The builder repo's extractor regenerates pandora-box-manifest from the component sources — whatever ops can drag in the builder is exactly what the manifest describes. Your registry must cover those type ids; unknown types are skipped (never crash). When the family bumps (layout/dp/manifest), update the three together and you're done — documents are forward-compatible (old props that a component dropped are simply ignored).

White-label theming

Styled engine components read dp-design's live seed tokens (useToken() / var(--aum-*)), and colour props store dp token names (e.g. "linkColor", resolved at render via resolveColor; Overlay backgrounds accept "successColor 0.6"color-mix). Re-skinning the dp ConfigProvider theme re-skins every published page — no document changes needed.


Bring-your-own components (generic, no dp-design)

import { PageRuntime } from 'pandora-box-react';
import { registry } from './my-registry';        // type id → your component
import manifest from './manifest.generated.json'; // emitted by your builder/extractor

export function Page({ doc, locale }) {
  return (
    <PageRuntime
      doc={doc}                 // the "Page JSON" the builder produces
      registry={registry}       // { Button, Image, Swiper, ... }
      manifest={manifest}       // tells the walker which props are slots / arrays / text
      locale={locale}           // "en" | "zh" | …  → localized text resolves to this
      fallbackLocale="en"
    />
  );
}

Pre-bind once with createRuntime

Bind your registry + manifest (+ an optional provider) once, get a tiny <Runtime doc locale />:

import { createRuntime } from 'pandora-box-react';
import { registry } from './my-registry';
import manifest from './manifest.generated.json';
import { ThemeProvider } from './theme'; // optional design-system provider

export const PageRuntime = createRuntime({
  registry,
  manifest,
  wrapper: ThemeProvider,   // receives `locale`; wraps the rendered page
  fallbackLocale: 'en',
});

// anywhere:
<PageRuntime doc={pageJson} locale="zh" />

React Native

Same component, native registry — the document JSON is identical across platforms:

import { createRuntime } from 'pandora-box-react';
import { registry } from './my-registry.native'; // your RN components by the same type ids
import manifest from './manifest.generated.json';

export const PageRuntime = createRuntime({ registry, manifest });

How it works

  • The document is { root, content: Node[], zones }; each Node is { type, props }.
  • The manifest describes each component's props. The walker uses it to know which props are slots (Node[] → recurse), arrays (rows, each possibly with slots/text), and text (resolve a { locale: string } map to the active language).
  • Unknown node.types are skipped (or pass a fallback). Each component is wrapped in an error boundary, so one bad component never crashes the page.

API

| export | description | | --- | --- | | PageRuntime | { doc, registry, manifest, locale?, fallbackLocale?, fallback?, wrapper?, onAction?, bindings?, transforms?, flags? } → renders the page | | createRuntime | pre-bind { registry, manifest, wrapper?, fallbackLocale?, onAction? }(props) => <Runtime/> | | Render | the low-level walker: { data, registry, manifest, locale?, fallbackLocale?, fallback?, onAction?, bindings?, transforms?, flags? } | | resolveLocalized(value, locale?, fallback?) | resolve a LocalizedString to a string | | isLocalizedMap(value) | is a value a { locale: string } map? | | resolveMedia(value) / isMediaRef(value) | resolve a { $media } image reference to its snapshot URL | | isAction(value) | is a value a usable declarative Action? | | applyTemplate(tpl, data) / hasTemplate(s) | fill / detect {{ … }} slots — the same functions the walker uses | | resolveBinding(rows, fields) / mapItem / getByPath | map a raw array onto a list component's items | | types | DocData, Node, Manifest, ComponentManifest, ManifestField, FieldDescriptor, LocalizedString, MediaRef, MediaValue, Action, ActionTarget |

i18n

Text props can be a plain string or a { locale: string } map:

{ "type": "Typography", "props": {
    "text": { "en": "Enjoy your Travel in China", "zh": "畅游中国之旅" } } }

The runtime resolves text to locale (→ fallbackLocale → first available → as-is). Plain strings keep working unchanged, so localization is fully backward-compatible.

Media ($media)

An image field's value can be a plain URL or a hybrid media reference carrying a CMS asset id plus a denormalized snapshot:

{ "src": { "$media": { "provider": "contentful", "kind": "asset", "id": "6WoO…", "url": "https://images.ctfassets.net/…/2.jpg" } } }

The runtime resolves it to the snapshot url before it reaches your component (top-level and inside array rows). Plain URL strings pass through unchanged.

Actions (onAction)

Documents are props-only JSON, so interactions are stored as DATA — an action descriptor ({ type: 'navigate', href } or { type: 'event', name, payload? }). The runtime turns it into a real onClick that calls YOUR onAction dispatcher (top-level action prop and per-row inside array fields with an action item field):

<PageRuntime
  doc={doc}
  registry={registry}
  manifest={manifest}
  onAction={(action) => {
    if (action.type === 'navigate') openWebview(action.href);
  }}
/>

The host decides what navigation means (web location, in-app webview, RN navigator) — the document stays portable.

Data (bindings)

Same idea as actions: the document says WHAT it wants, you fetch it and pass it in. Any text prop can carry {{ … }} slots, filled from bindings — an email template, basically:

{ "type": "Typography", "props": {
    "text": "{{ guests }} people at {{ time }} on {{ date }}" } }
<PageRuntime doc={doc} registry={registry} manifest={manifest}
  bindings={{ guests: 2, time: '14:00', date: '8 June 2026' }} />

The shape is yours. Use {{ a.b }} to reach into a nested object, which is also how several sources coexist without colliding:

bindings={{ booking: { time: '14:00' }, flight: { time: '16:30' } }}
// "{{ booking.time }}" and "{{ flight.time }}" stay distinct

The engine never interprets a value — it substitutes what you passed, so a value already formatted the way you want it is the simplest thing that works. If you'd rather format in the page, {{ at | date }} / | time / | upper / | lower / | default:x / | map:A=x,B=y are available, and | transform:name calls one of your own transforms functions.

Dates keep the offset the value carries: 2026-06-08T14:00:00+01:00 renders as 14:00 on every device, not re-stated in the reader's timezone. A value with no offset (2026-06-08T14:00, or an epoch number) never said where it was, so it falls back to the device.

A path that isn't in bindings renders empty and the rest of the sentence still ships. Text with no {{ … }} is untouched, so this costs nothing for ordinary copy.

For a LIST, a data-bound component's binding.source names an array in bindings and the saved field map shapes each row onto its items.

License

MIT