@contractspec/lib.contracts-runtime-client-react 2.0.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.
@@ -0,0 +1,424 @@
1
+ // src/drivers/rn-reusables.ts
2
+ function rnReusablesDriver(slots) {
3
+ return slots;
4
+ }
5
+
6
+ // src/drivers/shadcn.ts
7
+ function shadcnDriver(slots) {
8
+ return slots;
9
+ }
10
+
11
+ // src/feature-render.ts
12
+ import React from "react";
13
+ import {
14
+ createDefaultTransformEngine,
15
+ registerBasicValidation,
16
+ registerDefaultReactRenderer
17
+ } from "@contractspec/lib.contracts-spec/presentations/transform-engine";
18
+ function createEngineWithDefaults() {
19
+ return registerBasicValidation(registerDefaultReactRenderer(createDefaultTransformEngine()));
20
+ }
21
+ async function renderFeaturePresentation(engine, target, desc, options) {
22
+ if (target === "react") {
23
+ const rd = await engine.render("react", desc);
24
+ if (rd.kind === "react_component") {
25
+ const map = options?.componentMap ?? {};
26
+ const C = map[rd.componentKey];
27
+ if (!C)
28
+ return null;
29
+ const merged = {
30
+ ...rd.props ?? {},
31
+ ...options?.reactProps ?? {}
32
+ };
33
+ return React.createElement(C, merged);
34
+ }
35
+ if (rd.kind === "blocknotejs") {
36
+ if (options?.renderBlockNote)
37
+ return options.renderBlockNote(rd.docJson, rd.blockConfig);
38
+ return React.createElement("div", {}, "[BlockNote renderer not configured]");
39
+ }
40
+ return null;
41
+ }
42
+ if (target === "markdown")
43
+ return engine.render(target, desc);
44
+ if (target === "application/json")
45
+ return engine.render(target, desc);
46
+ if (target === "application/xml")
47
+ return engine.render(target, desc);
48
+ return null;
49
+ }
50
+ function createFeatureModule(meta, refs) {
51
+ return { meta, ...refs };
52
+ }
53
+ function registerFeature(registry, feature) {
54
+ registry.register(feature);
55
+ return registry;
56
+ }
57
+
58
+ // src/form-render.impl.tsx
59
+ import React2, { useEffect, useMemo, useState } from "react";
60
+ import {
61
+ Controller,
62
+ useFieldArray,
63
+ useForm
64
+ } from "react-hook-form";
65
+ import { zodResolver } from "@hookform/resolvers/zod";
66
+ import {
67
+ buildZodWithRelations,
68
+ evalPredicate
69
+ } from "@contractspec/lib.contracts-spec/forms";
70
+ import { jsxDEV, Fragment } from "react/jsx-dev-runtime";
71
+ "use client";
72
+ function toOptionsArray(src) {
73
+ if (!src)
74
+ return;
75
+ if (Array.isArray(src))
76
+ return { kind: "static", options: src };
77
+ return src;
78
+ }
79
+ function getAtPath(values, path) {
80
+ if (!path)
81
+ return;
82
+ const segs = path.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
83
+ let cur = values;
84
+ for (const s of segs) {
85
+ if (cur == null)
86
+ return;
87
+ cur = cur[s];
88
+ }
89
+ return cur;
90
+ }
91
+ function makeDepsKey(values, deps) {
92
+ if (!deps || deps.length === 0)
93
+ return "[]";
94
+ try {
95
+ return JSON.stringify(deps.map((d) => getAtPath(values, d)));
96
+ } catch {
97
+ return "[]";
98
+ }
99
+ }
100
+ function useResolvedOptions(values, source, resolvers) {
101
+ const [opts, setOpts] = useState([]);
102
+ const depKey = useMemo(() => {
103
+ if (!source)
104
+ return "nil";
105
+ if (source.kind === "static")
106
+ return JSON.stringify(source.options ?? []);
107
+ return makeDepsKey(values, source.deps);
108
+ }, [source, values]);
109
+ useEffect(() => {
110
+ let mounted = true;
111
+ const run = async () => {
112
+ if (!source)
113
+ return setOpts([]);
114
+ if (source.kind === "static")
115
+ return setOpts([...source.options ?? []]);
116
+ const fn = resolvers?.[source.resolverKey];
117
+ if (!fn)
118
+ return setOpts([]);
119
+ const res = await fn(values, source.args);
120
+ if (mounted)
121
+ setOpts([...res ?? []]);
122
+ };
123
+ run();
124
+ return () => {
125
+ mounted = false;
126
+ };
127
+ }, [
128
+ depKey,
129
+ source && source.kind === "resolver" ? source.resolverKey : undefined
130
+ ]);
131
+ return opts;
132
+ }
133
+ function fieldPath(parent, name, arrayIndex) {
134
+ if (!name)
135
+ return parent ?? "";
136
+ const child = typeof arrayIndex === "number" ? `${name.replace(/^\$index$/, String(arrayIndex))}` : name;
137
+ return parent ? `${parent}${typeof arrayIndex === "number" ? `.${arrayIndex}` : ""}.${child}`.replace(/\.+/g, ".") : child;
138
+ }
139
+ function createFormRenderer(base) {
140
+ const conf = base;
141
+ const { driver } = conf;
142
+ function InternalForm(props) {
143
+ const { spec, options, merged } = props;
144
+ const baseZod = useMemo(() => buildZodWithRelations(spec), [spec]);
145
+ const form = useForm({
146
+ ...merged.formOptions,
147
+ resolver: zodResolver(baseZod),
148
+ defaultValues: options?.defaultValues
149
+ });
150
+ const values = form.watch();
151
+ const renderOne = (f, parent, arrayIndex) => {
152
+ const DriverField = driver.Field;
153
+ const DriverLabel = driver.FieldLabel;
154
+ const DriverDesc = driver.FieldDescription;
155
+ const DriverError = driver.FieldError;
156
+ const name = fieldPath(parent, f.name, arrayIndex);
157
+ const visible = evalPredicate(values, f.visibleWhen);
158
+ const enabled = evalPredicate(values, f.enabledWhen);
159
+ const invalid = Boolean(form.getFieldState(name)?.invalid);
160
+ if (!visible)
161
+ return null;
162
+ const id = name?.replace(/\./g, "-");
163
+ const commonWrapProps = {
164
+ "data-invalid": invalid,
165
+ hidden: !visible,
166
+ disabled: !enabled
167
+ };
168
+ const labelNode = f.labelI18n ? /* @__PURE__ */ jsxDEV(DriverLabel, {
169
+ htmlFor: id,
170
+ children: f.labelI18n
171
+ }, undefined, false, undefined, this) : null;
172
+ const descNode = f.descriptionI18n ? /* @__PURE__ */ jsxDEV(DriverDesc, {
173
+ children: f.descriptionI18n
174
+ }, undefined, false, undefined, this) : null;
175
+ if (f.kind === "group") {
176
+ const children = f.fields.map((c, i) => /* @__PURE__ */ jsxDEV(React2.Fragment, {
177
+ children: renderOne(c, name, arrayIndex)
178
+ }, `${name}-${i}`, false, undefined, this));
179
+ return /* @__PURE__ */ jsxDEV(DriverField, {
180
+ ...commonWrapProps,
181
+ children: [
182
+ labelNode,
183
+ children,
184
+ descNode
185
+ ]
186
+ }, undefined, true, undefined, this);
187
+ }
188
+ if (f.kind === "array") {
189
+ return renderArray(f, parent);
190
+ }
191
+ return /* @__PURE__ */ jsxDEV(Controller, {
192
+ name,
193
+ control: form.control,
194
+ render: ({ field, fieldState }) => {
195
+ const err = fieldState.error ? [fieldState.error] : [];
196
+ const ariaInvalid = fieldState.invalid || undefined;
197
+ if (f.kind === "text") {
198
+ const textField = f;
199
+ const Input = driver.Input;
200
+ return /* @__PURE__ */ jsxDEV(DriverField, {
201
+ ...commonWrapProps,
202
+ children: [
203
+ labelNode,
204
+ /* @__PURE__ */ jsxDEV(Input, {
205
+ id,
206
+ "aria-invalid": ariaInvalid,
207
+ placeholder: f.placeholderI18n,
208
+ autoComplete: textField.autoComplete,
209
+ inputMode: textField.inputMode,
210
+ maxLength: textField.maxLength,
211
+ minLength: textField.minLength,
212
+ disabled: !enabled,
213
+ ...field,
214
+ ...f.uiProps
215
+ }, undefined, false, undefined, this),
216
+ descNode,
217
+ fieldState.invalid ? /* @__PURE__ */ jsxDEV(DriverError, {
218
+ errors: err
219
+ }, undefined, false, undefined, this) : null
220
+ ]
221
+ }, undefined, true, undefined, this);
222
+ }
223
+ if (f.kind === "textarea") {
224
+ const textareaField = f;
225
+ const Textarea = driver.Textarea;
226
+ return /* @__PURE__ */ jsxDEV(DriverField, {
227
+ ...commonWrapProps,
228
+ children: [
229
+ labelNode,
230
+ /* @__PURE__ */ jsxDEV(Textarea, {
231
+ id,
232
+ "aria-invalid": ariaInvalid,
233
+ placeholder: f.placeholderI18n,
234
+ rows: textareaField.rows,
235
+ maxLength: textareaField.maxLength,
236
+ disabled: !enabled,
237
+ ...field,
238
+ ...f.uiProps
239
+ }, undefined, false, undefined, this),
240
+ descNode,
241
+ fieldState.invalid ? /* @__PURE__ */ jsxDEV(DriverError, {
242
+ errors: err
243
+ }, undefined, false, undefined, this) : null
244
+ ]
245
+ }, undefined, true, undefined, this);
246
+ }
247
+ if (f.kind === "select") {
248
+ const selectField = f;
249
+ const Select = driver.Select;
250
+ const src = toOptionsArray(selectField.options);
251
+ const opts = useResolvedOptions(values, src, merged.resolvers);
252
+ return /* @__PURE__ */ jsxDEV(DriverField, {
253
+ ...commonWrapProps,
254
+ children: [
255
+ labelNode,
256
+ /* @__PURE__ */ jsxDEV(Select, {
257
+ id,
258
+ name,
259
+ "aria-invalid": ariaInvalid,
260
+ disabled: !enabled,
261
+ value: field.value,
262
+ onChange: (v) => field.onChange(v),
263
+ options: opts,
264
+ ...f.uiProps
265
+ }, undefined, false, undefined, this),
266
+ descNode,
267
+ fieldState.invalid ? /* @__PURE__ */ jsxDEV(DriverError, {
268
+ errors: err
269
+ }, undefined, false, undefined, this) : null
270
+ ]
271
+ }, undefined, true, undefined, this);
272
+ }
273
+ if (f.kind === "checkbox") {
274
+ const Checkbox = driver.Checkbox;
275
+ return /* @__PURE__ */ jsxDEV(DriverField, {
276
+ ...commonWrapProps,
277
+ children: [
278
+ labelNode,
279
+ /* @__PURE__ */ jsxDEV(Checkbox, {
280
+ id,
281
+ name,
282
+ disabled: !enabled,
283
+ checked: !!field.value,
284
+ onCheckedChange: (v) => field.onChange(v),
285
+ ...f.uiProps
286
+ }, undefined, false, undefined, this),
287
+ descNode,
288
+ fieldState.invalid ? /* @__PURE__ */ jsxDEV(DriverError, {
289
+ errors: err
290
+ }, undefined, false, undefined, this) : null
291
+ ]
292
+ }, undefined, true, undefined, this);
293
+ }
294
+ if (f.kind === "radio") {
295
+ const radioField = f;
296
+ const RadioGroup = driver.RadioGroup;
297
+ const src = toOptionsArray(radioField.options);
298
+ const opts = useResolvedOptions(values, src, merged.resolvers);
299
+ return /* @__PURE__ */ jsxDEV(DriverField, {
300
+ ...commonWrapProps,
301
+ children: [
302
+ labelNode,
303
+ /* @__PURE__ */ jsxDEV(RadioGroup, {
304
+ id,
305
+ name,
306
+ disabled: !enabled,
307
+ value: field.value,
308
+ onValueChange: (v) => field.onChange(v),
309
+ options: opts,
310
+ ...f.uiProps
311
+ }, undefined, false, undefined, this),
312
+ descNode,
313
+ fieldState.invalid ? /* @__PURE__ */ jsxDEV(DriverError, {
314
+ errors: err
315
+ }, undefined, false, undefined, this) : null
316
+ ]
317
+ }, undefined, true, undefined, this);
318
+ }
319
+ if (f.kind === "switch") {
320
+ const Switch = driver.Switch;
321
+ return /* @__PURE__ */ jsxDEV(DriverField, {
322
+ ...commonWrapProps,
323
+ children: [
324
+ labelNode,
325
+ /* @__PURE__ */ jsxDEV(Switch, {
326
+ id,
327
+ name,
328
+ disabled: !enabled,
329
+ checked: !!field.value,
330
+ onCheckedChange: (v) => field.onChange(v),
331
+ ...f.uiProps
332
+ }, undefined, false, undefined, this),
333
+ descNode,
334
+ fieldState.invalid ? /* @__PURE__ */ jsxDEV(DriverError, {
335
+ errors: err
336
+ }, undefined, false, undefined, this) : null
337
+ ]
338
+ }, undefined, true, undefined, this);
339
+ }
340
+ return /* @__PURE__ */ jsxDEV(Fragment, {}, undefined, false, undefined, this);
341
+ }
342
+ }, name, false, undefined, this);
343
+ };
344
+ const renderArray = (f, parent) => {
345
+ const name = fieldPath(parent, f.name);
346
+ const { fields, append, remove } = useFieldArray({
347
+ control: form.control,
348
+ name
349
+ });
350
+ const canAdd = f.max == null || fields.length < f.max;
351
+ const canRemove = (idx) => (f.min == null ? fields.length > 0 : fields.length > f.min) && idx >= 0;
352
+ const Button2 = driver.Button;
353
+ const Label = driver.FieldLabel;
354
+ return /* @__PURE__ */ jsxDEV("div", {
355
+ children: [
356
+ f.labelI18n ? /* @__PURE__ */ jsxDEV(Label, {
357
+ children: f.labelI18n
358
+ }, undefined, false, undefined, this) : null,
359
+ fields.map((row, idx) => /* @__PURE__ */ jsxDEV("div", {
360
+ children: [
361
+ renderOne(f.of, name, idx),
362
+ canRemove(idx) ? /* @__PURE__ */ jsxDEV(Button2, {
363
+ type: "button",
364
+ variant: "ghost",
365
+ size: "sm",
366
+ onClick: () => remove(idx),
367
+ children: "Remove"
368
+ }, undefined, false, undefined, this) : null
369
+ ]
370
+ }, row.id ?? idx, true, undefined, this)),
371
+ canAdd ? /* @__PURE__ */ jsxDEV(Button2, {
372
+ type: "button",
373
+ variant: "outline",
374
+ size: "sm",
375
+ onClick: () => append({}),
376
+ children: "Add"
377
+ }, undefined, false, undefined, this) : null
378
+ ]
379
+ }, name, true, undefined, this);
380
+ };
381
+ const onSubmit = async (data) => {
382
+ const actionKey = spec.actions?.[0]?.key ?? "submit";
383
+ if (merged.onSubmitOverride) {
384
+ return merged.onSubmitOverride(data, actionKey);
385
+ }
386
+ };
387
+ const Button = driver.Button;
388
+ return /* @__PURE__ */ jsxDEV("form", {
389
+ onSubmit: form.handleSubmit(onSubmit),
390
+ children: [
391
+ (spec.fields || []).map((f, i) => /* @__PURE__ */ jsxDEV(React2.Fragment, {
392
+ children: renderOne(f)
393
+ }, i, false, undefined, this)),
394
+ spec.actions && spec.actions.length ? /* @__PURE__ */ jsxDEV("div", {
395
+ children: spec.actions.map((a) => /* @__PURE__ */ jsxDEV(Button, {
396
+ type: "submit",
397
+ children: a.labelI18n
398
+ }, a.key, false, undefined, this))
399
+ }, undefined, false, undefined, this) : null
400
+ ]
401
+ }, undefined, true, undefined, this);
402
+ }
403
+ return {
404
+ render: (spec, options) => /* @__PURE__ */ jsxDEV(InternalForm, {
405
+ spec,
406
+ options,
407
+ merged: {
408
+ ...conf,
409
+ ...options?.overrides ?? {}
410
+ }
411
+ }, undefined, false, undefined, this)
412
+ };
413
+ }
414
+ // src/index.ts
415
+ "use client";
416
+ export {
417
+ shadcnDriver,
418
+ rnReusablesDriver,
419
+ renderFeaturePresentation,
420
+ registerFeature,
421
+ createFormRenderer,
422
+ createFeatureModule,
423
+ createEngineWithDefaults
424
+ };
package/package.json ADDED
@@ -0,0 +1,133 @@
1
+ {
2
+ "name": "@contractspec/lib.contracts-runtime-client-react",
3
+ "version": "2.0.0",
4
+ "description": "React runtime adapters for ContractSpec contracts",
5
+ "type": "module",
6
+ "types": "./dist/index.d.ts",
7
+ "scripts": {
8
+ "publish:pkg": "bun publish --tolerate-republish --ignore-scripts --verbose",
9
+ "publish:pkg:canary": "bun publish:pkg --tag canary",
10
+ "clean": "rm -rf dist",
11
+ "lint": "bun run lint:fix",
12
+ "lint:fix": "eslint src --fix",
13
+ "lint:check": "eslint src",
14
+ "build": "bun run prebuild && bun run build:bundle && bun run build:types",
15
+ "build:bundle": "contractspec-bun-build transpile",
16
+ "build:types": "contractspec-bun-build types",
17
+ "prebuild": "contractspec-bun-build prebuild",
18
+ "typecheck": "tsc --noEmit",
19
+ "dev": "contractspec-bun-build dev"
20
+ },
21
+ "peerDependencies": {
22
+ "@blocknote/core": "^0.46.1",
23
+ "@hookform/resolvers": "^5.2.2",
24
+ "react": "^19.2.4",
25
+ "react-dom": "^19.2.4",
26
+ "react-hook-form": "^7.70.0"
27
+ },
28
+ "dependencies": {
29
+ "@contractspec/lib.contracts-spec": "2.0.0",
30
+ "@contractspec/lib.schema": "2.0.0"
31
+ },
32
+ "devDependencies": {
33
+ "@contractspec/tool.typescript": "2.0.0",
34
+ "typescript": "^5.9.3",
35
+ "@contractspec/tool.bun": "2.0.0"
36
+ },
37
+ "files": [
38
+ "dist",
39
+ "README.md"
40
+ ],
41
+ "exports": {
42
+ ".": {
43
+ "types": "./dist/index.d.ts",
44
+ "bun": "./dist/index.js",
45
+ "node": "./dist/node/index.js",
46
+ "browser": "./dist/browser/index.js",
47
+ "default": "./dist/index.js"
48
+ },
49
+ "./drivers/rn-reusables": {
50
+ "types": "./dist/drivers/rn-reusables.d.ts",
51
+ "bun": "./dist/drivers/rn-reusables.js",
52
+ "node": "./dist/node/drivers/rn-reusables.js",
53
+ "browser": "./dist/browser/drivers/rn-reusables.js",
54
+ "default": "./dist/drivers/rn-reusables.js"
55
+ },
56
+ "./drivers/shadcn": {
57
+ "types": "./dist/drivers/shadcn.d.ts",
58
+ "bun": "./dist/drivers/shadcn.js",
59
+ "node": "./dist/node/drivers/shadcn.js",
60
+ "browser": "./dist/browser/drivers/shadcn.js",
61
+ "default": "./dist/drivers/shadcn.js"
62
+ },
63
+ "./feature-render": {
64
+ "types": "./dist/feature-render.d.ts",
65
+ "bun": "./dist/feature-render.js",
66
+ "node": "./dist/node/feature-render.js",
67
+ "browser": "./dist/browser/feature-render.js",
68
+ "default": "./dist/feature-render.js"
69
+ },
70
+ "./form-render": {
71
+ "types": "./dist/form-render.d.ts",
72
+ "bun": "./dist/form-render.js",
73
+ "node": "./dist/node/form-render.js",
74
+ "browser": "./dist/browser/form-render.js",
75
+ "default": "./dist/form-render.js"
76
+ },
77
+ "./form-render.impl": {
78
+ "types": "./dist/form-render.impl.d.ts",
79
+ "bun": "./dist/form-render.impl.js",
80
+ "node": "./dist/node/form-render.impl.js",
81
+ "browser": "./dist/browser/form-render.impl.js",
82
+ "default": "./dist/form-render.impl.js"
83
+ }
84
+ },
85
+ "publishConfig": {
86
+ "access": "public",
87
+ "registry": "https://registry.npmjs.org/",
88
+ "exports": {
89
+ ".": {
90
+ "types": "./dist/index.d.ts",
91
+ "bun": "./dist/index.js",
92
+ "node": "./dist/node/index.js",
93
+ "browser": "./dist/browser/index.js",
94
+ "default": "./dist/index.js"
95
+ },
96
+ "./drivers/rn-reusables": {
97
+ "types": "./dist/drivers/rn-reusables.d.ts",
98
+ "bun": "./dist/drivers/rn-reusables.js",
99
+ "node": "./dist/node/drivers/rn-reusables.js",
100
+ "browser": "./dist/browser/drivers/rn-reusables.js",
101
+ "default": "./dist/drivers/rn-reusables.js"
102
+ },
103
+ "./drivers/shadcn": {
104
+ "types": "./dist/drivers/shadcn.d.ts",
105
+ "bun": "./dist/drivers/shadcn.js",
106
+ "node": "./dist/node/drivers/shadcn.js",
107
+ "browser": "./dist/browser/drivers/shadcn.js",
108
+ "default": "./dist/drivers/shadcn.js"
109
+ },
110
+ "./feature-render": {
111
+ "types": "./dist/feature-render.d.ts",
112
+ "bun": "./dist/feature-render.js",
113
+ "node": "./dist/node/feature-render.js",
114
+ "browser": "./dist/browser/feature-render.js",
115
+ "default": "./dist/feature-render.js"
116
+ },
117
+ "./form-render": {
118
+ "types": "./dist/form-render.d.ts",
119
+ "bun": "./dist/form-render.js",
120
+ "node": "./dist/node/form-render.js",
121
+ "browser": "./dist/browser/form-render.js",
122
+ "default": "./dist/form-render.js"
123
+ },
124
+ "./form-render.impl": {
125
+ "types": "./dist/form-render.impl.d.ts",
126
+ "bun": "./dist/form-render.impl.js",
127
+ "node": "./dist/node/form-render.impl.js",
128
+ "browser": "./dist/browser/form-render.impl.js",
129
+ "default": "./dist/form-render.impl.js"
130
+ }
131
+ }
132
+ }
133
+ }