@plenary/react 0.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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/dist/bridge.d.ts +34 -0
  3. package/dist/bridge.js +452 -0
  4. package/dist/bridge.js.map +1 -0
  5. package/dist/components/Box.d.ts +14 -0
  6. package/dist/components/Box.js +8 -0
  7. package/dist/components/Box.js.map +1 -0
  8. package/dist/components/Form.d.ts +36 -0
  9. package/dist/components/Form.js +280 -0
  10. package/dist/components/Form.js.map +1 -0
  11. package/dist/components/Image.d.ts +25 -0
  12. package/dist/components/Image.js +116 -0
  13. package/dist/components/Image.js.map +1 -0
  14. package/dist/components/Placeholder.d.ts +12 -0
  15. package/dist/components/Placeholder.js +24 -0
  16. package/dist/components/Placeholder.js.map +1 -0
  17. package/dist/components/Section.d.ts +20 -0
  18. package/dist/components/Section.js +15 -0
  19. package/dist/components/Section.js.map +1 -0
  20. package/dist/components/Text.d.ts +15 -0
  21. package/dist/components/Text.js +8 -0
  22. package/dist/components/Text.js.map +1 -0
  23. package/dist/components/Video.d.ts +48 -0
  24. package/dist/components/Video.js +131 -0
  25. package/dist/components/Video.js.map +1 -0
  26. package/dist/context.d.ts +26 -0
  27. package/dist/context.js +20 -0
  28. package/dist/context.js.map +1 -0
  29. package/dist/form.d.ts +25 -0
  30. package/dist/form.js +11 -0
  31. package/dist/form.js.map +1 -0
  32. package/dist/index.d.ts +16 -0
  33. package/dist/index.js +55 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/media.d.ts +60 -0
  36. package/dist/media.js +74 -0
  37. package/dist/media.js.map +1 -0
  38. package/dist/registry.d.ts +45 -0
  39. package/dist/registry.js +232 -0
  40. package/dist/registry.js.map +1 -0
  41. package/dist/render.d.ts +46 -0
  42. package/dist/render.js +179 -0
  43. package/dist/render.js.map +1 -0
  44. package/dist/types.d.ts +24 -0
  45. package/dist/types.js +1 -0
  46. package/dist/types.js.map +1 -0
  47. package/package.json +46 -0
