@plenary/react 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/render.js CHANGED
@@ -13,8 +13,33 @@ import { registry } from "./registry.js";
13
13
  import { Placeholder } from "./components/Placeholder.js";
14
14
  import { toResolvedMedia } from "./media.js";
15
15
  const MEDIA_PROP_TYPES = /* @__PURE__ */ new Set(["image", "file"]);
16
- function mediaPropKeys(reg) {
17
- return (reg.props ?? []).filter((p) => MEDIA_PROP_TYPES.has(p.type)).map((p) => p.key);
16
+ function mapMediaProps(reg, props, fn) {
17
+ let out = props;
18
+ const set = (key, value) => {
19
+ if (out === props) out = { ...props };
20
+ out[key] = value;
21
+ };
22
+ for (const def of reg.props ?? []) {
23
+ const val = props[def.key];
24
+ if (MEDIA_PROP_TYPES.has(def.type)) {
25
+ if (val) set(def.key, fn(val));
26
+ continue;
27
+ }
28
+ const mediaFields = (def.fields ?? []).filter((f) => MEDIA_PROP_TYPES.has(f.type));
29
+ if (def.type !== "list" || mediaFields.length === 0 || !Array.isArray(val)) continue;
30
+ set(
31
+ def.key,
32
+ val.map((item) => {
33
+ if (!item || typeof item !== "object") return item;
34
+ const next = { ...item };
35
+ for (const f of mediaFields) {
36
+ if (next[f.key]) next[f.key] = fn(next[f.key]);
37
+ }
38
+ return next;
39
+ })
40
+ );
41
+ }
42
+ return out;
18
43
  }
19
44
  function walkNodes(nodes, visit) {
20
45
  for (const node of nodes) {
@@ -33,10 +58,10 @@ async function resolveMediaDetails(document, client) {
33
58
  walkNodes(document.root, (node) => {
34
59
  const reg = registry.get(node.component)?.registration;
35
60
  if (!reg) return;
36
- for (const key of mediaPropKeys(reg)) {
37
- const val = node.props[key];
38
- if (val && val.media_id && !val.url) ids.add(val.media_id);
39
- }
61
+ mapMediaProps(reg, node.props, (val) => {
62
+ if (val.media_id && !val.url) ids.add(val.media_id);
63
+ return val;
64
+ });
40
65
  });
41
66
  await Promise.all(
42
67
  [...ids].map(async (id) => {
@@ -50,18 +75,16 @@ async function resolveMediaDetails(document, client) {
50
75
  return { details, missing };
51
76
  }
52
77
  function effectiveProps(node, reg, mediaDetails, missingMedia) {
53
- const props = { ...node.props };
78
+ const props = mapMediaProps(
79
+ reg,
80
+ { ...node.props },
81
+ (val) => toResolvedMedia(
82
+ val,
83
+ val.media_id ? mediaDetails.get(val.media_id) : void 0,
84
+ val.media_id ? missingMedia.has(val.media_id) : false
85
+ )
86
+ );
54
87
  for (const key of Object.values(STYLE_PROP_FOR_BREAKPOINT)) delete props[key];
55
- for (const key of mediaPropKeys(reg)) {
56
- const val = props[key];
57
- if (val) {
58
- props[key] = toResolvedMedia(
59
- val,
60
- val.media_id ? mediaDetails.get(val.media_id) : void 0,
61
- val.media_id ? missingMedia.has(val.media_id) : false
62
- );
63
- }
64
- }
65
88
  return props;
66
89
  }
67
90
  function compileStyles(document, cutoffs) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/render.tsx"],"sourcesContent":["import { createElement, Fragment, Suspense, type ReactNode } from \"react\";\nimport {\n PlenaryNotFoundError,\n type PlenaryClient,\n type MediaDetail,\n} from \"@plenary/client\";\nimport {\n STYLE_PROP_FOR_BREAKPOINT,\n compileNodeCss,\n readNodeStyles,\n resolveCutoffs,\n type BreakpointCutoffs,\n type ComponentRegistration,\n type PlenaryDocument,\n type PlenaryNode,\n} from \"@plenary/core\";\nimport { registry } from \"./registry.js\";\nimport { Placeholder } from \"./components/Placeholder.js\";\nimport { toResolvedMedia, type MediaValue } from \"./media.js\";\nimport type { RenderContext } from \"./types.js\";\n\nconst MEDIA_PROP_TYPES = new Set([\"image\", \"file\"]);\n\n/** Prop keys of a registration whose values are media pointers. */\nfunction mediaPropKeys(reg: ComponentRegistration): string[] {\n return (reg.props ?? [])\n .filter((p) => MEDIA_PROP_TYPES.has(p.type))\n .map((p) => p.key);\n}\n\nfunction walkNodes(\n nodes: PlenaryNode[],\n visit: (n: PlenaryNode) => void,\n): void {\n for (const node of nodes) {\n visit(node);\n if (node.slots) {\n for (const children of Object.values(node.slots))\n walkNodes(children, visit);\n }\n }\n}\n\n/**\n * Batch-fetch the detail for every bare `media_id` referenced by a media-typed\n * prop, so nodes can then be rendered synchronously. Values already carrying a\n * URL, and all fetches, are skipped when there is no client.\n *\n * A 404 is reported separately from a failed fetch: it means the asset was\n * deleted while entries still point at it, and such a value must not fall back\n * to the URLs it carries (see `toResolvedMedia`).\n */\nasync function resolveMediaDetails(\n document: PlenaryDocument,\n client: PlenaryClient | undefined,\n): Promise<{ details: Map<string, MediaDetail>; missing: Set<string> }> {\n const details = new Map<string, MediaDetail>();\n const missing = new Set<string>();\n if (!client) return { details, missing };\n\n const ids = new Set<string>();\n walkNodes(document.root, (node) => {\n const reg = registry.get(node.component)?.registration;\n if (!reg) return;\n for (const key of mediaPropKeys(reg)) {\n const val = node.props[key] as MediaValue | undefined;\n if (val && val.media_id && !val.url) ids.add(val.media_id);\n }\n });\n\n await Promise.all(\n [...ids].map(async (id) => {\n try {\n details.set(id, await client.getMedia(id));\n } catch (err) {\n // Anything else degrades to an unresolved value (e.g. a network blip).\n if (err instanceof PlenaryNotFoundError) missing.add(id);\n }\n }),\n );\n return { details, missing };\n}\n\n/** Node props with media-typed values replaced by their resolved shape. */\nfunction effectiveProps(\n node: PlenaryNode,\n reg: ComponentRegistration,\n mediaDetails: Map<string, MediaDetail>,\n missingMedia: Set<string>,\n): Record<string, unknown> {\n const props: Record<string, unknown> = { ...node.props };\n // Editor styles render through the compiled per-node stylesheet, never as\n // props: an inline base style would beat the sheet's media overrides, and\n // components no longer need to do anything for styling to work.\n for (const key of Object.values(STYLE_PROP_FOR_BREAKPOINT)) delete props[key];\n for (const key of mediaPropKeys(reg)) {\n const val = props[key] as MediaValue | undefined;\n if (val) {\n props[key] = toResolvedMedia(\n val,\n val.media_id ? mediaDetails.get(val.media_id) : undefined,\n val.media_id ? missingMedia.has(val.media_id) : false,\n );\n }\n }\n return props;\n}\n\n/**\n * Compile every node's style layers into one stylesheet, and remember which\n * nodes contributed — those are the nodes the published render must wrap so\n * the sheet's `[data-plenary-id] > *` selectors have something to target.\n */\nfunction compileStyles(\n document: PlenaryDocument,\n cutoffs: BreakpointCutoffs,\n): { css: string; styledIds: Set<string> } {\n const parts: string[] = [];\n const styledIds = new Set<string>();\n walkNodes(document.root, (node) => {\n const css = compileNodeCss(node.id, readNodeStyles(node.props), cutoffs);\n if (css) {\n parts.push(css);\n styledIds.add(node.id);\n }\n });\n return { css: parts.join(\"\\n\"), styledIds };\n}\n\ninterface RenderOptions {\n mediaDetails: Map<string, MediaDetail>;\n /** Media ids whose asset was deleted — their values render as missing. */\n missingMedia: Set<string>;\n /** Wrap each node with a `data-plenary-id` marker (preview-bridge mode). */\n annotate?: boolean;\n /** Nodes with compiled styles — wrapped even outside annotate mode. */\n styledIds: Set<string>;\n /**\n * Per-node remount epochs (preview-bridge mode). A bumped epoch changes the\n * node's React key, forcing its subtree to be rebuilt from the document —\n * used after in-place editing, which mutates DOM React never tracked.\n */\n remountKeys?: ReadonlyMap<string, number>;\n}\n\n/** A node's React key: its id, salted by its remount epoch when one exists. */\nfunction nodeKey(id: string, opts: RenderOptions): string {\n const epoch = opts.remountKeys?.get(id);\n return epoch ? `${id}#${epoch}` : id;\n}\n\n/**\n * Wrap a node's element in a layout-neutral marker carrying its id. Every node\n * gets one in annotate mode (the preview bridge maps clicked DOM elements back\n * to nodes through it); on published renders only styled nodes do, since the\n * marker is what the compiled stylesheet's selectors target.\n */\nfunction annotate(\n id: string,\n element: ReactNode,\n opts: RenderOptions,\n empty?: string,\n): ReactNode {\n if (!opts.annotate && !opts.styledIds.has(id)) return element;\n return (\n <div\n key={nodeKey(id, opts)}\n data-plenary-id={id}\n data-plenary-empty={empty}\n style={{ display: \"contents\" }}\n >\n {element}\n </div>\n );\n}\n\nfunction renderNode(node: PlenaryNode, opts: RenderOptions): ReactNode {\n const key = nodeKey(node.id, opts);\n const found = registry.get(node.component);\n if (!found) {\n return annotate(\n node.id,\n <Placeholder key={key} component={node.component} />,\n opts,\n );\n }\n\n const { component: Component, registration } = found;\n const props = effectiveProps(\n node,\n registration,\n opts.mediaDetails,\n opts.missingMedia,\n );\n // Annotated renders are editor previews; the flag lets components show\n // editor-only affordances (e.g. the Image empty-state placeholder) that a\n // published render must not.\n if (opts.annotate) props.editor = true;\n\n // The `children` slot becomes React children; every other slot is delivered\n // via a `slots` prop record of already-rendered nodes.\n let children: ReactNode;\n let slots: Record<string, ReactNode> | undefined;\n if (node.slots) {\n for (const [name, childNodes] of Object.entries(node.slots)) {\n const rendered = (\n <Fragment>\n {childNodes.map((child) => renderNode(child, opts))}\n </Fragment>\n );\n if (name === \"children\") children = rendered;\n else (slots ??= {})[name] = rendered;\n }\n }\n if (slots) props.slots = slots;\n\n return annotate(\n node.id,\n createElement(Component, { ...props, key }, children),\n opts,\n opts.annotate && isEmptyContainer(node) ? node.component : undefined,\n );\n}\n\n/**\n * A built-in layout block with nothing in it. Editor previews mark these\n * (`data-plenary-empty`) so the bridge can hatch them and hold them open to a\n * clickable size; custom components are left alone.\n */\nfunction isEmptyContainer(node: PlenaryNode): boolean {\n if (node.component !== \"box\" && node.component !== \"section\") return false;\n return !Object.values(node.slots ?? {}).some((children) => children.length > 0);\n}\n\nexport interface SyncRenderOptions {\n /** Pre-fetched media detail, keyed by media_id (empty resolves nothing). */\n mediaDetails?: Map<string, MediaDetail>;\n /** Media ids known to be deleted. The console marks preview values instead. */\n missingMedia?: Set<string>;\n /** Pre-fetched form definitions, keyed by form key (empty resolves nothing). */\n /** Wrap nodes with `data-plenary-id` markers (for the preview bridge). */\n annotate?: boolean;\n /** Per-node remount epochs — see RenderOptions.remountKeys. */\n remountKeys?: ReadonlyMap<string, number>;\n}\n\n/**\n * Render a document synchronously from already-resolved inputs. Used by the\n * preview bridge (client-side, no async media pass). Media props that carry a\n * URL still render; bare `media_id`s without a supplied detail stay unresolved.\n *\n * Editor styles ride along as one compiled `<style data-plenary-styles>`\n * element ahead of the tree — real media queries against the registry theme's\n * cutoffs on every render path, preview and published alike (the artboard's\n * locked device widths make them evaluate correctly in the editor). The\n * compiler's sanitizer guarantees the CSS text cannot escape the style element,\n * which is what makes the innerHTML write safe.\n */\nexport function renderDocumentSync(\n document: PlenaryDocument,\n opts: SyncRenderOptions = {},\n): ReactNode {\n const { css, styledIds } = compileStyles(\n document,\n resolveCutoffs(registry.getTheme()),\n );\n const renderOpts: RenderOptions = {\n mediaDetails: opts.mediaDetails ?? new Map(),\n missingMedia: opts.missingMedia ?? new Set(),\n annotate: opts.annotate,\n styledIds,\n remountKeys: opts.remountKeys,\n };\n return (\n <Suspense>\n {css ? (\n <style\n data-plenary-styles=\"\"\n dangerouslySetInnerHTML={{ __html: css }}\n />\n ) : null}\n {document.root.map((node) => renderNode(node, renderOpts))}\n </Suspense>\n );\n}\n\n/**\n * Render a whole document to a React tree. Resolves media first (one batched\n * async pass), then walks the tree synchronously. Wrapped in Suspense so lazy\n * (`React.lazy` / `next/dynamic`) custom components are SSR-safe.\n */\nexport async function renderDocument(\n document: PlenaryDocument,\n ctx: RenderContext = {},\n): Promise<ReactNode> {\n const media = await resolveMediaDetails(document, ctx.client);\n return renderDocumentSync(document, {\n mediaDetails: media.details,\n missingMedia: media.missing,\n });\n}\n\nexport interface PlenaryPageProps {\n document: PlenaryDocument;\n /** Client used to resolve media props (optional). */\n client?: PlenaryClient;\n}\n\n/** Renders a Page's `dom` tree. An async server component (SSR-friendly). */\nexport async function PlenaryPage({\n document,\n client,\n}: PlenaryPageProps): Promise<ReactNode> {\n return renderDocument(document, { client });\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/render.tsx"],"sourcesContent":["import { createElement, Fragment, Suspense, type ReactNode } from \"react\";\nimport {\n PlenaryNotFoundError,\n type PlenaryClient,\n type MediaDetail,\n} from \"@plenary/client\";\nimport {\n STYLE_PROP_FOR_BREAKPOINT,\n compileNodeCss,\n readNodeStyles,\n resolveCutoffs,\n type BreakpointCutoffs,\n type ComponentRegistration,\n type PlenaryDocument,\n type PlenaryNode,\n} from \"@plenary/core\";\nimport { registry } from \"./registry.js\";\nimport { Placeholder } from \"./components/Placeholder.js\";\nimport { toResolvedMedia, type MediaValue } from \"./media.js\";\nimport type { RenderContext } from \"./types.js\";\n\nconst MEDIA_PROP_TYPES = new Set([\"image\", \"file\"]);\n\n/**\n * Props with every media value — a media-typed prop, or a media-typed sub-field\n * of a `list` prop's items (one level, as a list of groups goes) — replaced by\n * `fn`'s result. Returns the props unchanged when there is nothing to replace.\n */\nfunction mapMediaProps(\n reg: ComponentRegistration,\n props: Record<string, unknown>,\n fn: (val: MediaValue) => unknown,\n): Record<string, unknown> {\n let out = props;\n const set = (key: string, value: unknown) => {\n if (out === props) out = { ...props };\n out[key] = value;\n };\n for (const def of reg.props ?? []) {\n const val = props[def.key];\n if (MEDIA_PROP_TYPES.has(def.type)) {\n if (val) set(def.key, fn(val as MediaValue));\n continue;\n }\n const mediaFields = (def.fields ?? []).filter((f) => MEDIA_PROP_TYPES.has(f.type));\n if (def.type !== \"list\" || mediaFields.length === 0 || !Array.isArray(val)) continue;\n set(\n def.key,\n val.map((item) => {\n if (!item || typeof item !== \"object\") return item;\n const next: Record<string, unknown> = { ...item };\n for (const f of mediaFields) {\n if (next[f.key]) next[f.key] = fn(next[f.key] as MediaValue);\n }\n return next;\n }),\n );\n }\n return out;\n}\n\nfunction walkNodes(\n nodes: PlenaryNode[],\n visit: (n: PlenaryNode) => void,\n): void {\n for (const node of nodes) {\n visit(node);\n if (node.slots) {\n for (const children of Object.values(node.slots))\n walkNodes(children, visit);\n }\n }\n}\n\n/**\n * Batch-fetch the detail for every bare `media_id` referenced by a media-typed\n * prop, so nodes can then be rendered synchronously. Values already carrying a\n * URL, and all fetches, are skipped when there is no client.\n *\n * A 404 is reported separately from a failed fetch: it means the asset was\n * deleted while entries still point at it, and such a value must not fall back\n * to the URLs it carries (see `toResolvedMedia`).\n */\nasync function resolveMediaDetails(\n document: PlenaryDocument,\n client: PlenaryClient | undefined,\n): Promise<{ details: Map<string, MediaDetail>; missing: Set<string> }> {\n const details = new Map<string, MediaDetail>();\n const missing = new Set<string>();\n if (!client) return { details, missing };\n\n const ids = new Set<string>();\n walkNodes(document.root, (node) => {\n const reg = registry.get(node.component)?.registration;\n if (!reg) return;\n mapMediaProps(reg, node.props, (val) => {\n if (val.media_id && !val.url) ids.add(val.media_id);\n return val;\n });\n });\n\n await Promise.all(\n [...ids].map(async (id) => {\n try {\n details.set(id, await client.getMedia(id));\n } catch (err) {\n // Anything else degrades to an unresolved value (e.g. a network blip).\n if (err instanceof PlenaryNotFoundError) missing.add(id);\n }\n }),\n );\n return { details, missing };\n}\n\n/** Node props with media-typed values replaced by their resolved shape. */\nfunction effectiveProps(\n node: PlenaryNode,\n reg: ComponentRegistration,\n mediaDetails: Map<string, MediaDetail>,\n missingMedia: Set<string>,\n): Record<string, unknown> {\n const props = mapMediaProps(reg, { ...node.props }, (val) =>\n toResolvedMedia(\n val,\n val.media_id ? mediaDetails.get(val.media_id) : undefined,\n val.media_id ? missingMedia.has(val.media_id) : false,\n ),\n );\n // Editor styles render through the compiled per-node stylesheet, never as\n // props: an inline base style would beat the sheet's media overrides, and\n // components no longer need to do anything for styling to work.\n for (const key of Object.values(STYLE_PROP_FOR_BREAKPOINT)) delete props[key];\n return props;\n}\n\n/**\n * Compile every node's style layers into one stylesheet, and remember which\n * nodes contributed — those are the nodes the published render must wrap so\n * the sheet's `[data-plenary-id] > *` selectors have something to target.\n */\nfunction compileStyles(\n document: PlenaryDocument,\n cutoffs: BreakpointCutoffs,\n): { css: string; styledIds: Set<string> } {\n const parts: string[] = [];\n const styledIds = new Set<string>();\n walkNodes(document.root, (node) => {\n const css = compileNodeCss(node.id, readNodeStyles(node.props), cutoffs);\n if (css) {\n parts.push(css);\n styledIds.add(node.id);\n }\n });\n return { css: parts.join(\"\\n\"), styledIds };\n}\n\ninterface RenderOptions {\n mediaDetails: Map<string, MediaDetail>;\n /** Media ids whose asset was deleted — their values render as missing. */\n missingMedia: Set<string>;\n /** Wrap each node with a `data-plenary-id` marker (preview-bridge mode). */\n annotate?: boolean;\n /** Nodes with compiled styles — wrapped even outside annotate mode. */\n styledIds: Set<string>;\n /**\n * Per-node remount epochs (preview-bridge mode). A bumped epoch changes the\n * node's React key, forcing its subtree to be rebuilt from the document —\n * used after in-place editing, which mutates DOM React never tracked.\n */\n remountKeys?: ReadonlyMap<string, number>;\n}\n\n/** A node's React key: its id, salted by its remount epoch when one exists. */\nfunction nodeKey(id: string, opts: RenderOptions): string {\n const epoch = opts.remountKeys?.get(id);\n return epoch ? `${id}#${epoch}` : id;\n}\n\n/**\n * Wrap a node's element in a layout-neutral marker carrying its id. Every node\n * gets one in annotate mode (the preview bridge maps clicked DOM elements back\n * to nodes through it); on published renders only styled nodes do, since the\n * marker is what the compiled stylesheet's selectors target.\n */\nfunction annotate(\n id: string,\n element: ReactNode,\n opts: RenderOptions,\n empty?: string,\n): ReactNode {\n if (!opts.annotate && !opts.styledIds.has(id)) return element;\n return (\n <div\n key={nodeKey(id, opts)}\n data-plenary-id={id}\n data-plenary-empty={empty}\n style={{ display: \"contents\" }}\n >\n {element}\n </div>\n );\n}\n\nfunction renderNode(node: PlenaryNode, opts: RenderOptions): ReactNode {\n const key = nodeKey(node.id, opts);\n const found = registry.get(node.component);\n if (!found) {\n return annotate(\n node.id,\n <Placeholder key={key} component={node.component} />,\n opts,\n );\n }\n\n const { component: Component, registration } = found;\n const props = effectiveProps(\n node,\n registration,\n opts.mediaDetails,\n opts.missingMedia,\n );\n // Annotated renders are editor previews; the flag lets components show\n // editor-only affordances (e.g. the Image empty-state placeholder) that a\n // published render must not.\n if (opts.annotate) props.editor = true;\n\n // The `children` slot becomes React children; every other slot is delivered\n // via a `slots` prop record of already-rendered nodes.\n let children: ReactNode;\n let slots: Record<string, ReactNode> | undefined;\n if (node.slots) {\n for (const [name, childNodes] of Object.entries(node.slots)) {\n const rendered = (\n <Fragment>\n {childNodes.map((child) => renderNode(child, opts))}\n </Fragment>\n );\n if (name === \"children\") children = rendered;\n else (slots ??= {})[name] = rendered;\n }\n }\n if (slots) props.slots = slots;\n\n return annotate(\n node.id,\n createElement(Component, { ...props, key }, children),\n opts,\n opts.annotate && isEmptyContainer(node) ? node.component : undefined,\n );\n}\n\n/**\n * A built-in layout block with nothing in it. Editor previews mark these\n * (`data-plenary-empty`) so the bridge can hatch them and hold them open to a\n * clickable size; custom components are left alone.\n */\nfunction isEmptyContainer(node: PlenaryNode): boolean {\n if (node.component !== \"box\" && node.component !== \"section\") return false;\n return !Object.values(node.slots ?? {}).some((children) => children.length > 0);\n}\n\nexport interface SyncRenderOptions {\n /** Pre-fetched media detail, keyed by media_id (empty resolves nothing). */\n mediaDetails?: Map<string, MediaDetail>;\n /** Media ids known to be deleted. The console marks preview values instead. */\n missingMedia?: Set<string>;\n /** Pre-fetched form definitions, keyed by form key (empty resolves nothing). */\n /** Wrap nodes with `data-plenary-id` markers (for the preview bridge). */\n annotate?: boolean;\n /** Per-node remount epochs — see RenderOptions.remountKeys. */\n remountKeys?: ReadonlyMap<string, number>;\n}\n\n/**\n * Render a document synchronously from already-resolved inputs. Used by the\n * preview bridge (client-side, no async media pass). Media props that carry a\n * URL still render; bare `media_id`s without a supplied detail stay unresolved.\n *\n * Editor styles ride along as one compiled `<style data-plenary-styles>`\n * element ahead of the tree — real media queries against the registry theme's\n * cutoffs on every render path, preview and published alike (the artboard's\n * locked device widths make them evaluate correctly in the editor). The\n * compiler's sanitizer guarantees the CSS text cannot escape the style element,\n * which is what makes the innerHTML write safe.\n */\nexport function renderDocumentSync(\n document: PlenaryDocument,\n opts: SyncRenderOptions = {},\n): ReactNode {\n const { css, styledIds } = compileStyles(\n document,\n resolveCutoffs(registry.getTheme()),\n );\n const renderOpts: RenderOptions = {\n mediaDetails: opts.mediaDetails ?? new Map(),\n missingMedia: opts.missingMedia ?? new Set(),\n annotate: opts.annotate,\n styledIds,\n remountKeys: opts.remountKeys,\n };\n return (\n <Suspense>\n {css ? (\n <style\n data-plenary-styles=\"\"\n dangerouslySetInnerHTML={{ __html: css }}\n />\n ) : null}\n {document.root.map((node) => renderNode(node, renderOpts))}\n </Suspense>\n );\n}\n\n/**\n * Render a whole document to a React tree. Resolves media first (one batched\n * async pass), then walks the tree synchronously. Wrapped in Suspense so lazy\n * (`React.lazy` / `next/dynamic`) custom components are SSR-safe.\n */\nexport async function renderDocument(\n document: PlenaryDocument,\n ctx: RenderContext = {},\n): Promise<ReactNode> {\n const media = await resolveMediaDetails(document, ctx.client);\n return renderDocumentSync(document, {\n mediaDetails: media.details,\n missingMedia: media.missing,\n });\n}\n\nexport interface PlenaryPageProps {\n document: PlenaryDocument;\n /** Client used to resolve media props (optional). */\n client?: PlenaryClient;\n}\n\n/** Renders a Page's `dom` tree. An async server component (SSR-friendly). */\nexport async function PlenaryPage({\n document,\n client,\n}: PlenaryPageProps): Promise<ReactNode> {\n return renderDocument(document, { client });\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plenary/react",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Plenary React renderer, component registry, built-in components, and preview bridge.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -20,8 +20,8 @@
20
20
  "dist"
21
21
  ],
22
22
  "dependencies": {
23
- "@plenary/client": "1.0.1",
24
- "@plenary/core": "1.0.1"
23
+ "@plenary/client": "1.1.0",
24
+ "@plenary/core": "1.1.0"
25
25
  },
26
26
  "peerDependencies": {
27
27
  "react": "^18 || ^19",