@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 CHANGED
@@ -1,14 +1,23 @@
1
1
  # @petrarca/sonnet-forms
2
2
 
3
- Schema-driven form renderer and field components for the Petrarca Sonnet component library.
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** -- Renders a complete form from a JSON Schema definition with automatic widget resolution, validation, nested objects, and repeatable arrays.
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
- **Field components** -- Standalone form fields with label, description, error, and compact mode support: FormInput, FormTextarea, FormSelect, FormMultiSelect, FormCheckbox, FormTagsInput, FormQuantityInput.
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** -- Extensible widget registry for mapping schema types to UI controls. Built-in widgets for text, number, select, checkbox, textarea, tags, quantity, arrays, objects, entity references, and JSON editing.
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
- Apache 2.0
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(SimpleGroup, { justify: "flex-end", mt: "md", children: [
1423
- showCancel && /* @__PURE__ */ jsx10(Button3, { variant: "outline", onClick: onCancel, disabled, children: cancelLabel }),
1424
- /* @__PURE__ */ jsx10(Button3, { onClick: onSave, disabled: disabled || !hasChanges || !isValid, children: saveLabel })
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
  ) });