@petrarca/sonnet-forms 0.2.0 → 0.4.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 +259 -6
- package/dist/index.d.ts +12 -10
- package/dist/index.js +14 -6
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
# @petrarca/sonnet-forms
|
|
2
2
|
|
|
3
|
-
Schema-driven form renderer and field components for the Petrarca Sonnet
|
|
3
|
+
Schema-driven form renderer and field components for the Petrarca Sonnet
|
|
4
|
+
component library.
|
|
4
5
|
|
|
5
6
|
## What's included
|
|
6
7
|
|
|
7
|
-
**JsonSchemaFormRenderer**
|
|
8
|
+
**JsonSchemaFormRenderer** — Renders a complete form from a JSON Schema with
|
|
9
|
+
automatic widget resolution, validation, nested objects, arrays, and custom
|
|
10
|
+
UI schema overrides.
|
|
8
11
|
|
|
9
|
-
**
|
|
12
|
+
**Form field components** — Standalone fields with label, description, error,
|
|
13
|
+
and compact mode: `FormInput`, `FormTextarea`, `FormSelect`, `FormMultiSelect`,
|
|
14
|
+
`FormCheckbox`, `FormTagsInput`, `FormNumberInput`, `FormQuantityInput`.
|
|
10
15
|
|
|
11
|
-
**Widget system**
|
|
16
|
+
**Widget system** — Extensible widget registry mapping schema types/formats to
|
|
17
|
+
UI controls. Built-in widgets for text, number, select, checkbox, textarea,
|
|
18
|
+
tags, quantity, arrays, objects, entity references, and JSON editing.
|
|
19
|
+
|
|
20
|
+
---
|
|
12
21
|
|
|
13
22
|
## Install
|
|
14
23
|
|
|
@@ -16,8 +25,252 @@ Schema-driven form renderer and field components for the Petrarca Sonnet compone
|
|
|
16
25
|
pnpm add @petrarca/sonnet-forms @petrarca/sonnet-ui @petrarca/sonnet-core
|
|
17
26
|
```
|
|
18
27
|
|
|
19
|
-
Peer dependencies: `react`, `react-dom`, `tailwindcss`.
|
|
28
|
+
Peer dependencies: `react >=19`, `react-dom >=19`, `tailwindcss`.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Basic usage
|
|
33
|
+
|
|
34
|
+
### JsonSchemaFormRenderer
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
import { JsonSchemaFormRenderer } from "@petrarca/sonnet-forms";
|
|
38
|
+
|
|
39
|
+
const schema = {
|
|
40
|
+
type: "object",
|
|
41
|
+
properties: {
|
|
42
|
+
name: { type: "string", title: "Name" },
|
|
43
|
+
email: { type: "string", format: "email", title: "Email" },
|
|
44
|
+
age: { type: "integer", title: "Age" },
|
|
45
|
+
},
|
|
46
|
+
required: ["name", "email"],
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
<JsonSchemaFormRenderer
|
|
50
|
+
schema={schema}
|
|
51
|
+
data={formData}
|
|
52
|
+
onUpdate={(data, changedFields) => save(data)}
|
|
53
|
+
onCancel={handleClose}
|
|
54
|
+
showActions
|
|
55
|
+
saveLabel="Save"
|
|
56
|
+
/>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Key props
|
|
60
|
+
|
|
61
|
+
| Prop | Type | Default | Purpose |
|
|
62
|
+
|---|---|---|---|
|
|
63
|
+
| `schema` | `JsonSchema` | required | JSON Schema describing the form |
|
|
64
|
+
| `data` | `Record<string, unknown>` | required | Current form values |
|
|
65
|
+
| `onUpdate` | `(data, changedFields, diff) => void` | — | Called on save |
|
|
66
|
+
| `onCancel` | `() => void` | — | Called on cancel |
|
|
67
|
+
| `onChange` | `(data) => void` | — | Called on every field change (use instead of `onUpdate` when managing your own buttons) |
|
|
68
|
+
| `showActions` | `boolean` | `false` | Render built-in Save / Cancel buttons inside the form |
|
|
69
|
+
| `showCancel` | `boolean` | `true` | Show the Cancel button (requires `showActions`) |
|
|
70
|
+
| `saveLabel` | `string` | `"Save"` | Save button label |
|
|
71
|
+
| `cancelLabel` | `string` | `"Cancel"` | Cancel button label |
|
|
72
|
+
| `showExtraProperties` | `boolean` | `false` | Preserve form keys not in the schema. Set `true` for open-ended data (e.g. graph node properties) |
|
|
73
|
+
| `uiSchema` | `Record<string, UISchema>` | — | Per-field UI overrides |
|
|
74
|
+
| `widgets` | `WidgetRegistry` | `DEFAULT_WIDGETS` | Custom widget map |
|
|
75
|
+
| `deferStateUpdates` | `boolean` | `false` | Update state on blur instead of on change |
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Schema construction
|
|
80
|
+
|
|
81
|
+
The recommended pattern is Zod + `toJsonSchema()` with `.meta()` for UI hints:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import { z } from "zod";
|
|
85
|
+
import { toJsonSchema } from "@petrarca/sonnet-core/schema";
|
|
86
|
+
|
|
87
|
+
const UserSchema = z.object({
|
|
88
|
+
name: z.string().meta({
|
|
89
|
+
"x-ui-title": "Full name",
|
|
90
|
+
"x-ui-description": "As it appears on official documents.",
|
|
91
|
+
}),
|
|
92
|
+
role: z.enum(["admin", "user"]).meta({
|
|
93
|
+
"x-ui-title": "Role",
|
|
94
|
+
"x-ui-options": { enumNames: ["Administrator", "Standard user"] },
|
|
95
|
+
}),
|
|
96
|
+
bio: z.string().optional().meta({
|
|
97
|
+
"x-ui-title": "Bio",
|
|
98
|
+
"x-ui-widget": "textarea",
|
|
99
|
+
}),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Convert once, store as a module-level constant:
|
|
103
|
+
const USER_FORM_SCHEMA = toJsonSchema(UserSchema);
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Or write the JSON Schema directly for dynamic / server-driven forms.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Schema UI annotations
|
|
111
|
+
|
|
112
|
+
Annotate individual fields with `x-ui-*` to control rendering:
|
|
113
|
+
|
|
114
|
+
| Annotation | Purpose |
|
|
115
|
+
|---|---|
|
|
116
|
+
| `"x-ui-title"` | Override the field label (`false` to hide) |
|
|
117
|
+
| `"x-ui-description"` | Override the field description |
|
|
118
|
+
| `"x-ui-widget"` | Force a specific widget: `"textarea"`, `"tags"`, `"entity-select"`, or a custom key |
|
|
119
|
+
| `"x-ui-options"` | Widget-specific options (see widget docs) |
|
|
120
|
+
| `"x-ui-order"` | Array of field names to control rendering order in an object |
|
|
121
|
+
| `"x-ui-layout"` | Array layout config: `{ direction: "horizontal", columns, gap, compact }` |
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## onChange mode (external buttons)
|
|
126
|
+
|
|
127
|
+
When you want to drive your own Submit button outside the form, omit `showActions`
|
|
128
|
+
and use `onChange` instead of `onUpdate`:
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
const [formData, setFormData] = useState({});
|
|
132
|
+
|
|
133
|
+
<JsonSchemaFormRenderer
|
|
134
|
+
schema={schema}
|
|
135
|
+
data={formData}
|
|
136
|
+
onChange={setFormData}
|
|
137
|
+
// no showActions — caller owns the buttons
|
|
138
|
+
/>
|
|
139
|
+
|
|
140
|
+
<Button onClick={() => submit(formData)}>Submit</Button>
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## UISchema
|
|
146
|
+
|
|
147
|
+
Override rendering per-field without modifying the schema:
|
|
148
|
+
|
|
149
|
+
```tsx
|
|
150
|
+
<JsonSchemaFormRenderer
|
|
151
|
+
schema={schema}
|
|
152
|
+
data={data}
|
|
153
|
+
uiSchema={{
|
|
154
|
+
name: { "x-ui-title": "Display name" }, // override label
|
|
155
|
+
internal_id: { "x-ui-title": false }, // hide label entirely
|
|
156
|
+
notes: { "x-ui-widget": "textarea" }, // force widget
|
|
157
|
+
}}
|
|
158
|
+
onUpdate={onUpdate}
|
|
159
|
+
showActions
|
|
160
|
+
/>
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Custom widgets
|
|
166
|
+
|
|
167
|
+
Extend `DEFAULT_WIDGETS` with your own widget components:
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
import { DEFAULT_WIDGETS, type WidgetRegistry, type WidgetProps } from "@petrarca/sonnet-forms";
|
|
171
|
+
|
|
172
|
+
function MyColorWidget({ value, onChange, schema }: WidgetProps) {
|
|
173
|
+
return (
|
|
174
|
+
<input
|
|
175
|
+
type="color"
|
|
176
|
+
value={String(value ?? "#000000")}
|
|
177
|
+
onChange={(e) => onChange(e.target.value)}
|
|
178
|
+
/>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export const MY_WIDGETS: WidgetRegistry = {
|
|
183
|
+
...DEFAULT_WIDGETS,
|
|
184
|
+
"color-picker": MyColorWidget, // key matches "x-ui-widget": "color-picker" in schema
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// Usage:
|
|
188
|
+
<JsonSchemaFormRenderer schema={schema} data={data} widgets={MY_WIDGETS} onUpdate={onUpdate} showActions />
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
`WidgetProps` includes `formData` (the full current form state) — use this to
|
|
192
|
+
build dependent widgets that react to sibling field values.
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Dynamic schema (edit vs. create)
|
|
197
|
+
|
|
198
|
+
To hide fields that are immutable after creation, delete them from a cloned
|
|
199
|
+
schema. Also exclude them from `data` — if a key exists in `data` but not
|
|
200
|
+
the schema, the form engine treats it as a dirty field immediately:
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
const schema = useMemo(() => {
|
|
204
|
+
if (!isEditing) return BASE_SCHEMA;
|
|
205
|
+
const s = structuredClone(BASE_SCHEMA);
|
|
206
|
+
delete s.properties?.app_key; // read-only after creation
|
|
207
|
+
return s;
|
|
208
|
+
}, [isEditing]);
|
|
209
|
+
|
|
210
|
+
const data = isEditing
|
|
211
|
+
? { name: entity.name } // omit app_key from data too
|
|
212
|
+
: {};
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Standalone field components
|
|
218
|
+
|
|
219
|
+
Use field components directly outside the renderer — in sidebars, toolbars,
|
|
220
|
+
or any custom form layout:
|
|
221
|
+
|
|
222
|
+
```tsx
|
|
223
|
+
import {
|
|
224
|
+
FormInput, FormTextarea, FormSelect,
|
|
225
|
+
FormCheckbox, FormMultiSelect, FormTagsInput,
|
|
226
|
+
} from "@petrarca/sonnet-forms";
|
|
227
|
+
|
|
228
|
+
<FormInput
|
|
229
|
+
label="Name"
|
|
230
|
+
description="Your full name"
|
|
231
|
+
value={name}
|
|
232
|
+
onChange={(e) => setName(e.currentTarget.value)}
|
|
233
|
+
error={nameError}
|
|
234
|
+
/>
|
|
235
|
+
|
|
236
|
+
<FormSelect
|
|
237
|
+
label="Role"
|
|
238
|
+
options={[{ value: "admin", label: "Admin" }, { value: "user", label: "User" }]}
|
|
239
|
+
value={role}
|
|
240
|
+
onChange={setRole}
|
|
241
|
+
clearable={false}
|
|
242
|
+
/>
|
|
243
|
+
|
|
244
|
+
<FormCheckbox
|
|
245
|
+
label="Active"
|
|
246
|
+
checked={active}
|
|
247
|
+
onChange={(checked) => setActive(checked === true)}
|
|
248
|
+
/>
|
|
249
|
+
|
|
250
|
+
<FormTextarea
|
|
251
|
+
label="Notes"
|
|
252
|
+
value={notes}
|
|
253
|
+
onChange={(e) => setNotes(e.currentTarget.value)}
|
|
254
|
+
autosize
|
|
255
|
+
minRows={3}
|
|
256
|
+
rightSection={<ClearButton />}
|
|
257
|
+
rightSectionWidth={40}
|
|
258
|
+
/>
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
**Common props on all field components:**
|
|
262
|
+
|
|
263
|
+
| Prop | Purpose |
|
|
264
|
+
|---|---|
|
|
265
|
+
| `label` | Field label. Pass `false` to suppress. |
|
|
266
|
+
| `description` | Help text below the field |
|
|
267
|
+
| `error` | Error message (also sets error styling) |
|
|
268
|
+
| `compact` | Tighter vertical spacing |
|
|
269
|
+
| `disabled` | Disables the field |
|
|
270
|
+
| `wrapperClassName` | Class on the outer wrapper div |
|
|
271
|
+
|
|
272
|
+
---
|
|
20
273
|
|
|
21
274
|
## License
|
|
22
275
|
|
|
23
|
-
|
|
276
|
+
See [LICENSE.md](../../LICENSE.md).
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import * as React$1 from 'react';
|
|
3
|
-
import React__default, { ComponentType } from 'react';
|
|
3
|
+
import React__default, { ComponentType, ReactNode } from 'react';
|
|
4
4
|
import { Input, Textarea, badgeVariants } from '@petrarca/sonnet-ui';
|
|
5
5
|
import { JsonSchemaProperty, UILayoutConfig, JsonSchema, UISchema, UISchemaOptions, FormDiff } from '@petrarca/sonnet-core/schema';
|
|
6
6
|
export { UISchema, UISchemaOptions } from '@petrarca/sonnet-core/schema';
|
|
@@ -591,14 +591,6 @@ interface FormQuantityInputProps {
|
|
|
591
591
|
*/
|
|
592
592
|
declare const FormQuantityInput: React__default.ForwardRefExoticComponent<FormQuantityInputProps & React__default.RefAttributes<HTMLInputElement>>;
|
|
593
593
|
|
|
594
|
-
/**
|
|
595
|
-
* FormActions - Save / Cancel button row for buffered forms
|
|
596
|
-
*
|
|
597
|
-
* Rendered only when the parent JsonSchemaFormRenderer is in buffered mode
|
|
598
|
-
* (showActions=true). Keeps button logic out of the orchestrator.
|
|
599
|
-
*
|
|
600
|
-
* @module components/Forms/FormActions
|
|
601
|
-
*/
|
|
602
594
|
interface FormActionsProps {
|
|
603
595
|
onSave: () => void;
|
|
604
596
|
onCancel: () => void;
|
|
@@ -608,8 +600,13 @@ interface FormActionsProps {
|
|
|
608
600
|
saveLabel?: string;
|
|
609
601
|
cancelLabel?: string;
|
|
610
602
|
showCancel?: boolean;
|
|
603
|
+
/**
|
|
604
|
+
* Optional content rendered left-aligned in the action row (e.g. a secondary
|
|
605
|
+
* action like "Test connection"). Cancel/Save stay grouped on the right.
|
|
606
|
+
*/
|
|
607
|
+
leadingActions?: ReactNode;
|
|
611
608
|
}
|
|
612
|
-
declare function FormActions({ onSave, onCancel, disabled, hasChanges, isValid, saveLabel, cancelLabel, showCancel, }: FormActionsProps): react_jsx_runtime.JSX.Element;
|
|
609
|
+
declare function FormActions({ onSave, onCancel, disabled, hasChanges, isValid, saveLabel, cancelLabel, showCancel, leadingActions, }: FormActionsProps): react_jsx_runtime.JSX.Element;
|
|
613
610
|
|
|
614
611
|
type Props = {
|
|
615
612
|
properties: Record<string, any>;
|
|
@@ -944,6 +941,11 @@ type JsonSchemaFormRendererProps = {
|
|
|
944
941
|
cancelLabel?: string;
|
|
945
942
|
/** Show the Cancel button (default: true) */
|
|
946
943
|
showCancel?: boolean;
|
|
944
|
+
/**
|
|
945
|
+
* Optional content rendered left-aligned in the Save/Cancel action row
|
|
946
|
+
* (only when showActions=true), e.g. a secondary action like "Test connection".
|
|
947
|
+
*/
|
|
948
|
+
leadingActions?: React__default.ReactNode;
|
|
947
949
|
/** Show extra properties card (default: true) */
|
|
948
950
|
showExtraProperties?: boolean;
|
|
949
951
|
/** Widget registry for custom widgets (default: DEFAULT_WIDGETS) */
|
package/dist/index.js
CHANGED
|
@@ -1417,11 +1417,15 @@ function FormActions({
|
|
|
1417
1417
|
isValid,
|
|
1418
1418
|
saveLabel = "Save",
|
|
1419
1419
|
cancelLabel = "Cancel",
|
|
1420
|
-
showCancel = true
|
|
1420
|
+
showCancel = true,
|
|
1421
|
+
leadingActions
|
|
1421
1422
|
}) {
|
|
1422
|
-
return /* @__PURE__ */ jsxs9(
|
|
1423
|
-
|
|
1424
|
-
/* @__PURE__ */
|
|
1423
|
+
return /* @__PURE__ */ jsxs9("div", { className: "flex items-center justify-between mt-4", children: [
|
|
1424
|
+
/* @__PURE__ */ jsx10("div", { className: "flex items-center gap-2", children: leadingActions }),
|
|
1425
|
+
/* @__PURE__ */ jsxs9(SimpleGroup, { justify: "flex-end", children: [
|
|
1426
|
+
showCancel && /* @__PURE__ */ jsx10(Button3, { variant: "outline", onClick: onCancel, disabled, children: cancelLabel }),
|
|
1427
|
+
/* @__PURE__ */ jsx10(Button3, { onClick: onSave, disabled: disabled || !hasChanges || !isValid, children: saveLabel })
|
|
1428
|
+
] })
|
|
1425
1429
|
] });
|
|
1426
1430
|
}
|
|
1427
1431
|
|
|
@@ -3323,6 +3327,7 @@ function FormContainer({
|
|
|
3323
3327
|
saveLabel,
|
|
3324
3328
|
cancelLabel,
|
|
3325
3329
|
showCancel,
|
|
3330
|
+
leadingActions,
|
|
3326
3331
|
children
|
|
3327
3332
|
}) {
|
|
3328
3333
|
if (displayContents) {
|
|
@@ -3341,7 +3346,8 @@ function FormContainer({
|
|
|
3341
3346
|
isValid,
|
|
3342
3347
|
saveLabel,
|
|
3343
3348
|
cancelLabel,
|
|
3344
|
-
showCancel
|
|
3349
|
+
showCancel,
|
|
3350
|
+
leadingActions
|
|
3345
3351
|
}
|
|
3346
3352
|
)
|
|
3347
3353
|
] });
|
|
@@ -3355,7 +3361,8 @@ function JsonSchemaFormRenderer(props) {
|
|
|
3355
3361
|
onValidationChange,
|
|
3356
3362
|
onUpdate,
|
|
3357
3363
|
onCancel,
|
|
3358
|
-
templates
|
|
3364
|
+
templates,
|
|
3365
|
+
leadingActions
|
|
3359
3366
|
} = props;
|
|
3360
3367
|
const {
|
|
3361
3368
|
disabled,
|
|
@@ -3458,6 +3465,7 @@ function JsonSchemaFormRenderer(props) {
|
|
|
3458
3465
|
saveLabel,
|
|
3459
3466
|
cancelLabel,
|
|
3460
3467
|
showCancel,
|
|
3468
|
+
leadingActions,
|
|
3461
3469
|
children: layoutFields
|
|
3462
3470
|
}
|
|
3463
3471
|
) });
|