@@ -0,0 +1,232 @@
1
+ import { Box } from "./components/Box.js";
2
+ import { Section } from "./components/Section.js";
3
+ import { Text } from "./components/Text.js";
4
+ import { Image } from "./components/Image.js";
5
+ import { Video } from "./components/Video.js";
6
+ import { Form } from "./components/Form.js";
7
+ const BUILTINS = [
8
+ {
9
+ component: Box,
10
+ registration: {
11
+ key: "box",
12
+ label: "Box",
13
+ category: "Layout",
14
+ props: [{ key: "className", type: "text" }],
15
+ slots: [{ name: "children", label: "Content" }],
16
+ styles: ["layout", "size", "background", "css"]
17
+ }
18
+ },
19
+ {
20
+ component: Section,
21
+ registration: {
22
+ key: "section",
23
+ label: "Section",
24
+ category: "Layout",
25
+ // Column slots ("0"…"n-1") are dynamic, driven by `columns`, and managed
26
+ // by the built-in — so they are not declared statically here.
27
+ props: [
28
+ { key: "columns", type: "number", defaultValue: 1 },
29
+ { key: "gap", type: "number", defaultValue: 16 },
30
+ { key: "className", type: "text" }
31
+ ],
32
+ styles: ["size", "background", "css"]
33
+ }
34
+ },
35
+ {
36
+ component: Text,
37
+ registration: {
38
+ key: "text",
39
+ label: "Text",
40
+ category: "Content",
41
+ props: [
42
+ { key: "content", type: "text" },
43
+ {
44
+ key: "as",
45
+ type: "select",
46
+ defaultValue: "p",
47
+ // Options use the server's select field-settings shape ({value,label})
48
+ // so the console's select input can drive them directly.
49
+ settings: {
50
+ options: [
51
+ { value: "p", label: "Paragraph" },
52
+ { value: "h1", label: "Heading 1" },
53
+ { value: "h2", label: "Heading 2" },
54
+ { value: "h3", label: "Heading 3" },
55
+ { value: "h4", label: "Heading 4" },
56
+ { value: "h5", label: "Heading 5" },
57
+ { value: "h6", label: "Heading 6" },
58
+ { value: "span", label: "Span" },
59
+ { value: "code", label: "Code" }
60
+ ]
61
+ }
62
+ }
63
+ ],
64
+ styles: ["size", "typography", "css"]
65
+ }
66
+ },
67
+ {
68
+ component: Image,
69
+ registration: {
70
+ key: "image",
71
+ label: "Image",
72
+ category: "Content",
73
+ props: [
74
+ // Alt is a block prop (below), so the media picker shows only the
75
+ // asset itself — not its own alt/caption sub-fields.
76
+ { key: "media", type: "image", ui: { show_alt_caption: false } },
77
+ { key: "alt", type: "text", ui: { label: "Alt Text" } },
78
+ // A one-off variant spec for this placement. The server enqueues the
79
+ // matching variant on save and hydrates `media.variant_url` on read;
80
+ // with no dimensions set, the original renders. Labels say "output"
81
+ // (pixels served) to keep these apart from the Styles tab's layout Size.
82
+ { key: "width", type: "number", ui: { label: "Max Width (px)" } },
83
+ { key: "height", type: "number", ui: { label: "Max Height (px)" } },
84
+ { key: "quality", type: "number", ui: { label: "Quality (%)" } },
85
+ { key: "crop", type: "boolean", ui: { label: "Crop to fill" } }
86
+ ],
87
+ styles: ["size", "css"]
88
+ }
89
+ },
90
+ {
91
+ component: Video,
92
+ registration: {
93
+ key: "video",
94
+ label: "Video",
95
+ category: "Content",
96
+ props: [
97
+ // The source file is served as uploaded — no transcoding — so the
98
+ // picker pins to the library's video type. Alt/caption sub-fields are
99
+ // hidden like the Image block's: the player has no use for them.
100
+ { key: "media", type: "file", ui: { label: "Video", media_type: "video", show_alt_caption: false } },
101
+ { key: "poster", type: "image", ui: { label: "Poster Image", show_alt_caption: false } },
102
+ { key: "controls", type: "boolean", defaultValue: true },
103
+ { key: "autoplay", type: "boolean", ui: { label: "Autoplay (browsers require Muted)" } },
104
+ { key: "loop", type: "boolean" },
105
+ { key: "muted", type: "boolean" },
106
+ { key: "playsinline", type: "boolean", defaultValue: true, ui: { label: "Plays Inline" } },
107
+ {
108
+ key: "preload",
109
+ type: "select",
110
+ defaultValue: "metadata",
111
+ settings: {
112
+ options: [
113
+ { value: "none", label: "None" },
114
+ { value: "metadata", label: "Metadata" },
115
+ { value: "auto", label: "Auto" }
116
+ ]
117
+ }
118
+ }
119
+ ],
120
+ styles: ["size", "css"]
121
+ }
122
+ },
123
+ {
124
+ component: Form,
125
+ registration: {
126
+ key: "form",
127
+ label: "Form",
128
+ category: "Content",
129
+ props: [
130
+ { key: "form", type: "form", ui: { label: "Form" } },
131
+ { key: "showTitle", type: "boolean", ui: { label: "Show Form Title" } },
132
+ { key: "submitLabel", type: "text", defaultValue: "Submit", ui: { label: "Submit Button Label" } },
133
+ {
134
+ key: "onSuccess",
135
+ type: "select",
136
+ // Required: a non-required select offers an empty "None" choice,
137
+ // which is meaningless here — the block always does one of the two.
138
+ required: true,
139
+ defaultValue: "message",
140
+ ui: { label: "After Submit" },
141
+ settings: {
142
+ options: [
143
+ { value: "message", label: "Show a success message" },
144
+ { value: "redirect", label: "Redirect to a page" }
145
+ ]
146
+ }
147
+ },
148
+ {
149
+ key: "successMessage",
150
+ type: "text",
151
+ defaultValue: "Thanks! Your submission has been received.",
152
+ ui: { label: "Success Message", control: "textarea" },
153
+ visibleWhen: { prop: "onSuccess", equals: "message" }
154
+ },
155
+ {
156
+ key: "redirectUrl",
157
+ type: "url",
158
+ ui: { label: "Redirect URL" },
159
+ visibleWhen: { prop: "onSuccess", equals: "redirect" }
160
+ }
161
+ ],
162
+ styles: ["layout", "size", "typography", "css"]
163
+ }
164
+ }
165
+ ];
166
+ class Registry {
167
+ components = /* @__PURE__ */ new Map();
168
+ theme;
169
+ constructor() {
170
+ for (const b of BUILTINS) this.register(b.component, b.registration);
171
+ }
172
+ /** Register a component (built-in or custom) under its registration key. */
173
+ register(component, registration) {
174
+ this.components.set(registration.key, { component, registration });
175
+ return this;
176
+ }
177
+ /**
178
+ * Declare the site's design tokens. Each token mirrors a CSS custom property
179
+ * the site's stylesheet defines (`--<name>`); the editor stores references
180
+ * (`var(--<name>)`), so the tokens must actually exist in the site's CSS for
181
+ * styled pages to render.
182
+ */
183
+ registerTheme(theme) {
184
+ this.theme = theme;
185
+ return this;
186
+ }
187
+ getTheme() {
188
+ return this.theme;
189
+ }
190
+ get(key) {
191
+ return this.components.get(key);
192
+ }
193
+ all() {
194
+ return [...this.components.values()];
195
+ }
196
+ /** The full component manifest announced to the console over the bridge. */
197
+ manifest() {
198
+ return this.all().map(({ registration: r }) => ({
199
+ name: r.key,
200
+ label: r.label,
201
+ icon: r.icon,
202
+ props: r.props ?? [],
203
+ slots: r.slots ?? [],
204
+ category: r.category,
205
+ styles: r.styles
206
+ }));
207
+ }
208
+ }
209
+ const registry = new Registry();
210
+ function register(component, registration) {
211
+ return registry.register(component, registration);
212
+ }
213
+ function registerTheme(theme) {
214
+ return registry.registerTheme(theme);
215
+ }
216
+ const Plenary = { register, registerTheme };
217
+ function getManifest() {
218
+ return registry.manifest();
219
+ }
220
+ function getTheme() {
221
+ return registry.getTheme();
222
+ }
223
+ export {
224
+ Plenary,
225
+ Registry,
226
+ getManifest,
227
+ getTheme,
228
+ register,
229
+ registerTheme,
230
+ registry
231
+ };
232
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/registry.ts"],"sourcesContent":["import type {\n ComponentRegistration,\n ManifestComponent,\n ThemeRegistration,\n} from \"@plenary/core\";\nimport type { PlenaryComponent, RegisteredComponent } from \"./types.js\";\nimport { Box } from \"./components/Box.js\";\nimport { Section } from \"./components/Section.js\";\nimport { Text } from \"./components/Text.js\";\nimport { Image } from \"./components/Image.js\";\nimport { Video } from \"./components/Video.js\";\nimport { Form } from \"./components/Form.js\";\n\n/** The built-in primitives, pre-registered so they behave like any custom component. */\nconst BUILTINS: { component: PlenaryComponent; registration: ComponentRegistration }[] = [\n {\n component: Box,\n registration: {\n key: \"box\",\n label: \"Box\",\n category: \"Layout\",\n props: [{ key: \"className\", type: \"text\" }],\n slots: [{ name: \"children\", label: \"Content\" }],\n styles: [\"layout\", \"size\", \"background\", \"css\"],\n },\n },\n {\n component: Section,\n registration: {\n key: \"section\",\n label: \"Section\",\n category: \"Layout\",\n // Column slots (\"0\"…\"n-1\") are dynamic, driven by `columns`, and managed\n // by the built-in — so they are not declared statically here.\n props: [\n { key: \"columns\", type: \"number\", defaultValue: 1 },\n { key: \"gap\", type: \"number\", defaultValue: 16 },\n { key: \"className\", type: \"text\" },\n ],\n styles: [\"size\", \"background\", \"css\"],\n },\n },\n {\n component: Text,\n registration: {\n key: \"text\",\n label: \"Text\",\n category: \"Content\",\n props: [\n { key: \"content\", type: \"text\" },\n {\n key: \"as\",\n type: \"select\",\n defaultValue: \"p\",\n // Options use the server's select field-settings shape ({value,label})\n // so the console's select input can drive them directly.\n settings: {\n options: [\n { value: \"p\", label: \"Paragraph\" },\n { value: \"h1\", label: \"Heading 1\" },\n { value: \"h2\", label: \"Heading 2\" },\n { value: \"h3\", label: \"Heading 3\" },\n { value: \"h4\", label: \"Heading 4\" },\n { value: \"h5\", label: \"Heading 5\" },\n { value: \"h6\", label: \"Heading 6\" },\n { value: \"span\", label: \"Span\" },\n { value: \"code\", label: \"Code\" },\n ],\n },\n },\n ],\n styles: [\"size\", \"typography\", \"css\"],\n },\n },\n {\n component: Image,\n registration: {\n key: \"image\",\n label: \"Image\",\n category: \"Content\",\n props: [\n // Alt is a block prop (below), so the media picker shows only the\n // asset itself — not its own alt/caption sub-fields.\n { key: \"media\", type: \"image\", ui: { show_alt_caption: false } },\n { key: \"alt\", type: \"text\", ui: { label: \"Alt Text\" } },\n // A one-off variant spec for this placement. The server enqueues the\n // matching variant on save and hydrates `media.variant_url` on read;\n // with no dimensions set, the original renders. Labels say \"output\"\n // (pixels served) to keep these apart from the Styles tab's layout Size.\n { key: \"width\", type: \"number\", ui: { label: \"Max Width (px)\" } },\n { key: \"height\", type: \"number\", ui: { label: \"Max Height (px)\" } },\n { key: \"quality\", type: \"number\", ui: { label: \"Quality (%)\" } },\n { key: \"crop\", type: \"boolean\", ui: { label: \"Crop to fill\" } },\n ],\n styles: [\"size\", \"css\"],\n },\n },\n {\n component: Video,\n registration: {\n key: \"video\",\n label: \"Video\",\n category: \"Content\",\n props: [\n // The source file is served as uploaded — no transcoding — so the\n // picker pins to the library's video type. Alt/caption sub-fields are\n // hidden like the Image block's: the player has no use for them.\n { key: \"media\", type: \"file\", ui: { label: \"Video\", media_type: \"video\", show_alt_caption: false } },\n { key: \"poster\", type: \"image\", ui: { label: \"Poster Image\", show_alt_caption: false } },\n { key: \"controls\", type: \"boolean\", defaultValue: true },\n { key: \"autoplay\", type: \"boolean\", ui: { label: \"Autoplay (browsers require Muted)\" } },\n { key: \"loop\", type: \"boolean\" },\n { key: \"muted\", type: \"boolean\" },\n { key: \"playsinline\", type: \"boolean\", defaultValue: true, ui: { label: \"Plays Inline\" } },\n {\n key: \"preload\",\n type: \"select\",\n defaultValue: \"metadata\",\n settings: {\n options: [\n { value: \"none\", label: \"None\" },\n { value: \"metadata\", label: \"Metadata\" },\n { value: \"auto\", label: \"Auto\" },\n ],\n },\n },\n ],\n styles: [\"size\", \"css\"],\n },\n },\n {\n component: Form,\n registration: {\n key: \"form\",\n label: \"Form\",\n category: \"Content\",\n props: [\n { key: \"form\", type: \"form\", ui: { label: \"Form\" } },\n { key: \"showTitle\", type: \"boolean\", ui: { label: \"Show Form Title\" } },\n { key: \"submitLabel\", type: \"text\", defaultValue: \"Submit\", ui: { label: \"Submit Button Label\" } },\n {\n key: \"onSuccess\",\n type: \"select\",\n // Required: a non-required select offers an empty \"None\" choice,\n // which is meaningless here — the block always does one of the two.\n required: true,\n defaultValue: \"message\",\n ui: { label: \"After Submit\" },\n settings: {\n options: [\n { value: \"message\", label: \"Show a success message\" },\n { value: \"redirect\", label: \"Redirect to a page\" },\n ],\n },\n },\n {\n key: \"successMessage\",\n type: \"text\",\n defaultValue: \"Thanks! Your submission has been received.\",\n ui: { label: \"Success Message\", control: \"textarea\" },\n visibleWhen: { prop: \"onSuccess\", equals: \"message\" },\n },\n {\n key: \"redirectUrl\",\n type: \"url\",\n ui: { label: \"Redirect URL\" },\n visibleWhen: { prop: \"onSuccess\", equals: \"redirect\" },\n },\n ],\n styles: [\"layout\", \"size\", \"typography\", \"css\"],\n },\n },\n];\n\n/**\n * The module-singleton component registry. Because it is a shared module, both\n * the render path (SSR + client) and the preview bridge see the same\n * registrations — the site's registry file is imported wherever either is used.\n */\nexport class Registry {\n private components = new Map<string, RegisteredComponent>();\n private theme: ThemeRegistration | undefined;\n\n constructor() {\n for (const b of BUILTINS) this.register(b.component, b.registration);\n }\n\n /** Register a component (built-in or custom) under its registration key. */\n register(component: PlenaryComponent, registration: ComponentRegistration): this {\n this.components.set(registration.key, { component, registration });\n return this;\n }\n\n /**\n * Declare the site's design tokens. Each token mirrors a CSS custom property\n * the site's stylesheet defines (`--<name>`); the editor stores references\n * (`var(--<name>)`), so the tokens must actually exist in the site's CSS for\n * styled pages to render.\n */\n registerTheme(theme: ThemeRegistration): this {\n this.theme = theme;\n return this;\n }\n\n getTheme(): ThemeRegistration | undefined {\n return this.theme;\n }\n\n get(key: string): RegisteredComponent | undefined {\n return this.components.get(key);\n }\n\n all(): RegisteredComponent[] {\n return [...this.components.values()];\n }\n\n /** The full component manifest announced to the console over the bridge. */\n manifest(): ManifestComponent[] {\n return this.all().map(({ registration: r }) => ({\n name: r.key,\n label: r.label,\n icon: r.icon,\n props: r.props ?? [],\n slots: r.slots ?? [],\n category: r.category,\n styles: r.styles,\n }));\n }\n}\n\nexport const registry = new Registry();\n\n/** Register a component so the dom tree may reference it by key. */\nexport function register(component: PlenaryComponent, registration: ComponentRegistration): Registry {\n return registry.register(component, registration);\n}\n\n/** Declare the site's design tokens (see Registry.registerTheme). */\nexport function registerTheme(theme: ThemeRegistration): Registry {\n return registry.registerTheme(theme);\n}\n\n/** Namespace mirroring the design brief's `Plenary.register(...)` surface. */\nexport const Plenary = { register, registerTheme };\n\n/** The manifest of every registered component (built-ins + custom). */\nexport function getManifest(): ManifestComponent[] {\n return registry.manifest();\n}\n\n/** The site's registered design tokens, if any. */\nexport function getTheme(): ThemeRegistration | undefined {\n return registry.getTheme();\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,46 @@
1
+ import { ReactNode } from 'react';
2
+ import { PlenaryClient, MediaDetail, PublicFormDefinition } from '@plenary/client';
3
+ import { PlenaryDocument } from '@plenary/core';
4
+ import { RenderContext } from './types.js';
5
+
6
+ interface SyncRenderOptions {
7
+ /** Pre-fetched media detail, keyed by media_id (empty resolves nothing). */
8
+ mediaDetails?: Map<string, MediaDetail>;
9
+ /** Media ids known to be deleted. The console marks preview values instead. */
10
+ missingMedia?: Set<string>;
11
+ /** Pre-fetched form definitions, keyed by form key (empty resolves nothing). */
12
+ formDefinitions?: Map<string, PublicFormDefinition>;
13
+ /** Wrap nodes with `data-plenary-id` markers (for the preview bridge). */
14
+ annotate?: boolean;
15
+ /** Per-node remount epochs — see RenderOptions.remountKeys. */
16
+ remountKeys?: ReadonlyMap<string, number>;
17
+ }
18
+ /**
19
+ * Render a document synchronously from already-resolved inputs. Used by the
20
+ * preview bridge (client-side, no async media pass). Media props that carry a
21
+ * URL still render; bare `media_id`s without a supplied detail stay unresolved.
22
+ *
23
+ * Editor styles ride along as one compiled `<style data-plenary-styles>`
24
+ * element ahead of the tree — real media queries against the registry theme's
25
+ * cutoffs on every render path, preview and published alike (the artboard's
26
+ * locked device widths make them evaluate correctly in the editor). The
27
+ * compiler's sanitizer guarantees the CSS text cannot escape the style element,
28
+ * which is what makes the innerHTML write safe.
29
+ */
30
+ declare function renderDocumentSync(document: PlenaryDocument, opts?: SyncRenderOptions): ReactNode;
31
+ /**
32
+ * Render a whole document to a React tree. Resolves media and form definitions
33
+ * first (batched async passes), then walks the tree synchronously. Wrapped in
34
+ * Suspense so lazy (`React.lazy` / `next/dynamic`) custom components are
35
+ * SSR-safe.
36
+ */
37
+ declare function renderDocument(document: PlenaryDocument, ctx?: RenderContext): Promise<ReactNode>;
38
+ interface PlenaryPageProps {
39
+ document: PlenaryDocument;
40
+ /** Client used to resolve media and form props (optional). */
41
+ client?: PlenaryClient;
42
+ }
43
+ /** Renders a Page's `dom` tree. An async server component (SSR-friendly). */
44
+ declare function PlenaryPage({ document, client }: PlenaryPageProps): Promise<ReactNode>;
45
+
46
+ export { PlenaryPage, type PlenaryPageProps, type SyncRenderOptions, renderDocument, renderDocumentSync };
package/dist/render.js ADDED
@@ -0,0 +1,179 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { createElement, Fragment, Suspense } from "react";
3
+ import {
4
+ PlenaryNotFoundError
5
+ } from "@plenary/client";
6
+ import {
7
+ STYLE_PROP_FOR_BREAKPOINT,
8
+ compileNodeCss,
9
+ readNodeStyles,
10
+ resolveCutoffs
11
+ } from "@plenary/core";
12
+ import { registry } from "./registry.js";
13
+ import { Placeholder } from "./components/Placeholder.js";
14
+ import { toResolvedMedia } from "./media.js";
15
+ import { toResolvedForm } from "./form.js";
16
+ const MEDIA_PROP_TYPES = /* @__PURE__ */ new Set(["image", "file"]);
17
+ function mediaPropKeys(reg) {
18
+ return (reg.props ?? []).filter((p) => MEDIA_PROP_TYPES.has(p.type)).map((p) => p.key);
19
+ }
20
+ function formPropKeys(reg) {
21
+ return (reg.props ?? []).filter((p) => p.type === "form").map((p) => p.key);
22
+ }
23
+ function walkNodes(nodes, visit) {
24
+ for (const node of nodes) {
25
+ visit(node);
26
+ if (node.slots) {
27
+ for (const children of Object.values(node.slots)) walkNodes(children, visit);
28
+ }
29
+ }
30
+ }
31
+ async function resolveMediaDetails(document, client) {
32
+ const details = /* @__PURE__ */ new Map();
33
+ const missing = /* @__PURE__ */ new Set();
34
+ if (!client) return { details, missing };
35
+ const ids = /* @__PURE__ */ new Set();
36
+ walkNodes(document.root, (node) => {
37
+ const reg = registry.get(node.component)?.registration;
38
+ if (!reg) return;
39
+ for (const key of mediaPropKeys(reg)) {
40
+ const val = node.props[key];
41
+ if (val && val.media_id && !val.url) ids.add(val.media_id);
42
+ }
43
+ });
44
+ await Promise.all(
45
+ [...ids].map(async (id) => {
46
+ try {
47
+ details.set(id, await client.getMedia(id));
48
+ } catch (err) {
49
+ if (err instanceof PlenaryNotFoundError) missing.add(id);
50
+ }
51
+ })
52
+ );
53
+ return { details, missing };
54
+ }
55
+ async function resolveFormDefinitions(document, client) {
56
+ const map = /* @__PURE__ */ new Map();
57
+ if (!client) return map;
58
+ const keys = /* @__PURE__ */ new Set();
59
+ walkNodes(document.root, (node) => {
60
+ const reg = registry.get(node.component)?.registration;
61
+ if (!reg) return;
62
+ for (const key of formPropKeys(reg)) {
63
+ const val = node.props[key];
64
+ if (val && val.form_key && val.definition === void 0) keys.add(val.form_key);
65
+ }
66
+ });
67
+ await Promise.all(
68
+ [...keys].map(async (formKey) => {
69
+ try {
70
+ map.set(formKey, await client.getForm(formKey));
71
+ } catch {
72
+ }
73
+ })
74
+ );
75
+ return map;
76
+ }
77
+ function effectiveProps(node, reg, mediaDetails, missingMedia, formDefinitions) {
78
+ const props = { ...node.props };
79
+ for (const key of Object.values(STYLE_PROP_FOR_BREAKPOINT)) delete props[key];
80
+ for (const key of mediaPropKeys(reg)) {
81
+ const val = props[key];
82
+ if (val) {
83
+ props[key] = toResolvedMedia(
84
+ val,
85
+ val.media_id ? mediaDetails.get(val.media_id) : void 0,
86
+ val.media_id ? missingMedia.has(val.media_id) : false
87
+ );
88
+ }
89
+ }
90
+ for (const key of formPropKeys(reg)) {
91
+ const val = props[key];
92
+ if (val) {
93
+ props[key] = toResolvedForm(val, val.form_key ? formDefinitions.get(val.form_key) : void 0);
94
+ }
95
+ }
96
+ return props;
97
+ }
98
+ function compileStyles(document, cutoffs) {
99
+ const parts = [];
100
+ const styledIds = /* @__PURE__ */ new Set();
101
+ walkNodes(document.root, (node) => {
102
+ const css = compileNodeCss(node.id, readNodeStyles(node.props), cutoffs);
103
+ if (css) {
104
+ parts.push(css);
105
+ styledIds.add(node.id);
106
+ }
107
+ });
108
+ return { css: parts.join("\n"), styledIds };
109
+ }
110
+ function nodeKey(id, opts) {
111
+ const epoch = opts.remountKeys?.get(id);
112
+ return epoch ? `${id}#${epoch}` : id;
113
+ }
114
+ function annotate(id, element, opts) {
115
+ if (!opts.annotate && !opts.styledIds.has(id)) return element;
116
+ return /* @__PURE__ */ jsx("div", { "data-plenary-id": id, style: { display: "contents" }, children: element }, nodeKey(id, opts));
117
+ }
118
+ function renderNode(node, opts) {
119
+ const key = nodeKey(node.id, opts);
120
+ const found = registry.get(node.component);
121
+ if (!found) {
122
+ return annotate(node.id, /* @__PURE__ */ jsx(Placeholder, { component: node.component }, key), opts);
123
+ }
124
+ const { component: Component, registration } = found;
125
+ const props = effectiveProps(
126
+ node,
127
+ registration,
128
+ opts.mediaDetails,
129
+ opts.missingMedia,
130
+ opts.formDefinitions
131
+ );
132
+ if (opts.annotate) props.editor = true;
133
+ let children;
134
+ let slots;
135
+ if (node.slots) {
136
+ for (const [name, childNodes] of Object.entries(node.slots)) {
137
+ const rendered = /* @__PURE__ */ jsx(Fragment, { children: childNodes.map((child) => renderNode(child, opts)) });
138
+ if (name === "children") children = rendered;
139
+ else (slots ??= {})[name] = rendered;
140
+ }
141
+ }
142
+ if (slots) props.slots = slots;
143
+ return annotate(node.id, createElement(Component, { ...props, key }, children), opts);
144
+ }
145
+ function renderDocumentSync(document, opts = {}) {
146
+ const { css, styledIds } = compileStyles(document, resolveCutoffs(registry.getTheme()));
147
+ const renderOpts = {
148
+ mediaDetails: opts.mediaDetails ?? /* @__PURE__ */ new Map(),
149
+ missingMedia: opts.missingMedia ?? /* @__PURE__ */ new Set(),
150
+ formDefinitions: opts.formDefinitions ?? /* @__PURE__ */ new Map(),
151
+ annotate: opts.annotate,
152
+ styledIds,
153
+ remountKeys: opts.remountKeys
154
+ };
155
+ return /* @__PURE__ */ jsxs(Suspense, { children: [
156
+ css ? /* @__PURE__ */ jsx("style", { "data-plenary-styles": "", dangerouslySetInnerHTML: { __html: css } }) : null,
157
+ document.root.map((node) => renderNode(node, renderOpts))
158
+ ] });
159
+ }
160
+ async function renderDocument(document, ctx = {}) {
161
+ const [media, formDefinitions] = await Promise.all([
162
+ resolveMediaDetails(document, ctx.client),
163
+ resolveFormDefinitions(document, ctx.client)
164
+ ]);
165
+ return renderDocumentSync(document, {
166
+ mediaDetails: media.details,
167
+ missingMedia: media.missing,
168
+ formDefinitions
169
+ });
170
+ }
171
+ async function PlenaryPage({ document, client }) {
172
+ return renderDocument(document, { client });
173
+ }
174
+ export {
175
+ PlenaryPage,
176
+ renderDocument,
177
+ renderDocumentSync
178
+ };
179
+ //# sourceMappingURL=render.js.map
@@ -0,0 +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 type PublicFormDefinition,\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 { toResolvedForm, type FormValue } from \"./form.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 ?? []).filter((p) => MEDIA_PROP_TYPES.has(p.type)).map((p) => p.key);\n}\n\n/** Prop keys of a registration whose values are form pointers. */\nfunction formPropKeys(reg: ComponentRegistration): string[] {\n return (reg.props ?? []).filter((p) => p.type === \"form\").map((p) => p.key);\n}\n\nfunction walkNodes(nodes: PlenaryNode[], visit: (n: PlenaryNode) => void): void {\n for (const node of nodes) {\n visit(node);\n if (node.slots) {\n for (const children of Object.values(node.slots)) 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/**\n * Batch-fetch the render-safe definition for every form referenced by a\n * form-typed prop, so nodes can then be rendered synchronously. Values already\n * carrying a definition (the preview inlines them), and all fetches, are\n * skipped when there is no client.\n */\nasync function resolveFormDefinitions(\n document: PlenaryDocument,\n client: PlenaryClient | undefined,\n): Promise<Map<string, PublicFormDefinition>> {\n const map = new Map<string, PublicFormDefinition>();\n if (!client) return map;\n\n const keys = 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 formPropKeys(reg)) {\n const val = node.props[key] as FormValue | undefined;\n if (val && val.form_key && val.definition === undefined) keys.add(val.form_key);\n }\n });\n\n await Promise.all(\n [...keys].map(async (formKey) => {\n try {\n map.set(formKey, await client.getForm(formKey));\n } catch {\n // Degrade to an unresolved value (deleted/inactive form, wrong site).\n }\n }),\n );\n return map;\n}\n\n/** Node props with media- and form-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 formDefinitions: Map<string, PublicFormDefinition>,\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 for (const key of formPropKeys(reg)) {\n const val = props[key] as FormValue | undefined;\n if (val) {\n props[key] = toResolvedForm(val, val.form_key ? formDefinitions.get(val.form_key) : undefined);\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 formDefinitions: Map<string, PublicFormDefinition>;\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(id: string, element: ReactNode, opts: RenderOptions): ReactNode {\n if (!opts.annotate && !opts.styledIds.has(id)) return element;\n return (\n <div key={nodeKey(id, opts)} data-plenary-id={id} style={{ display: \"contents\" }}>\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(node.id, <Placeholder key={key} component={node.component} />, opts);\n }\n\n const { component: Component, registration } = found;\n const props = effectiveProps(\n node,\n registration,\n opts.mediaDetails,\n opts.missingMedia,\n opts.formDefinitions,\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>{childNodes.map((child) => renderNode(child, opts))}</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(node.id, createElement(Component, { ...props, key }, children), opts);\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 formDefinitions?: Map<string, PublicFormDefinition>;\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(document, resolveCutoffs(registry.getTheme()));\n const renderOpts: RenderOptions = {\n mediaDetails: opts.mediaDetails ?? new Map(),\n missingMedia: opts.missingMedia ?? new Set(),\n formDefinitions: opts.formDefinitions ?? new Map(),\n annotate: opts.annotate,\n styledIds,\n remountKeys: opts.remountKeys,\n };\n return (\n <Suspense>\n {css ? <style data-plenary-styles=\"\" dangerouslySetInnerHTML={{ __html: css }} /> : 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 and form definitions\n * first (batched async passes), then walks the tree synchronously. Wrapped in\n * Suspense so lazy (`React.lazy` / `next/dynamic`) custom components are\n * SSR-safe.\n */\nexport async function renderDocument(\n document: PlenaryDocument,\n ctx: RenderContext = {},\n): Promise<ReactNode> {\n const [media, formDefinitions] = await Promise.all([\n resolveMediaDetails(document, ctx.client),\n resolveFormDefinitions(document, ctx.client),\n ]);\n return renderDocumentSync(document, {\n mediaDetails: media.details,\n missingMedia: media.missing,\n formDefinitions,\n });\n}\n\nexport interface PlenaryPageProps {\n document: PlenaryDocument;\n /** Client used to resolve media and form props (optional). */\n client?: PlenaryClient;\n}\n\n/** Renders a Page's `dom` tree. An async server component (SSR-friendly). */\nexport async function PlenaryPage({ document, client }: PlenaryPageProps): Promise<ReactNode> {\n return renderDocument(document, { client });\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,24 @@
1
+ import { ComponentType, ReactNode } from 'react';
2
+ import { ComponentRegistration } from '@plenary/core';
3
+ import { PlenaryClient } from '@plenary/client';
4
+
5
+ /** A React component usable as a Plenary component (built-in or custom). */
6
+ type PlenaryComponent = ComponentType<any>;
7
+ /** A component together with the registration that describes it to the console. */
8
+ interface RegisteredComponent {
9
+ registration: ComponentRegistration;
10
+ component: PlenaryComponent;
11
+ }
12
+ /**
13
+ * Props every non-leaf component receives. Child slots other than the default
14
+ * `children` arrive as a record of already-rendered nodes.
15
+ */
16
+ interface SlotProps {
17
+ slots?: Record<string, ReactNode>;
18
+ }
19
+ /** Context threaded through a render: the client (for media) and the registry. */
20
+ interface RenderContext {
21
+ client?: PlenaryClient;
22
+ }
23
+
24
+ export type { PlenaryComponent, RegisteredComponent, RenderContext, SlotProps };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@plenary/react",
3
+ "version": "0.1.0",
4
+ "description": "Plenary React renderer, component registry, built-in components, and preview bridge.",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ },
14
+ "./bridge": {
15
+ "types": "./dist/bridge.d.ts",
16
+ "import": "./dist/bridge.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "dependencies": {
23
+ "@plenary/client": "0.1.0",
24
+ "@plenary/core": "0.1.0"
25
+ },
26
+ "peerDependencies": {
27
+ "react": "^18 || ^19",
28
+ "react-dom": "^18 || ^19"
29
+ },
30
+ "devDependencies": {
31
+ "@testing-library/dom": "^10.4.0",
32
+ "@testing-library/react": "^16.3.0",
33
+ "@types/react": "^19.2.17",
34
+ "@types/react-dom": "^19.2.3",
35
+ "jsdom": "^25.0.1",
36
+ "react": "^19.2.7",
37
+ "react-dom": "^19.2.7"
38
+ },
39
+ "license": "MIT",
40
+ "scripts": {
41
+ "build": "tsup",
42
+ "dev": "tsup --watch",
43
+ "test": "vitest run",
44
+ "typecheck": "tsc --noEmit"
45
+ }
46
+ }