npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@petrarca/sonnet-forms

v0.4.2

Published

Schema-driven form renderer, field components, and widgets for the Petrarca Sonnet component library

Downloads

633

Readme

@petrarca/sonnet-forms

Schema-driven form renderer and field components for the Petrarca Sonnet component library.

What's included

JsonSchemaFormRenderer — Renders a complete form from a JSON Schema with automatic widget resolution, validation, nested objects, arrays, and custom UI schema overrides.

Form field components — Standalone fields with label, description, error, and compact mode: FormInput, FormTextarea, FormSelect, FormMultiSelect, FormCheckbox, FormTagsInput, FormNumberInput, FormQuantityInput.

Widget system — Extensible widget registry mapping schema types/formats to UI controls. Built-in widgets for text, number, select, checkbox, textarea, tags, quantity, arrays, objects, entity references, and JSON editing.


Install

pnpm add @petrarca/sonnet-forms @petrarca/sonnet-ui @petrarca/sonnet-core

Peer dependencies: react >=19, react-dom >=19, tailwindcss.


Basic usage

JsonSchemaFormRenderer

import { JsonSchemaFormRenderer } from "@petrarca/sonnet-forms";

const schema = {
  type: "object",
  properties: {
    name:  { type: "string", title: "Name" },
    email: { type: "string", format: "email", title: "Email" },
    age:   { type: "integer", title: "Age" },
  },
  required: ["name", "email"],
};

<JsonSchemaFormRenderer
  schema={schema}
  data={formData}
  onUpdate={(data, changedFields) => save(data)}
  onCancel={handleClose}
  showActions
  saveLabel="Save"
/>

Key props

| Prop | Type | Default | Purpose | |---|---|---|---| | schema | JsonSchema | required | JSON Schema describing the form | | data | Record<string, unknown> | required | Current form values | | onUpdate | (data, changedFields, diff) => void | — | Called on save | | onCancel | () => void | — | Called on cancel | | onChange | (data) => void | — | Called on every field change (use instead of onUpdate when managing your own buttons) | | showActions | boolean | false | Render built-in Save / Cancel buttons inside the form | | showCancel | boolean | true | Show the Cancel button (requires showActions) | | saveLabel | string | "Save" | Save button label | | cancelLabel | string | "Cancel" | Cancel button label | | showExtraProperties | boolean | false | Preserve form keys not in the schema. Set true for open-ended data (e.g. graph node properties) | | uiSchema | Record<string, UISchema> | — | Per-field UI overrides | | widgets | WidgetRegistry | DEFAULT_WIDGETS | Custom widget map | | deferStateUpdates | boolean | false | Update state on blur instead of on change |


Schema construction

The recommended pattern is Zod + toJsonSchema() with .meta() for UI hints:

import { z } from "zod";
import { toJsonSchema } from "@petrarca/sonnet-core/schema";

const UserSchema = z.object({
  name: z.string().meta({
    "x-ui-title": "Full name",
    "x-ui-description": "As it appears on official documents.",
  }),
  role: z.enum(["admin", "user"]).meta({
    "x-ui-title": "Role",
    "x-ui-options": { enumNames: ["Administrator", "Standard user"] },
  }),
  bio: z.string().optional().meta({
    "x-ui-title": "Bio",
    "x-ui-widget": "textarea",
  }),
});

// Convert once, store as a module-level constant:
const USER_FORM_SCHEMA = toJsonSchema(UserSchema);

Or write the JSON Schema directly for dynamic / server-driven forms.


Schema UI annotations

Annotate individual fields with x-ui-* to control rendering:

| Annotation | Purpose | |---|---| | "x-ui-title" | Override the field label (false to hide) | | "x-ui-description" | Override the field description | | "x-ui-widget" | Force a specific widget: "textarea", "tags", "entity-select", or a custom key | | "x-ui-options" | Widget-specific options (see widget docs) | | "x-ui-order" | Array of field names to control rendering order in an object | | "x-ui-layout" | Array layout config: { direction: "horizontal", columns, gap, compact } |


onChange mode (external buttons)

When you want to drive your own Submit button outside the form, omit showActions and use onChange instead of onUpdate:

const [formData, setFormData] = useState({});

<JsonSchemaFormRenderer
  schema={schema}
  data={formData}
  onChange={setFormData}
  // no showActions — caller owns the buttons
/>

<Button onClick={() => submit(formData)}>Submit</Button>

UISchema

Override rendering per-field without modifying the schema:

<JsonSchemaFormRenderer
  schema={schema}
  data={data}
  uiSchema={{
    name:        { "x-ui-title": "Display name" },   // override label
    internal_id: { "x-ui-title": false },             // hide label entirely
    notes:       { "x-ui-widget": "textarea" },       // force widget
  }}
  onUpdate={onUpdate}
  showActions
/>

Custom widgets

Extend DEFAULT_WIDGETS with your own widget components:

import { DEFAULT_WIDGETS, type WidgetRegistry, type WidgetProps } from "@petrarca/sonnet-forms";

function MyColorWidget({ value, onChange, schema }: WidgetProps) {
  return (
    <input
      type="color"
      value={String(value ?? "#000000")}
      onChange={(e) => onChange(e.target.value)}
    />
  );
}

export const MY_WIDGETS: WidgetRegistry = {
  ...DEFAULT_WIDGETS,
  "color-picker": MyColorWidget,   // key matches "x-ui-widget": "color-picker" in schema
};

// Usage:
<JsonSchemaFormRenderer schema={schema} data={data} widgets={MY_WIDGETS} onUpdate={onUpdate} showActions />

WidgetProps includes formData (the full current form state) — use this to build dependent widgets that react to sibling field values.


Dynamic schema (edit vs. create)

To hide fields that are immutable after creation, delete them from a cloned schema. Also exclude them from data — if a key exists in data but not the schema, the form engine treats it as a dirty field immediately:

const schema = useMemo(() => {
  if (!isEditing) return BASE_SCHEMA;
  const s = structuredClone(BASE_SCHEMA);
  delete s.properties?.app_key;   // read-only after creation
  return s;
}, [isEditing]);

const data = isEditing
  ? { name: entity.name }          // omit app_key from data too
  : {};

Standalone field components

Use field components directly outside the renderer — in sidebars, toolbars, or any custom form layout:

import {
  FormInput, FormTextarea, FormSelect,
  FormCheckbox, FormMultiSelect, FormTagsInput,
} from "@petrarca/sonnet-forms";

<FormInput
  label="Name"
  description="Your full name"
  value={name}
  onChange={(e) => setName(e.currentTarget.value)}
  error={nameError}
/>

<FormSelect
  label="Role"
  options={[{ value: "admin", label: "Admin" }, { value: "user", label: "User" }]}
  value={role}
  onChange={setRole}
  clearable={false}
/>

<FormCheckbox
  label="Active"
  checked={active}
  onChange={(checked) => setActive(checked === true)}
/>

<FormTextarea
  label="Notes"
  value={notes}
  onChange={(e) => setNotes(e.currentTarget.value)}
  autosize
  minRows={3}
  rightSection={<ClearButton />}
  rightSectionWidth={40}
/>

Common props on all field components:

| Prop | Purpose | |---|---| | label | Field label. Pass false to suppress. | | description | Help text below the field | | error | Error message (also sets error styling) | | compact | Tighter vertical spacing | | disabled | Disables the field | | wrapperClassName | Class on the outer wrapper div |


License

See LICENSE.md.