pandora-box-react
v0.3.0
Published
Puck-free, framework-agnostic runtime that renders low-code builder JSON with your own components (React web + React Native).
Maintainers
Readme
pandora-box-react
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.
npm install pandora-box-reactPeer dependency:
react >= 18.
Quick start
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 }; eachNodeis{ type, props }. - The
manifestdescribes 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 afallback). 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? } → 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? } |
| 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? |
| 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.
License
MIT
