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