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

@alphinex/forms

v1.4.1

Published

Form Engine: FormProvider, Zod/RHF integration, field components, dynamic form builder.

Readme

@alphinex/forms

The platform's form engine: a thin wrapper over React Hook Form with Zod as the single source of validation truth, a matched set of typed field components, and buildFormFromSchema for rendering forms from a JSON-serializable definition (e.g. per-tenant custom fields configured outside the codebase). Every field component reads its value/error state from RHF's context — nothing is passed to them explicitly beyond a name.

useAppForm + Form

useAppForm is the one way to create a form in this platform: it's RHF's useForm pre-wired to a Zod schema via zodResolver, so validation logic never has to be duplicated between a schema and ad hoc RHF rules. Form wraps RHF's own FormProvider with a real <form> element bound to handleSubmit:

import { Form, useAppForm, TextField, NumberField } from "@alphinex/forms";
import { z } from "zod";

const schema = z.object({
  name: z.string().min(1, "Name is required"),
  age: z.coerce.number().min(0),
});

function ProfileForm() {
  const form = useAppForm({
    schema,
    defaultValues: { name: "", age: 0 },
  });

  return (
    <Form form={form} onSubmit={(values) => console.log(values)}>
      <TextField name="name" label="Name" required />
      <NumberField name="age" label="Age" description="In years" />
      <button type="submit">Save</button>
    </Form>
  );
}

useAppForm's options are RHF's UseFormProps (defaultValues, mode, ...) plus schema — every other RHF option (resolver aside, which is set for you) passes through unchanged.

Field components

