blocks-renderer 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Constructive
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # blocks-renderer
2
+
3
+ React adapter for `blocks-schema` UI documents: a recursive
4
+ renderer with layerable widget registries, binding resolution, form state, and
5
+ a visible unknown-block fallback.
6
+
7
+ ## Usage
8
+
9
+ ```tsx
10
+ import { DocumentRenderer, composeRegistry } from 'blocks-renderer';
11
+
12
+ const registry = composeRegistry(
13
+ baseRegistry, // shadcn-style primitives
14
+ appRegistry, // app-specific blocks
15
+ documentOverrides, // per-document swaps
16
+ );
17
+
18
+ <DocumentRenderer
19
+ document={document}
20
+ registry={registry}
21
+ scope={{ row, user }}
22
+ initialValues={{ title: 'Draft' }}
23
+ onSubmit={(values) => save(values)}
24
+ onAction={(action, event) => runFlow(action)}
25
+ />;
26
+ ```
27
+
28
+ ## Concepts
29
+
30
+ - **Registry layering** — `composeRegistry(...layers)` merges
31
+ `type → component` maps left-to-right, later layers winning. Hosts customize
32
+ widgets (inputs, textareas, rich text, custom blocks) by layering, never by
33
+ forking the renderer.
34
+ - **Blocks** — every component receives `{ node, props, children }`, where
35
+ `props` has the node's `bindings` resolved against the current scope and
36
+ `children` are already rendered.
37
+ - **Bindings** — `{{ path.to.value }}` templates resolve against
38
+ `scope + values`; a lone placeholder yields the raw value, mixed text
39
+ interpolates.
40
+ - **Fields** — widget implementations call `useBlockField(name)` for
41
+ value/error wiring; constraints from the document (`required`, length,
42
+ range, `pattern`) validate on change and on submit.
43
+ - **Unknown blocks** — unregistered node types render `UnknownBlock`, a
44
+ visible gap instead of a crash, so documents can name blocks a host has not
45
+ installed.
package/dist/index.cjs ADDED
@@ -0,0 +1,214 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ BlockRenderer: () => BlockRenderer,
24
+ DocumentRenderer: () => DocumentRenderer,
25
+ RendererProvider: () => RendererProvider,
26
+ UnknownBlock: () => UnknownBlock,
27
+ composeRegistry: () => composeRegistry,
28
+ readPath: () => readPath,
29
+ registeredTypes: () => registeredTypes,
30
+ resolveBinding: () => resolveBinding,
31
+ resolveBlock: () => resolveBlock,
32
+ resolveNodeProps: () => resolveNodeProps,
33
+ useBlockField: () => useBlockField,
34
+ useRenderer: () => useRenderer
35
+ });
36
+ module.exports = __toCommonJS(index_exports);
37
+
38
+ // src/bindings.ts
39
+ var TEMPLATE = /\{\{\s*([^}\s]+)\s*\}\}/g;
40
+ function readPath(scope, path) {
41
+ let current = scope;
42
+ for (const segment of path.split(".")) {
43
+ if (current == null || typeof current !== "object") return void 0;
44
+ current = current[segment];
45
+ }
46
+ return current;
47
+ }
48
+ function resolveBinding(expression, scope) {
49
+ const single = expression.match(/^\{\{\s*([^}\s]+)\s*\}\}$/);
50
+ if (single) {
51
+ return readPath(scope, single[1]);
52
+ }
53
+ return expression.replace(TEMPLATE, (_match, path) => {
54
+ const value = readPath(scope, path);
55
+ return value == null ? "" : String(value);
56
+ });
57
+ }
58
+ function resolveNodeProps(node, scope) {
59
+ if (!node.bindings) return node.props ?? {};
60
+ const resolved = { ...node.props ?? {} };
61
+ for (const [prop, expression] of Object.entries(node.bindings)) {
62
+ resolved[prop] = resolveBinding(expression, scope);
63
+ }
64
+ return resolved;
65
+ }
66
+
67
+ // src/context.tsx
68
+ var import_react = require("react");
69
+ var import_jsx_runtime = require("react/jsx-runtime");
70
+ var RendererContext = (0, import_react.createContext)(null);
71
+ function RendererProvider({ children, value }) {
72
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RendererContext.Provider, { value, children });
73
+ }
74
+ function useRenderer() {
75
+ const context = (0, import_react.useContext)(RendererContext);
76
+ if (!context) {
77
+ throw new Error("useRenderer must be used within a RendererProvider (or a DocumentRenderer)");
78
+ }
79
+ return context;
80
+ }
81
+ function useBlockField(name) {
82
+ const { values, errors, setValue, setError, mode } = useRenderer();
83
+ if (!name) {
84
+ return { value: void 0, error: void 0, setValue: () => {
85
+ }, setError: () => {
86
+ }, mode };
87
+ }
88
+ return {
89
+ value: values[name],
90
+ error: errors[name],
91
+ setValue: (value) => setValue(name, value),
92
+ setError: (error) => setError(name, error),
93
+ mode
94
+ };
95
+ }
96
+
97
+ // src/registry.ts
98
+ function composeRegistry(...layers) {
99
+ const composed = {};
100
+ for (const layer of layers) {
101
+ if (!layer) continue;
102
+ Object.assign(composed, layer);
103
+ }
104
+ return composed;
105
+ }
106
+ function resolveBlock(registry, type) {
107
+ return registry[type];
108
+ }
109
+ function registeredTypes(registry) {
110
+ return Object.keys(registry).sort();
111
+ }
112
+
113
+ // src/renderer.tsx
114
+ var import_blocks_schema = require("blocks-schema");
115
+ var import_react2 = require("react");
116
+
117
+ // src/unknown-block.tsx
118
+ var import_jsx_runtime2 = require("react/jsx-runtime");
119
+ function UnknownBlock({ node }) {
120
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { "data-block-unknown": node.type, role: "note", children: [
121
+ "Unknown block: ",
122
+ node.type
123
+ ] });
124
+ }
125
+
126
+ // src/renderer.tsx
127
+ var import_jsx_runtime3 = require("react/jsx-runtime");
128
+ function BlockRenderer({ node }) {
129
+ const { registry, scope } = useRenderer();
130
+ const Component = registry[node.type];
131
+ if (!Component) {
132
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(UnknownBlock, { node });
133
+ }
134
+ const children = node.children?.length ? node.children.map((child) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(BlockRenderer, { node: child }, child.key)) : void 0;
135
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Component, { node, props: resolveNodeProps(node, scope), children });
136
+ }
137
+ function DocumentRenderer({
138
+ document,
139
+ registry,
140
+ initialValues,
141
+ scope: externalScope,
142
+ onSubmit,
143
+ onChange,
144
+ onAction,
145
+ mode = "preview",
146
+ className
147
+ }) {
148
+ const defaults = (0, import_react2.useMemo)(() => (0, import_blocks_schema.collectDefaultValues)(document.page), [document]);
149
+ const constraints = (0, import_react2.useMemo)(() => (0, import_blocks_schema.collectFieldConstraints)(document.page), [document]);
150
+ const [values, setValues] = (0, import_react2.useState)(() => ({ ...defaults, ...initialValues }));
151
+ const [errors, setErrors] = (0, import_react2.useState)({});
152
+ (0, import_react2.useEffect)(() => {
153
+ setValues((previous) => ({ ...defaults, ...initialValues, ...previous }));
154
+ }, [defaults, initialValues]);
155
+ const setError = (0, import_react2.useCallback)((name, error) => {
156
+ setErrors((previous) => {
157
+ if (error) return { ...previous, [name]: error };
158
+ if (!(name in previous)) return previous;
159
+ const next = { ...previous };
160
+ delete next[name];
161
+ return next;
162
+ });
163
+ }, []);
164
+ const setValue = (0, import_react2.useCallback)(
165
+ (name, value) => {
166
+ setValues((previous) => {
167
+ const next = { ...previous, [name]: value };
168
+ onChange?.(next);
169
+ return next;
170
+ });
171
+ const field = constraints[name];
172
+ if (field) {
173
+ setError(name, (0, import_blocks_schema.validateField)(value, field.constraints, field.required));
174
+ }
175
+ },
176
+ [onChange, constraints, setError]
177
+ );
178
+ const handleAction = (0, import_react2.useCallback)(
179
+ (action, event) => {
180
+ if (event === "submit" && onSubmit) {
181
+ const failures = {};
182
+ for (const name of (0, import_blocks_schema.collectFieldNames)(document.page)) {
183
+ const field = constraints[name];
184
+ if (!field) continue;
185
+ const error = (0, import_blocks_schema.validateField)(values[name], field.constraints, field.required);
186
+ if (error) failures[name] = error;
187
+ }
188
+ if (Object.keys(failures).length > 0) {
189
+ setErrors(failures);
190
+ return;
191
+ }
192
+ onSubmit(values);
193
+ }
194
+ onAction?.(action, event);
195
+ },
196
+ [onSubmit, onAction, values, document.page, constraints]
197
+ );
198
+ const contextValue = (0, import_react2.useMemo)(
199
+ () => ({
200
+ document,
201
+ registry,
202
+ mode,
203
+ values,
204
+ errors,
205
+ setValue,
206
+ setError,
207
+ scope: { ...externalScope, values, ...values },
208
+ onAction: handleAction
209
+ }),
210
+ [document, registry, mode, values, errors, setValue, setError, externalScope, handleAction]
211
+ );
212
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RendererProvider, { value: contextValue, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(BlockRenderer, { node: document.page }) }) });
213
+ }
214
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/bindings.ts","../src/context.tsx","../src/registry.ts","../src/renderer.tsx","../src/unknown-block.tsx"],"sourcesContent":["export { readPath, resolveBinding, resolveNodeProps } from './bindings';\nexport { RendererProvider, useBlockField, useRenderer } from './context';\nexport { composeRegistry, registeredTypes, resolveBlock } from './registry';\nexport { BlockRenderer, DocumentRenderer } from './renderer';\nexport type { DocumentRendererProps } from './renderer';\nexport { UnknownBlock } from './unknown-block';\nexport type { BlockComponent, BlockProps, BlockRegistry, RenderMode, RendererContextValue } from './types';\n","import type { UINode, UINodeProps } from 'blocks-schema';\n\nconst TEMPLATE = /\\{\\{\\s*([^}\\s]+)\\s*\\}\\}/g;\n\n/** Read a dotted path (`row.author.name`) out of a scope object. */\nexport function readPath(scope: Record<string, unknown>, path: string): unknown {\n\tlet current: unknown = scope;\n\tfor (const segment of path.split('.')) {\n\t\tif (current == null || typeof current !== 'object') return undefined;\n\t\tcurrent = (current as Record<string, unknown>)[segment];\n\t}\n\treturn current;\n}\n\n/**\n * Resolve a binding expression. A template that is exactly one placeholder\n * yields the raw value (so a boolean or an object survives); a template mixed\n * with text is interpolated as a string.\n */\nexport function resolveBinding(expression: string, scope: Record<string, unknown>): unknown {\n\tconst single = expression.match(/^\\{\\{\\s*([^}\\s]+)\\s*\\}\\}$/);\n\tif (single) {\n\t\treturn readPath(scope, single[1]);\n\t}\n\n\treturn expression.replace(TEMPLATE, (_match, path: string) => {\n\t\tconst value = readPath(scope, path);\n\t\treturn value == null ? '' : String(value);\n\t});\n}\n\n/** Apply a node's `bindings` over its static props. */\nexport function resolveNodeProps(node: UINode, scope: Record<string, unknown>): UINodeProps {\n\tif (!node.bindings) return node.props ?? {};\n\n\tconst resolved: UINodeProps = { ...(node.props ?? {}) };\n\tfor (const [prop, expression] of Object.entries(node.bindings)) {\n\t\tresolved[prop] = resolveBinding(expression, scope);\n\t}\n\treturn resolved;\n}\n","'use client';\n\nimport { createContext, useContext } from 'react';\nimport type { ReactNode } from 'react';\n\nimport type { RendererContextValue } from './types';\n\nconst RendererContext = createContext<RendererContextValue | null>(null);\n\nexport function RendererProvider({ children, value }: { children: ReactNode; value: RendererContextValue }) {\n\treturn <RendererContext.Provider value={value}>{children}</RendererContext.Provider>;\n}\n\nexport function useRenderer(): RendererContextValue {\n\tconst context = useContext(RendererContext);\n\tif (!context) {\n\t\tthrow new Error('useRenderer must be used within a RendererProvider (or a DocumentRenderer)');\n\t}\n\treturn context;\n}\n\n/**\n * Resolved props plus the value/error wiring for a field node. Widget\n * implementations use this instead of reaching into the document themselves.\n */\nexport function useBlockField(name: string | undefined) {\n\tconst { values, errors, setValue, setError, mode } = useRenderer();\n\tif (!name) {\n\t\treturn { value: undefined, error: undefined, setValue: () => {}, setError: () => {}, mode };\n\t}\n\treturn {\n\t\tvalue: values[name],\n\t\terror: errors[name],\n\t\tsetValue: (value: unknown) => setValue(name, value),\n\t\tsetError: (error: string | null) => setError(name, error),\n\t\tmode,\n\t};\n}\n","import type { BlockComponent, BlockRegistry } from './types';\n\n/**\n * Layer registries left-to-right, later layers winning. This is how a host\n * customizes rendering: base primitives, then an app registry, then per-document\n * overrides — no forking of the renderer, and no single global map.\n */\nexport function composeRegistry(...layers: (BlockRegistry | undefined)[]): BlockRegistry {\n\tconst composed: BlockRegistry = {};\n\tfor (const layer of layers) {\n\t\tif (!layer) continue;\n\t\tObject.assign(composed, layer);\n\t}\n\treturn composed;\n}\n\nexport function resolveBlock(registry: BlockRegistry, type: string): BlockComponent | undefined {\n\treturn registry[type];\n}\n\n/** Node types the registry can render, sorted for stable output. */\nexport function registeredTypes(registry: BlockRegistry): string[] {\n\treturn Object.keys(registry).sort();\n}\n","'use client';\n\nimport {\n\tcollectDefaultValues,\n\tcollectFieldConstraints,\n\tcollectFieldNames,\n\tvalidateField,\n\ttype UIAction,\n\ttype UIDocument,\n\ttype UINode,\n} from 'blocks-schema';\nimport { useCallback, useEffect, useMemo, useState } from 'react';\n\nimport { resolveNodeProps } from './bindings';\nimport { RendererProvider, useRenderer } from './context';\nimport { UnknownBlock } from './unknown-block';\nimport type { BlockRegistry, RenderMode, RendererContextValue } from './types';\n\n/**\n * Renders one node and, recursively, its children: resolve the node type in the\n * registry, resolve bindings against the current scope, render children first,\n * fall back to {@link UnknownBlock}.\n */\nexport function BlockRenderer({ node }: { node: UINode }) {\n\tconst { registry, scope } = useRenderer();\n\tconst Component = registry[node.type];\n\n\tif (!Component) {\n\t\treturn <UnknownBlock node={node} />;\n\t}\n\n\tconst children = node.children?.length\n\t\t? node.children.map((child) => <BlockRenderer key={child.key} node={child} />)\n\t\t: undefined;\n\n\treturn (\n\t\t<Component node={node} props={resolveNodeProps(node, scope)}>\n\t\t\t{children}\n\t\t</Component>\n\t);\n}\n\nexport interface DocumentRendererProps {\n\tdocument: UIDocument;\n\tregistry: BlockRegistry;\n\tinitialValues?: Record<string, unknown>;\n\t/** Extra data for binding expressions, e.g. `{ row, user, params }`. */\n\tscope?: Record<string, unknown>;\n\tonSubmit?: (values: Record<string, unknown>) => void;\n\tonChange?: (values: Record<string, unknown>) => void;\n\tonAction?: (action: UIAction, event: string) => void;\n\tmode?: RenderMode;\n\tclassName?: string;\n}\n\nexport function DocumentRenderer({\n\tdocument,\n\tregistry,\n\tinitialValues,\n\tscope: externalScope,\n\tonSubmit,\n\tonChange,\n\tonAction,\n\tmode = 'preview',\n\tclassName,\n}: DocumentRendererProps) {\n\tconst defaults = useMemo(() => collectDefaultValues(document.page), [document]);\n\tconst constraints = useMemo(() => collectFieldConstraints(document.page), [document]);\n\n\tconst [values, setValues] = useState<Record<string, unknown>>(() => ({ ...defaults, ...initialValues }));\n\tconst [errors, setErrors] = useState<Record<string, string>>({});\n\n\tuseEffect(() => {\n\t\tsetValues((previous) => ({ ...defaults, ...initialValues, ...previous }));\n\t}, [defaults, initialValues]);\n\n\tconst setError = useCallback((name: string, error: string | null) => {\n\t\tsetErrors((previous) => {\n\t\t\tif (error) return { ...previous, [name]: error };\n\t\t\tif (!(name in previous)) return previous;\n\t\t\tconst next = { ...previous };\n\t\t\tdelete next[name];\n\t\t\treturn next;\n\t\t});\n\t}, []);\n\n\tconst setValue = useCallback(\n\t\t(name: string, value: unknown) => {\n\t\t\tsetValues((previous) => {\n\t\t\t\tconst next = { ...previous, [name]: value };\n\t\t\t\tonChange?.(next);\n\t\t\t\treturn next;\n\t\t\t});\n\n\t\t\tconst field = constraints[name];\n\t\t\tif (field) {\n\t\t\t\tsetError(name, validateField(value, field.constraints, field.required));\n\t\t\t}\n\t\t},\n\t\t[onChange, constraints, setError],\n\t);\n\n\tconst handleAction = useCallback(\n\t\t(action: UIAction, event: string) => {\n\t\t\tif (event === 'submit' && onSubmit) {\n\t\t\t\tconst failures: Record<string, string> = {};\n\t\t\t\tfor (const name of collectFieldNames(document.page)) {\n\t\t\t\t\tconst field = constraints[name];\n\t\t\t\t\tif (!field) continue;\n\t\t\t\t\tconst error = validateField(values[name], field.constraints, field.required);\n\t\t\t\t\tif (error) failures[name] = error;\n\t\t\t\t}\n\n\t\t\t\tif (Object.keys(failures).length > 0) {\n\t\t\t\t\tsetErrors(failures);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tonSubmit(values);\n\t\t\t}\n\n\t\t\tonAction?.(action, event);\n\t\t},\n\t\t[onSubmit, onAction, values, document.page, constraints],\n\t);\n\n\tconst contextValue: RendererContextValue = useMemo(\n\t\t() => ({\n\t\t\tdocument,\n\t\t\tregistry,\n\t\t\tmode,\n\t\t\tvalues,\n\t\t\terrors,\n\t\t\tsetValue,\n\t\t\tsetError,\n\t\t\tscope: { ...externalScope, values, ...values },\n\t\t\tonAction: handleAction,\n\t\t}),\n\t\t[document, registry, mode, values, errors, setValue, setError, externalScope, handleAction],\n\t);\n\n\treturn (\n\t\t<RendererProvider value={contextValue}>\n\t\t\t<div className={className}>\n\t\t\t\t<BlockRenderer node={document.page} />\n\t\t\t</div>\n\t\t</RendererProvider>\n\t);\n}\n","'use client';\n\nimport type { UINode } from 'blocks-schema';\n\n/**\n * Rendered when no registry layer satisfies a node type. A document may name\n * blocks a given host has not installed, so an unknown type is a visible gap,\n * never a thrown render.\n */\nexport function UnknownBlock({ node }: { node: UINode }) {\n\treturn (\n\t\t<div data-block-unknown={node.type} role=\"note\">\n\t\t\tUnknown block: {node.type}\n\t\t</div>\n\t);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACEA,IAAM,WAAW;AAGV,SAAS,SAAS,OAAgC,MAAuB;AAC/E,MAAI,UAAmB;AACvB,aAAW,WAAW,KAAK,MAAM,GAAG,GAAG;AACtC,QAAI,WAAW,QAAQ,OAAO,YAAY,SAAU,QAAO;AAC3D,cAAW,QAAoC,OAAO;AAAA,EACvD;AACA,SAAO;AACR;AAOO,SAAS,eAAe,YAAoB,OAAyC;AAC3F,QAAM,SAAS,WAAW,MAAM,2BAA2B;AAC3D,MAAI,QAAQ;AACX,WAAO,SAAS,OAAO,OAAO,CAAC,CAAC;AAAA,EACjC;AAEA,SAAO,WAAW,QAAQ,UAAU,CAAC,QAAQ,SAAiB;AAC7D,UAAM,QAAQ,SAAS,OAAO,IAAI;AAClC,WAAO,SAAS,OAAO,KAAK,OAAO,KAAK;AAAA,EACzC,CAAC;AACF;AAGO,SAAS,iBAAiB,MAAc,OAA6C;AAC3F,MAAI,CAAC,KAAK,SAAU,QAAO,KAAK,SAAS,CAAC;AAE1C,QAAM,WAAwB,EAAE,GAAI,KAAK,SAAS,CAAC,EAAG;AACtD,aAAW,CAAC,MAAM,UAAU,KAAK,OAAO,QAAQ,KAAK,QAAQ,GAAG;AAC/D,aAAS,IAAI,IAAI,eAAe,YAAY,KAAK;AAAA,EAClD;AACA,SAAO;AACR;;;ACtCA,mBAA0C;AAQlC;AAHR,IAAM,sBAAkB,4BAA2C,IAAI;AAEhE,SAAS,iBAAiB,EAAE,UAAU,MAAM,GAAyD;AAC3G,SAAO,4CAAC,gBAAgB,UAAhB,EAAyB,OAAe,UAAS;AAC1D;AAEO,SAAS,cAAoC;AACnD,QAAM,cAAU,yBAAW,eAAe;AAC1C,MAAI,CAAC,SAAS;AACb,UAAM,IAAI,MAAM,4EAA4E;AAAA,EAC7F;AACA,SAAO;AACR;AAMO,SAAS,cAAc,MAA0B;AACvD,QAAM,EAAE,QAAQ,QAAQ,UAAU,UAAU,KAAK,IAAI,YAAY;AACjE,MAAI,CAAC,MAAM;AACV,WAAO,EAAE,OAAO,QAAW,OAAO,QAAW,UAAU,MAAM;AAAA,IAAC,GAAG,UAAU,MAAM;AAAA,IAAC,GAAG,KAAK;AAAA,EAC3F;AACA,SAAO;AAAA,IACN,OAAO,OAAO,IAAI;AAAA,IAClB,OAAO,OAAO,IAAI;AAAA,IAClB,UAAU,CAAC,UAAmB,SAAS,MAAM,KAAK;AAAA,IAClD,UAAU,CAAC,UAAyB,SAAS,MAAM,KAAK;AAAA,IACxD;AAAA,EACD;AACD;;;AC9BO,SAAS,mBAAmB,QAAsD;AACxF,QAAM,WAA0B,CAAC;AACjC,aAAW,SAAS,QAAQ;AAC3B,QAAI,CAAC,MAAO;AACZ,WAAO,OAAO,UAAU,KAAK;AAAA,EAC9B;AACA,SAAO;AACR;AAEO,SAAS,aAAa,UAAyB,MAA0C;AAC/F,SAAO,SAAS,IAAI;AACrB;AAGO,SAAS,gBAAgB,UAAmC;AAClE,SAAO,OAAO,KAAK,QAAQ,EAAE,KAAK;AACnC;;;ACrBA,2BAQO;AACP,IAAAA,gBAA0D;;;ACAxD,IAAAC,sBAAA;AAFK,SAAS,aAAa,EAAE,KAAK,GAAqB;AACxD,SACC,8CAAC,SAAI,sBAAoB,KAAK,MAAM,MAAK,QAAO;AAAA;AAAA,IAC/B,KAAK;AAAA,KACtB;AAEF;;;ADaS,IAAAC,sBAAA;AALF,SAAS,cAAc,EAAE,KAAK,GAAqB;AACzD,QAAM,EAAE,UAAU,MAAM,IAAI,YAAY;AACxC,QAAM,YAAY,SAAS,KAAK,IAAI;AAEpC,MAAI,CAAC,WAAW;AACf,WAAO,6CAAC,gBAAa,MAAY;AAAA,EAClC;AAEA,QAAM,WAAW,KAAK,UAAU,SAC7B,KAAK,SAAS,IAAI,CAAC,UAAU,6CAAC,iBAA8B,MAAM,SAAjB,MAAM,GAAkB,CAAE,IAC3E;AAEH,SACC,6CAAC,aAAU,MAAY,OAAO,iBAAiB,MAAM,KAAK,GACxD,UACF;AAEF;AAeO,SAAS,iBAAiB;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP;AACD,GAA0B;AACzB,QAAM,eAAW,uBAAQ,UAAM,2CAAqB,SAAS,IAAI,GAAG,CAAC,QAAQ,CAAC;AAC9E,QAAM,kBAAc,uBAAQ,UAAM,8CAAwB,SAAS,IAAI,GAAG,CAAC,QAAQ,CAAC;AAEpF,QAAM,CAAC,QAAQ,SAAS,QAAI,wBAAkC,OAAO,EAAE,GAAG,UAAU,GAAG,cAAc,EAAE;AACvG,QAAM,CAAC,QAAQ,SAAS,QAAI,wBAAiC,CAAC,CAAC;AAE/D,+BAAU,MAAM;AACf,cAAU,CAAC,cAAc,EAAE,GAAG,UAAU,GAAG,eAAe,GAAG,SAAS,EAAE;AAAA,EACzE,GAAG,CAAC,UAAU,aAAa,CAAC;AAE5B,QAAM,eAAW,2BAAY,CAAC,MAAc,UAAyB;AACpE,cAAU,CAAC,aAAa;AACvB,UAAI,MAAO,QAAO,EAAE,GAAG,UAAU,CAAC,IAAI,GAAG,MAAM;AAC/C,UAAI,EAAE,QAAQ,UAAW,QAAO;AAChC,YAAM,OAAO,EAAE,GAAG,SAAS;AAC3B,aAAO,KAAK,IAAI;AAChB,aAAO;AAAA,IACR,CAAC;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,QAAM,eAAW;AAAA,IAChB,CAAC,MAAc,UAAmB;AACjC,gBAAU,CAAC,aAAa;AACvB,cAAM,OAAO,EAAE,GAAG,UAAU,CAAC,IAAI,GAAG,MAAM;AAC1C,mBAAW,IAAI;AACf,eAAO;AAAA,MACR,CAAC;AAED,YAAM,QAAQ,YAAY,IAAI;AAC9B,UAAI,OAAO;AACV,iBAAS,UAAM,oCAAc,OAAO,MAAM,aAAa,MAAM,QAAQ,CAAC;AAAA,MACvE;AAAA,IACD;AAAA,IACA,CAAC,UAAU,aAAa,QAAQ;AAAA,EACjC;AAEA,QAAM,mBAAe;AAAA,IACpB,CAAC,QAAkB,UAAkB;AACpC,UAAI,UAAU,YAAY,UAAU;AACnC,cAAM,WAAmC,CAAC;AAC1C,mBAAW,YAAQ,wCAAkB,SAAS,IAAI,GAAG;AACpD,gBAAM,QAAQ,YAAY,IAAI;AAC9B,cAAI,CAAC,MAAO;AACZ,gBAAM,YAAQ,oCAAc,OAAO,IAAI,GAAG,MAAM,aAAa,MAAM,QAAQ;AAC3E,cAAI,MAAO,UAAS,IAAI,IAAI;AAAA,QAC7B;AAEA,YAAI,OAAO,KAAK,QAAQ,EAAE,SAAS,GAAG;AACrC,oBAAU,QAAQ;AAClB;AAAA,QACD;AAEA,iBAAS,MAAM;AAAA,MAChB;AAEA,iBAAW,QAAQ,KAAK;AAAA,IACzB;AAAA,IACA,CAAC,UAAU,UAAU,QAAQ,SAAS,MAAM,WAAW;AAAA,EACxD;AAEA,QAAM,mBAAqC;AAAA,IAC1C,OAAO;AAAA,MACN;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,OAAO,EAAE,GAAG,eAAe,QAAQ,GAAG,OAAO;AAAA,MAC7C,UAAU;AAAA,IACX;AAAA,IACA,CAAC,UAAU,UAAU,MAAM,QAAQ,QAAQ,UAAU,UAAU,eAAe,YAAY;AAAA,EAC3F;AAEA,SACC,6CAAC,oBAAiB,OAAO,cACxB,uDAAC,SAAI,WACJ,uDAAC,iBAAc,MAAM,SAAS,MAAM,GACrC,GACD;AAEF;","names":["import_react","import_jsx_runtime","import_jsx_runtime"]}
@@ -0,0 +1,106 @@
1
+ import { UINode, UINodeProps, UIDocument, UIAction } from 'blocks-schema';
2
+ import * as react from 'react';
3
+ import { ComponentType, ReactNode } from 'react';
4
+
5
+ /** Read a dotted path (`row.author.name`) out of a scope object. */
6
+ declare function readPath(scope: Record<string, unknown>, path: string): unknown;
7
+ /**
8
+ * Resolve a binding expression. A template that is exactly one placeholder
9
+ * yields the raw value (so a boolean or an object survives); a template mixed
10
+ * with text is interpolated as a string.
11
+ */
12
+ declare function resolveBinding(expression: string, scope: Record<string, unknown>): unknown;
13
+ /** Apply a node's `bindings` over its static props. */
14
+ declare function resolveNodeProps(node: UINode, scope: Record<string, unknown>): UINodeProps;
15
+
16
+ type RenderMode = 'preview' | 'edit';
17
+ /**
18
+ * Props every block component receives. Children are already rendered, and
19
+ * `props` has the node's bindings resolved against the current scope.
20
+ */
21
+ interface BlockProps {
22
+ node: UINode;
23
+ props: UINodeProps;
24
+ children?: ReactNode;
25
+ }
26
+ type BlockComponent = ComponentType<BlockProps>;
27
+ /** Node type → component. Layered by {@link composeRegistry}. */
28
+ type BlockRegistry = Record<string, BlockComponent>;
29
+ interface RendererContextValue {
30
+ document: UIDocument;
31
+ registry: BlockRegistry;
32
+ mode: RenderMode;
33
+ values: Record<string, unknown>;
34
+ errors: Record<string, string>;
35
+ setValue: (name: string, value: unknown) => void;
36
+ setError: (name: string, error: string | null) => void;
37
+ /** Scope for binding expressions (`{{ row.title }}`), merged with `values`. */
38
+ scope: Record<string, unknown>;
39
+ onAction?: (action: UIAction, event: string) => void;
40
+ }
41
+
42
+ declare function RendererProvider({ children, value }: {
43
+ children: ReactNode;
44
+ value: RendererContextValue;
45
+ }): react.JSX.Element;
46
+ declare function useRenderer(): RendererContextValue;
47
+ /**
48
+ * Resolved props plus the value/error wiring for a field node. Widget
49
+ * implementations use this instead of reaching into the document themselves.
50
+ */
51
+ declare function useBlockField(name: string | undefined): {
52
+ value: undefined;
53
+ error: undefined;
54
+ setValue: () => void;
55
+ setError: () => void;
56
+ mode: RenderMode;
57
+ } | {
58
+ value: unknown;
59
+ error: string;
60
+ setValue: (value: unknown) => void;
61
+ setError: (error: string | null) => void;
62
+ mode: RenderMode;
63
+ };
64
+
65
+ /**
66
+ * Layer registries left-to-right, later layers winning. This is how a host
67
+ * customizes rendering: base primitives, then an app registry, then per-document
68
+ * overrides — no forking of the renderer, and no single global map.
69
+ */
70
+ declare function composeRegistry(...layers: (BlockRegistry | undefined)[]): BlockRegistry;
71
+ declare function resolveBlock(registry: BlockRegistry, type: string): BlockComponent | undefined;
72
+ /** Node types the registry can render, sorted for stable output. */
73
+ declare function registeredTypes(registry: BlockRegistry): string[];
74
+
75
+ /**
76
+ * Renders one node and, recursively, its children: resolve the node type in the
77
+ * registry, resolve bindings against the current scope, render children first,
78
+ * fall back to {@link UnknownBlock}.
79
+ */
80
+ declare function BlockRenderer({ node }: {
81
+ node: UINode;
82
+ }): react.JSX.Element;
83
+ interface DocumentRendererProps {
84
+ document: UIDocument;
85
+ registry: BlockRegistry;
86
+ initialValues?: Record<string, unknown>;
87
+ /** Extra data for binding expressions, e.g. `{ row, user, params }`. */
88
+ scope?: Record<string, unknown>;
89
+ onSubmit?: (values: Record<string, unknown>) => void;
90
+ onChange?: (values: Record<string, unknown>) => void;
91
+ onAction?: (action: UIAction, event: string) => void;
92
+ mode?: RenderMode;
93
+ className?: string;
94
+ }
95
+ declare function DocumentRenderer({ document, registry, initialValues, scope: externalScope, onSubmit, onChange, onAction, mode, className, }: DocumentRendererProps): react.JSX.Element;
96
+
97
+ /**
98
+ * Rendered when no registry layer satisfies a node type. A document may name
99
+ * blocks a given host has not installed, so an unknown type is a visible gap,
100
+ * never a thrown render.
101
+ */
102
+ declare function UnknownBlock({ node }: {
103
+ node: UINode;
104
+ }): react.JSX.Element;
105
+
106
+ export { type BlockComponent, type BlockProps, type BlockRegistry, BlockRenderer, DocumentRenderer, type DocumentRendererProps, type RenderMode, type RendererContextValue, RendererProvider, UnknownBlock, composeRegistry, readPath, registeredTypes, resolveBinding, resolveBlock, resolveNodeProps, useBlockField, useRenderer };
@@ -0,0 +1,106 @@
1
+ import { UINode, UINodeProps, UIDocument, UIAction } from 'blocks-schema';
2
+ import * as react from 'react';
3
+ import { ComponentType, ReactNode } from 'react';
4
+
5
+ /** Read a dotted path (`row.author.name`) out of a scope object. */
6
+ declare function readPath(scope: Record<string, unknown>, path: string): unknown;
7
+ /**
8
+ * Resolve a binding expression. A template that is exactly one placeholder
9
+ * yields the raw value (so a boolean or an object survives); a template mixed
10
+ * with text is interpolated as a string.
11
+ */
12
+ declare function resolveBinding(expression: string, scope: Record<string, unknown>): unknown;
13
+ /** Apply a node's `bindings` over its static props. */
14
+ declare function resolveNodeProps(node: UINode, scope: Record<string, unknown>): UINodeProps;
15
+
16
+ type RenderMode = 'preview' | 'edit';
17
+ /**
18
+ * Props every block component receives. Children are already rendered, and
19
+ * `props` has the node's bindings resolved against the current scope.
20
+ */
21
+ interface BlockProps {
22
+ node: UINode;
23
+ props: UINodeProps;
24
+ children?: ReactNode;
25
+ }
26
+ type BlockComponent = ComponentType<BlockProps>;
27
+ /** Node type → component. Layered by {@link composeRegistry}. */
28
+ type BlockRegistry = Record<string, BlockComponent>;
29
+ interface RendererContextValue {
30
+ document: UIDocument;
31
+ registry: BlockRegistry;
32
+ mode: RenderMode;
33
+ values: Record<string, unknown>;
34
+ errors: Record<string, string>;
35
+ setValue: (name: string, value: unknown) => void;
36
+ setError: (name: string, error: string | null) => void;
37
+ /** Scope for binding expressions (`{{ row.title }}`), merged with `values`. */
38
+ scope: Record<string, unknown>;
39
+ onAction?: (action: UIAction, event: string) => void;
40
+ }
41
+
42
+ declare function RendererProvider({ children, value }: {
43
+ children: ReactNode;
44
+ value: RendererContextValue;
45
+ }): react.JSX.Element;
46
+ declare function useRenderer(): RendererContextValue;
47
+ /**
48
+ * Resolved props plus the value/error wiring for a field node. Widget
49
+ * implementations use this instead of reaching into the document themselves.
50
+ */
51
+ declare function useBlockField(name: string | undefined): {
52
+ value: undefined;
53
+ error: undefined;
54
+ setValue: () => void;
55
+ setError: () => void;
56
+ mode: RenderMode;
57
+ } | {
58
+ value: unknown;
59
+ error: string;
60
+ setValue: (value: unknown) => void;
61
+ setError: (error: string | null) => void;
62
+ mode: RenderMode;
63
+ };
64
+
65
+ /**
66
+ * Layer registries left-to-right, later layers winning. This is how a host
67
+ * customizes rendering: base primitives, then an app registry, then per-document
68
+ * overrides — no forking of the renderer, and no single global map.
69
+ */
70
+ declare function composeRegistry(...layers: (BlockRegistry | undefined)[]): BlockRegistry;
71
+ declare function resolveBlock(registry: BlockRegistry, type: string): BlockComponent | undefined;
72
+ /** Node types the registry can render, sorted for stable output. */
73
+ declare function registeredTypes(registry: BlockRegistry): string[];
74
+
75
+ /**
76
+ * Renders one node and, recursively, its children: resolve the node type in the
77
+ * registry, resolve bindings against the current scope, render children first,
78
+ * fall back to {@link UnknownBlock}.
79
+ */
80
+ declare function BlockRenderer({ node }: {
81
+ node: UINode;
82
+ }): react.JSX.Element;
83
+ interface DocumentRendererProps {
84
+ document: UIDocument;
85
+ registry: BlockRegistry;
86
+ initialValues?: Record<string, unknown>;
87
+ /** Extra data for binding expressions, e.g. `{ row, user, params }`. */
88
+ scope?: Record<string, unknown>;
89
+ onSubmit?: (values: Record<string, unknown>) => void;
90
+ onChange?: (values: Record<string, unknown>) => void;
91
+ onAction?: (action: UIAction, event: string) => void;
92
+ mode?: RenderMode;
93
+ className?: string;
94
+ }
95
+ declare function DocumentRenderer({ document, registry, initialValues, scope: externalScope, onSubmit, onChange, onAction, mode, className, }: DocumentRendererProps): react.JSX.Element;
96
+
97
+ /**
98
+ * Rendered when no registry layer satisfies a node type. A document may name
99
+ * blocks a given host has not installed, so an unknown type is a visible gap,
100
+ * never a thrown render.
101
+ */
102
+ declare function UnknownBlock({ node }: {
103
+ node: UINode;
104
+ }): react.JSX.Element;
105
+
106
+ export { type BlockComponent, type BlockProps, type BlockRegistry, BlockRenderer, DocumentRenderer, type DocumentRendererProps, type RenderMode, type RendererContextValue, RendererProvider, UnknownBlock, composeRegistry, readPath, registeredTypes, resolveBinding, resolveBlock, resolveNodeProps, useBlockField, useRenderer };
package/dist/index.js ADDED
@@ -0,0 +1,196 @@
1
+ // src/bindings.ts
2
+ var TEMPLATE = /\{\{\s*([^}\s]+)\s*\}\}/g;
3
+ function readPath(scope, path) {
4
+ let current = scope;
5
+ for (const segment of path.split(".")) {
6
+ if (current == null || typeof current !== "object") return void 0;
7
+ current = current[segment];
8
+ }
9
+ return current;
10
+ }
11
+ function resolveBinding(expression, scope) {
12
+ const single = expression.match(/^\{\{\s*([^}\s]+)\s*\}\}$/);
13
+ if (single) {
14
+ return readPath(scope, single[1]);
15
+ }
16
+ return expression.replace(TEMPLATE, (_match, path) => {
17
+ const value = readPath(scope, path);
18
+ return value == null ? "" : String(value);
19
+ });
20
+ }
21
+ function resolveNodeProps(node, scope) {
22
+ if (!node.bindings) return node.props ?? {};
23
+ const resolved = { ...node.props ?? {} };
24
+ for (const [prop, expression] of Object.entries(node.bindings)) {
25
+ resolved[prop] = resolveBinding(expression, scope);
26
+ }
27
+ return resolved;
28
+ }
29
+
30
+ // src/context.tsx
31
+ import { createContext, useContext } from "react";
32
+ import { jsx } from "react/jsx-runtime";
33
+ var RendererContext = createContext(null);
34
+ function RendererProvider({ children, value }) {
35
+ return /* @__PURE__ */ jsx(RendererContext.Provider, { value, children });
36
+ }
37
+ function useRenderer() {
38
+ const context = useContext(RendererContext);
39
+ if (!context) {
40
+ throw new Error("useRenderer must be used within a RendererProvider (or a DocumentRenderer)");
41
+ }
42
+ return context;
43
+ }
44
+ function useBlockField(name) {
45
+ const { values, errors, setValue, setError, mode } = useRenderer();
46
+ if (!name) {
47
+ return { value: void 0, error: void 0, setValue: () => {
48
+ }, setError: () => {
49
+ }, mode };
50
+ }
51
+ return {
52
+ value: values[name],
53
+ error: errors[name],
54
+ setValue: (value) => setValue(name, value),
55
+ setError: (error) => setError(name, error),
56
+ mode
57
+ };
58
+ }
59
+
60
+ // src/registry.ts
61
+ function composeRegistry(...layers) {
62
+ const composed = {};
63
+ for (const layer of layers) {
64
+ if (!layer) continue;
65
+ Object.assign(composed, layer);
66
+ }
67
+ return composed;
68
+ }
69
+ function resolveBlock(registry, type) {
70
+ return registry[type];
71
+ }
72
+ function registeredTypes(registry) {
73
+ return Object.keys(registry).sort();
74
+ }
75
+
76
+ // src/renderer.tsx
77
+ import {
78
+ collectDefaultValues,
79
+ collectFieldConstraints,
80
+ collectFieldNames,
81
+ validateField
82
+ } from "blocks-schema";
83
+ import { useCallback, useEffect, useMemo, useState } from "react";
84
+
85
+ // src/unknown-block.tsx
86
+ import { jsxs } from "react/jsx-runtime";
87
+ function UnknownBlock({ node }) {
88
+ return /* @__PURE__ */ jsxs("div", { "data-block-unknown": node.type, role: "note", children: [
89
+ "Unknown block: ",
90
+ node.type
91
+ ] });
92
+ }
93
+
94
+ // src/renderer.tsx
95
+ import { jsx as jsx2 } from "react/jsx-runtime";
96
+ function BlockRenderer({ node }) {
97
+ const { registry, scope } = useRenderer();
98
+ const Component = registry[node.type];
99
+ if (!Component) {
100
+ return /* @__PURE__ */ jsx2(UnknownBlock, { node });
101
+ }
102
+ const children = node.children?.length ? node.children.map((child) => /* @__PURE__ */ jsx2(BlockRenderer, { node: child }, child.key)) : void 0;
103
+ return /* @__PURE__ */ jsx2(Component, { node, props: resolveNodeProps(node, scope), children });
104
+ }
105
+ function DocumentRenderer({
106
+ document,
107
+ registry,
108
+ initialValues,
109
+ scope: externalScope,
110
+ onSubmit,
111
+ onChange,
112
+ onAction,
113
+ mode = "preview",
114
+ className
115
+ }) {
116
+ const defaults = useMemo(() => collectDefaultValues(document.page), [document]);
117
+ const constraints = useMemo(() => collectFieldConstraints(document.page), [document]);
118
+ const [values, setValues] = useState(() => ({ ...defaults, ...initialValues }));
119
+ const [errors, setErrors] = useState({});
120
+ useEffect(() => {
121
+ setValues((previous) => ({ ...defaults, ...initialValues, ...previous }));
122
+ }, [defaults, initialValues]);
123
+ const setError = useCallback((name, error) => {
124
+ setErrors((previous) => {
125
+ if (error) return { ...previous, [name]: error };
126
+ if (!(name in previous)) return previous;
127
+ const next = { ...previous };
128
+ delete next[name];
129
+ return next;
130
+ });
131
+ }, []);
132
+ const setValue = useCallback(
133
+ (name, value) => {
134
+ setValues((previous) => {
135
+ const next = { ...previous, [name]: value };
136
+ onChange?.(next);
137
+ return next;
138
+ });
139
+ const field = constraints[name];
140
+ if (field) {
141
+ setError(name, validateField(value, field.constraints, field.required));
142
+ }
143
+ },
144
+ [onChange, constraints, setError]
145
+ );
146
+ const handleAction = useCallback(
147
+ (action, event) => {
148
+ if (event === "submit" && onSubmit) {
149
+ const failures = {};
150
+ for (const name of collectFieldNames(document.page)) {
151
+ const field = constraints[name];
152
+ if (!field) continue;
153
+ const error = validateField(values[name], field.constraints, field.required);
154
+ if (error) failures[name] = error;
155
+ }
156
+ if (Object.keys(failures).length > 0) {
157
+ setErrors(failures);
158
+ return;
159
+ }
160
+ onSubmit(values);
161
+ }
162
+ onAction?.(action, event);
163
+ },
164
+ [onSubmit, onAction, values, document.page, constraints]
165
+ );
166
+ const contextValue = useMemo(
167
+ () => ({
168
+ document,
169
+ registry,
170
+ mode,
171
+ values,
172
+ errors,
173
+ setValue,
174
+ setError,
175
+ scope: { ...externalScope, values, ...values },
176
+ onAction: handleAction
177
+ }),
178
+ [document, registry, mode, values, errors, setValue, setError, externalScope, handleAction]
179
+ );
180
+ return /* @__PURE__ */ jsx2(RendererProvider, { value: contextValue, children: /* @__PURE__ */ jsx2("div", { className, children: /* @__PURE__ */ jsx2(BlockRenderer, { node: document.page }) }) });
181
+ }
182
+ export {
183
+ BlockRenderer,
184
+ DocumentRenderer,
185
+ RendererProvider,
186
+ UnknownBlock,
187
+ composeRegistry,
188
+ readPath,
189
+ registeredTypes,
190
+ resolveBinding,
191
+ resolveBlock,
192
+ resolveNodeProps,
193
+ useBlockField,
194
+ useRenderer
195
+ };
196
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/bindings.ts","../src/context.tsx","../src/registry.ts","../src/renderer.tsx","../src/unknown-block.tsx"],"sourcesContent":["import type { UINode, UINodeProps } from 'blocks-schema';\n\nconst TEMPLATE = /\\{\\{\\s*([^}\\s]+)\\s*\\}\\}/g;\n\n/** Read a dotted path (`row.author.name`) out of a scope object. */\nexport function readPath(scope: Record<string, unknown>, path: string): unknown {\n\tlet current: unknown = scope;\n\tfor (const segment of path.split('.')) {\n\t\tif (current == null || typeof current !== 'object') return undefined;\n\t\tcurrent = (current as Record<string, unknown>)[segment];\n\t}\n\treturn current;\n}\n\n/**\n * Resolve a binding expression. A template that is exactly one placeholder\n * yields the raw value (so a boolean or an object survives); a template mixed\n * with text is interpolated as a string.\n */\nexport function resolveBinding(expression: string, scope: Record<string, unknown>): unknown {\n\tconst single = expression.match(/^\\{\\{\\s*([^}\\s]+)\\s*\\}\\}$/);\n\tif (single) {\n\t\treturn readPath(scope, single[1]);\n\t}\n\n\treturn expression.replace(TEMPLATE, (_match, path: string) => {\n\t\tconst value = readPath(scope, path);\n\t\treturn value == null ? '' : String(value);\n\t});\n}\n\n/** Apply a node's `bindings` over its static props. */\nexport function resolveNodeProps(node: UINode, scope: Record<string, unknown>): UINodeProps {\n\tif (!node.bindings) return node.props ?? {};\n\n\tconst resolved: UINodeProps = { ...(node.props ?? {}) };\n\tfor (const [prop, expression] of Object.entries(node.bindings)) {\n\t\tresolved[prop] = resolveBinding(expression, scope);\n\t}\n\treturn resolved;\n}\n","'use client';\n\nimport { createContext, useContext } from 'react';\nimport type { ReactNode } from 'react';\n\nimport type { RendererContextValue } from './types';\n\nconst RendererContext = createContext<RendererContextValue | null>(null);\n\nexport function RendererProvider({ children, value }: { children: ReactNode; value: RendererContextValue }) {\n\treturn <RendererContext.Provider value={value}>{children}</RendererContext.Provider>;\n}\n\nexport function useRenderer(): RendererContextValue {\n\tconst context = useContext(RendererContext);\n\tif (!context) {\n\t\tthrow new Error('useRenderer must be used within a RendererProvider (or a DocumentRenderer)');\n\t}\n\treturn context;\n}\n\n/**\n * Resolved props plus the value/error wiring for a field node. Widget\n * implementations use this instead of reaching into the document themselves.\n */\nexport function useBlockField(name: string | undefined) {\n\tconst { values, errors, setValue, setError, mode } = useRenderer();\n\tif (!name) {\n\t\treturn { value: undefined, error: undefined, setValue: () => {}, setError: () => {}, mode };\n\t}\n\treturn {\n\t\tvalue: values[name],\n\t\terror: errors[name],\n\t\tsetValue: (value: unknown) => setValue(name, value),\n\t\tsetError: (error: string | null) => setError(name, error),\n\t\tmode,\n\t};\n}\n","import type { BlockComponent, BlockRegistry } from './types';\n\n/**\n * Layer registries left-to-right, later layers winning. This is how a host\n * customizes rendering: base primitives, then an app registry, then per-document\n * overrides — no forking of the renderer, and no single global map.\n */\nexport function composeRegistry(...layers: (BlockRegistry | undefined)[]): BlockRegistry {\n\tconst composed: BlockRegistry = {};\n\tfor (const layer of layers) {\n\t\tif (!layer) continue;\n\t\tObject.assign(composed, layer);\n\t}\n\treturn composed;\n}\n\nexport function resolveBlock(registry: BlockRegistry, type: string): BlockComponent | undefined {\n\treturn registry[type];\n}\n\n/** Node types the registry can render, sorted for stable output. */\nexport function registeredTypes(registry: BlockRegistry): string[] {\n\treturn Object.keys(registry).sort();\n}\n","'use client';\n\nimport {\n\tcollectDefaultValues,\n\tcollectFieldConstraints,\n\tcollectFieldNames,\n\tvalidateField,\n\ttype UIAction,\n\ttype UIDocument,\n\ttype UINode,\n} from 'blocks-schema';\nimport { useCallback, useEffect, useMemo, useState } from 'react';\n\nimport { resolveNodeProps } from './bindings';\nimport { RendererProvider, useRenderer } from './context';\nimport { UnknownBlock } from './unknown-block';\nimport type { BlockRegistry, RenderMode, RendererContextValue } from './types';\n\n/**\n * Renders one node and, recursively, its children: resolve the node type in the\n * registry, resolve bindings against the current scope, render children first,\n * fall back to {@link UnknownBlock}.\n */\nexport function BlockRenderer({ node }: { node: UINode }) {\n\tconst { registry, scope } = useRenderer();\n\tconst Component = registry[node.type];\n\n\tif (!Component) {\n\t\treturn <UnknownBlock node={node} />;\n\t}\n\n\tconst children = node.children?.length\n\t\t? node.children.map((child) => <BlockRenderer key={child.key} node={child} />)\n\t\t: undefined;\n\n\treturn (\n\t\t<Component node={node} props={resolveNodeProps(node, scope)}>\n\t\t\t{children}\n\t\t</Component>\n\t);\n}\n\nexport interface DocumentRendererProps {\n\tdocument: UIDocument;\n\tregistry: BlockRegistry;\n\tinitialValues?: Record<string, unknown>;\n\t/** Extra data for binding expressions, e.g. `{ row, user, params }`. */\n\tscope?: Record<string, unknown>;\n\tonSubmit?: (values: Record<string, unknown>) => void;\n\tonChange?: (values: Record<string, unknown>) => void;\n\tonAction?: (action: UIAction, event: string) => void;\n\tmode?: RenderMode;\n\tclassName?: string;\n}\n\nexport function DocumentRenderer({\n\tdocument,\n\tregistry,\n\tinitialValues,\n\tscope: externalScope,\n\tonSubmit,\n\tonChange,\n\tonAction,\n\tmode = 'preview',\n\tclassName,\n}: DocumentRendererProps) {\n\tconst defaults = useMemo(() => collectDefaultValues(document.page), [document]);\n\tconst constraints = useMemo(() => collectFieldConstraints(document.page), [document]);\n\n\tconst [values, setValues] = useState<Record<string, unknown>>(() => ({ ...defaults, ...initialValues }));\n\tconst [errors, setErrors] = useState<Record<string, string>>({});\n\n\tuseEffect(() => {\n\t\tsetValues((previous) => ({ ...defaults, ...initialValues, ...previous }));\n\t}, [defaults, initialValues]);\n\n\tconst setError = useCallback((name: string, error: string | null) => {\n\t\tsetErrors((previous) => {\n\t\t\tif (error) return { ...previous, [name]: error };\n\t\t\tif (!(name in previous)) return previous;\n\t\t\tconst next = { ...previous };\n\t\t\tdelete next[name];\n\t\t\treturn next;\n\t\t});\n\t}, []);\n\n\tconst setValue = useCallback(\n\t\t(name: string, value: unknown) => {\n\t\t\tsetValues((previous) => {\n\t\t\t\tconst next = { ...previous, [name]: value };\n\t\t\t\tonChange?.(next);\n\t\t\t\treturn next;\n\t\t\t});\n\n\t\t\tconst field = constraints[name];\n\t\t\tif (field) {\n\t\t\t\tsetError(name, validateField(value, field.constraints, field.required));\n\t\t\t}\n\t\t},\n\t\t[onChange, constraints, setError],\n\t);\n\n\tconst handleAction = useCallback(\n\t\t(action: UIAction, event: string) => {\n\t\t\tif (event === 'submit' && onSubmit) {\n\t\t\t\tconst failures: Record<string, string> = {};\n\t\t\t\tfor (const name of collectFieldNames(document.page)) {\n\t\t\t\t\tconst field = constraints[name];\n\t\t\t\t\tif (!field) continue;\n\t\t\t\t\tconst error = validateField(values[name], field.constraints, field.required);\n\t\t\t\t\tif (error) failures[name] = error;\n\t\t\t\t}\n\n\t\t\t\tif (Object.keys(failures).length > 0) {\n\t\t\t\t\tsetErrors(failures);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tonSubmit(values);\n\t\t\t}\n\n\t\t\tonAction?.(action, event);\n\t\t},\n\t\t[onSubmit, onAction, values, document.page, constraints],\n\t);\n\n\tconst contextValue: RendererContextValue = useMemo(\n\t\t() => ({\n\t\t\tdocument,\n\t\t\tregistry,\n\t\t\tmode,\n\t\t\tvalues,\n\t\t\terrors,\n\t\t\tsetValue,\n\t\t\tsetError,\n\t\t\tscope: { ...externalScope, values, ...values },\n\t\t\tonAction: handleAction,\n\t\t}),\n\t\t[document, registry, mode, values, errors, setValue, setError, externalScope, handleAction],\n\t);\n\n\treturn (\n\t\t<RendererProvider value={contextValue}>\n\t\t\t<div className={className}>\n\t\t\t\t<BlockRenderer node={document.page} />\n\t\t\t</div>\n\t\t</RendererProvider>\n\t);\n}\n","'use client';\n\nimport type { UINode } from 'blocks-schema';\n\n/**\n * Rendered when no registry layer satisfies a node type. A document may name\n * blocks a given host has not installed, so an unknown type is a visible gap,\n * never a thrown render.\n */\nexport function UnknownBlock({ node }: { node: UINode }) {\n\treturn (\n\t\t<div data-block-unknown={node.type} role=\"note\">\n\t\t\tUnknown block: {node.type}\n\t\t</div>\n\t);\n}\n"],"mappings":"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","names":["jsx"]}
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "blocks-renderer",
3
+ "version": "0.1.1",
4
+ "description": "Recursive React renderer for Constructive Blocks JSON UI documents, with layerable widget registries",
5
+ "private": false,
6
+ "license": "MIT",
7
+ "homepage": "https://constructive-io.github.io/blocks/",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/constructive-io/blocks.git",
11
+ "directory": "packages/blocks-renderer"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/constructive-io/blocks/issues"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "type": "module",
20
+ "sideEffects": false,
21
+ "exports": {
22
+ ".": {
23
+ "import": {
24
+ "types": "./dist/index.d.ts",
25
+ "default": "./dist/index.js"
26
+ },
27
+ "require": {
28
+ "types": "./dist/index.d.cts",
29
+ "default": "./dist/index.cjs"
30
+ }
31
+ },
32
+ "./package.json": "./package.json"
33
+ },
34
+ "main": "./dist/index.cjs",
35
+ "module": "./dist/index.js",
36
+ "types": "./dist/index.d.ts",
37
+ "files": [
38
+ "dist",
39
+ "LICENSE",
40
+ "README.md"
41
+ ],
42
+ "scripts": {
43
+ "build": "tsup",
44
+ "dev": "tsup --watch",
45
+ "lint:types": "tsc --noEmit",
46
+ "test": "vitest run",
47
+ "test:watch": "vitest",
48
+ "clean": "rm -rf dist"
49
+ },
50
+ "dependencies": {
51
+ "blocks-schema": "^0.2.0"
52
+ },
53
+ "peerDependencies": {
54
+ "react": "^18.0.0 || ^19.0.0"
55
+ },
56
+ "devDependencies": {
57
+ "@types/react": "^19.2.7",
58
+ "@types/react-dom": "^19.2.3",
59
+ "react": "^19.2.3",
60
+ "react-dom": "^19.2.3",
61
+ "tsup": "^8.5.1",
62
+ "typescript": "^5.9.3",
63
+ "vitest": "^3.2.4"
64
+ },
65
+ "engines": {
66
+ "node": ">=24.0.0"
67
+ },
68
+ "gitHead": "b0fd1126b68b23543a51949c1e847c4d820863d3"
69
+ }