TextField, NumberField, TextareaField, CheckboxField, SwitchField, SelectField, and DateField all take a name (matching a key in your Zod schema/defaultValues), plus optional label, description, and required. They must render inside a <Form> (or anything wrapping RHF's FormProvider) since they call useController({ name }) internally. Everything else spreads onto the underlying @alphinex/ui control, so e.g. TextField also accepts placeholder, Select's props flow through SelectField, and so on.

import {
  Form,
  useAppForm,
  TextField,
  TextareaField,
  CheckboxField,
  SwitchField,
  SelectField,
  DateField,
} from "@alphinex/forms";
import { z } from "zod";

const schema = z.object({
  title: z.string().min(1),
  notes: z.string().optional(),
  isUrgent: z.boolean(),
  notifyByEmail: z.boolean(),
  priority: z.string(),
  dueDate: z.string(),
});

function TaskForm() {
  const form = useAppForm({
    schema,
    defaultValues: {
      title: "",
      notes: "",
      isUrgent: false,
      notifyByEmail: true,
      priority: "normal",
      dueDate: "",
    },
  });

  return (
    <Form form={form} onSubmit={(values) => console.log(values)}>
      <TextField name="title" label="Title" required />
      <TextareaField name="notes" label="Notes" />
      <CheckboxField name="isUrgent" label="Urgent" />
      <SwitchField name="notifyByEmail" label="Email me updates" />
      <SelectField
        name="priority"
        label="Priority"
        options={[
          { value: "low", label: "Low" },
          { value: "normal", label: "Normal" },
          { value: "high", label: "High" },
        ]}
      />
      <DateField name="dueDate" label="Due date" />
      <button type="submit">Create task</button>
    </Form>
  );
}

NumberField renders <input type="number"> bound to RHF as a string like any other text input — pair it with z.coerce.number() in the field's schema so validation (and the typed form values) resolve to an actual number. DateField similarly renders a native <input type="date">, bound as an ISO "YYYY-MM-DD" string.

FieldWrapper and FieldMessage are the shared label/control/help-or-error layout every field above renders through — reach for them directly only when building a new field type that doesn't exist yet. getFieldError(errors, name) resolves a possibly-nested error (e.g. "address.street") out of RHF's errors object, and useFieldMeta(name, description) derives the id/aria wiring (id, descriptionId, errorId, error, describedBy) that every field component above uses internally.

Data-entry fields

RadioGroupField, NumberInputField, SliderField, RangeSliderField, TagInputField, OtpInputField, RatingField, and ColorPickerField bind the matching @alphinex/ui controls to RHF, taking the same name/label/description/required props as the fields above and spreading everything else onto the underlying control:

import {
  Form,
  useAppForm,
  RadioGroupField,
  NumberInputField,
  SliderField,
  RangeSliderField,
  TagInputField,
  OtpInputField,
  RatingField,
  ColorPickerField,
} from "@alphinex/forms";
import { z } from "zod";

const schema = z.object({
  plan: z.string().min(1, "Pick a plan"),
  seats: z.number().min(1),
  volume: z.number(),
  budget: z.tuple([z.number(), z.number()]),
  tags: z.array(z.string()).min(1, "Add at least one tag"),
  code: z.string().length(6, "Enter all 6 digits"),
  score: z.number().min(1, "Rate it"),
  brand: z.string(),
});

<Form form={form} onSubmit={save}>
  <RadioGroupField
    name="plan"
    label="Plan"
    options={[
      { value: "basic", label: "Basic" },
      { value: "pro", label: "Pro" },
    ]}
  />
  <NumberInputField name="seats" label="Seats" min={1} max={50} />
  <SliderField name="volume" label="Volume" />
  <RangeSliderField name="budget" label="Budget" />
  <TagInputField name="tags" label="Tags" />
  <OtpInputField name="code" label="Verification code" length={6} />
  <RatingField name="score" label="Score" allowHalf />
  <ColorPickerField name="brand" label="Brand color" />
</Form>;

Note NumberInputField is distinct from NumberField: the latter wraps a plain Input, while this one wraps NumberInput and brings increment/decrement affordances plus min/max clamping. Controls with no single focusable element (a radiogroup, a slider, a rating) are labelled via aria-labelledby rather than htmlFor, which FieldWrapper's labelId prop handles.

Selection fields

ComboboxField, MultiSelectField, AutocompleteField, CascaderField, TreeSelectField, and TransferField bind the searchable-selection controls to RHF:

import {
  ComboboxField,
  MultiSelectField,
  AutocompleteField,
  CascaderField,
  TreeSelectField,
  TransferField,
} from "@alphinex/forms";

<Form form={form} onSubmit={save}>
  {/* value: the selected option's `value` */}
  <ComboboxField name="country" label="Country" options={countries} clearable />
  {/* value: string[] of option values */}
  <MultiSelectField name="visited" label="Visited" options={countries} max={5} />
  {/* value: whatever text is in the field */}
  <AutocompleteField name="city" label="City" options={cities} />
  {/* value: the path array, root-first */}
  <CascaderField name="region" label="Region" options={regions} />
  {/* value: the selected node's id */}
  <TreeSelectField name="category" label="Category" items={categories} />
  {/* value: string[] of chosen values */}
  <TransferField name="grants" label="Grants" options={permissions} searchable />
</Form>;

Match the Zod type to what each field stores — z.string() for ComboboxField/TreeSelectField/ AutocompleteField, z.array(z.string()) for MultiSelectField/CascaderField/TransferField.

Date and time fields

DatePickerField, DateRangePickerField, TimePickerField, and DateTimePickerField bind @alphinex/dates' pickers to RHF. They need a <ThemeProvider> above them, like SliderField.

import {
  DatePickerField,
  DateRangePickerField,
  DateTimePickerField,
  TimePickerField,
} from "@alphinex/forms";

<Form form={form} onSubmit={save}>
  {/* value: Date */}
  <DatePickerField name="dueAt" label="Due date" min={new Date()} disabledDaysOfWeek={[0, 6]} />
  {/* value: Date — the calendar day of any existing value is preserved */}
  <TimePickerField name="reminderAt" label="Reminder" minuteStep={15} minTime="09:00" />
  {/* value: Date */}
  <DateTimePickerField name="startsAt" label="Starts at" defaultTime="09:00" />
  {/* value: { start: Date | null; end: Date | null } */}
  <DateRangePickerField name="stay" label="Stay" startLabel="Check in" endLabel="Check out" />
</Form>;

Use z.date() for the three single-value fields, and z.object({ start: z.date().nullable(), end: z.date().nullable() }) for the range.

defaultValues may hold ISO strings rather than Date objects — a form hydrated straight from a JSON API needs no conversion. They are read as local dates, so "2024-03-20" is the 20th everywhere, not the 19th west of Greenwich.

buildFormFromSchema

Renders a form from a JSON-serializable array of FieldDefinition objects instead of hand-written JSX — the mechanism behind admin-configurable forms (e.g. per-tenant custom fields stored in a database), built entirely on top of the same field components you'd use directly. Wrap the result in <Form>:

import { Form, useAppForm, buildFormFromSchema, type FieldDefinition } from "@alphinex/forms";
import { z } from "zod";

const fieldDefs: FieldDefinition[] = [
  { type: "text", name: "companyName", label: "Company name", required: true },
  {
    type: "select",
    name: "industry",
    label: "Industry",
    options: [
      { value: "tech", label: "Technology" },
      { value: "retail", label: "Retail" },
    ],
  },
  { type: "checkbox", name: "acceptsTerms", label: "I accept the terms" },
];

const schema = z.object({
  companyName: z.string().min(1),
  industry: z.string(),
  acceptsTerms: z.boolean(),
});

function DynamicOnboardingForm() {
  const form = useAppForm({
    schema,
    defaultValues: { companyName: "", industry: "", acceptsTerms: false },
  });

  return (
    <Form form={form} onSubmit={(values) => console.log(values)}>
      {buildFormFromSchema(fieldDefs)}
      <button type="submit">Continue</button>
    </Form>
  );
}

Each FieldDefinition is a discriminated union on type ("text" | "number" | "textarea" | "checkbox" | "switch" | "select" | "date"), so TypeScript narrows the extra options per type — select requires options: SelectOption[], text/number/textarea accept placeholder, and so on. The Zod schema is still yours to write and keep in sync with the field names.

See documentation/ARCHITECTURE.md for the full package contract, dependency rules, and roadmap placement.

FileUploadField

import { FileUploadField } from "@alphinex/forms";

<FileUploadField
  name="attachments"
  label="Attachments"
  multiple
  accept=".pdf,.png"
  maxSize={5_000_000}
/>;

The stored value is always a File[], even in single-file mode — one shape means a schema and a submit handler never have to branch on multiple:

const schema = z.object({
  attachments: z.array(z.instanceof(File)).min(1, "Attach at least one file"),
  avatar: z.array(z.instanceof(File)).max(1),
});

Files failing the field's own accept/maxSize checks never reach the form value, and the reason is shown under the drop zone.