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

@letar/forms

v1.3.0

Published

Declarative form components for React with 50+ field types, built-in analytics, server error mapping, undo/redo — powered by TanStack Form and Chakra UI v3

Downloads

77

Readme

@lena/form-components

Переиспользуемая UI-библиотека компонентов форм на базе TanStack Form для монорепозитория Lena.

English documentation

Quick Start

import { Form } from '@lena/form-components'
import { z } from 'zod/v4'

const Schema = z.object({
  title: z.string().min(2).meta({ ui: { title: 'Название', placeholder: 'Введите...' } }),
  rating: z.number().min(0).max(10).meta({ ui: { title: 'Рейтинг' } }),
})

<Form schema={Schema} initialValue={{ title: '', rating: 5 }} onSubmit={save}>
  <Form.Field.String name="title" />
  <Form.Field.Number name="rating" />
  <Form.Button.Submit>Сохранить</Form.Button.Submit>
</Form>

Или полная автогенерация:

<Form.FromSchema schema={Schema} initialValue={data} onSubmit={handleSubmit} submitLabel="Создать" />

Философия: Отделение вёрстки от логики

| Аспект | Где определяется | Как используется в JSX | | ----------------- | -------------------------- | ------------------------------- | | Валидация | Zod схема | schema={Schema} | | UI метаданные | Zod .meta({ ui: {...} }) | Автоматически из схемы | | Структура | TypeScript типы | initialValue={data} | | Вёрстка | JSX | <HStack>, <VStack>, <Box> |

Результат: JSX содержит только вёрстку и имена полей. Вся логика живёт в схеме.


Документация

| Категория | Документация | Описание | | ---------------- | -------------------------------------------------------- | -------------------------------------------- | | Field компоненты | docs/fields.md | 56 типов полей (String, Number, Select, ...) | | Form-level | docs/form-level.md | Steps, When, Watch, Errors, Persistence | | Schema генерация | docs/schema-generation.md | FromSchema, AutoFields, Builder, Templates | | Server Errors | docs/server-errors.md | Маппинг Prisma/ZenStack/Zod ошибок на поля | | Offline | docs/offline.md | Оффлайн режим, очередь синхронизации | | ZenStack | docs/zenstack.md | Плагин, @form.* директивы, withUIMeta | | i18n | docs/i18n.md | Мультиязычность, перевод ошибок валидации | | Analytics | docs/analytics.md | Field-level аналитика, 4 адаптера | | API Reference | docs/api-reference.md | Хуки, контексты, типы |


Основные возможности

56 Field компонентов

// Текстовые
<Form.Field.String name="title" />
<Form.Field.Textarea name="description" />
<Form.Field.RichText name="content" />

// Числовые
<Form.Field.Number name="price" />
<Form.Field.Slider name="rating" />
<Form.Field.Currency name="amount" />

// Выбор
<Form.Field.Select name="category" />
<Form.Field.RadioGroup name="type" />
<Form.Field.Checkbox name="agree" />

// Специальные
<Form.Field.Date name="birthday" />
<Form.Field.Phone name="phone" />
<Form.Field.FileUpload name="avatar" />
<Form.Field.Signature name="signature" />
<Form.Field.CreditCard name="card" />

// Защита
<Form.Captcha />

Полный список → docs/fields.md

Form-level компоненты

<Form schema={Schema} initialValue={data} onSubmit={save}>
  {/* Реактивные побочные эффекты */}
  <Form.Watch
    field="name"
    onChange={(v, { setFieldValue }) => {
      setFieldValue('slug', transliterate(String(v)))
    }}
  />

  {/* Условный рендеринг */}
  <Form.When field="type" is="company">
    <Form.Field.String name="companyName" />
  </Form.When>

  {/* Мультистеп формы */}
  <Form.Steps animated validateOnNext>
    <Form.Steps.Step title="Шаг 1">...</Form.Steps.Step>
    <Form.Steps.Step title="Шаг 2">...</Form.Steps.Step>
    <Form.Steps.Navigation />
  </Form.Steps>

  {/* Информационный блок */}
  <Form.InfoBlock variant="info" title="Подсказка">
    Заполните все поля для скидки.
  </Form.InfoBlock>

  {/* Разделитель секций */}
  <Form.Divider label="Контакты" />

  {/* Вычисляемые поля */}
  <Form.Field.Calculated
    name="total"
    compute={(v) => v.price * v.qty}
    format={(v) => `${v.toLocaleString()} ₽`}
    deps={['price', 'qty']}
  />

  {/* Табличный редактор (массив объектов) */}
  <Form.Field.TableEditor
    name="items"
    columns={[
      { name: 'product', width: '40%' },
      { name: 'qty', width: '15%', align: 'right' },
      { name: 'price', width: '15%', align: 'right' },
      { name: 'total', computed: (row) => row.qty * row.price, label: 'Итого' },
    ]}
    addLabel="Добавить товар"
    footer={[{ column: 'total', aggregate: 'sum', label: 'Итого:' }]}
  />

  {/* Скрытые поля (UTM, referral) */}
  <Form.Field.Hidden name="utm_source" value="landing" />

  {/* Сводка ошибок */}
  <Form.Errors title="Исправьте ошибки:" />

  {/* JSON-инспектор значений (скрыт в production) */}
  <Form.DebugValues />

  <Form.Button.Submit />
</Form>

Подробнее → docs/form-level.md

Группы и массивы

// Вложенный объект
<Form.Group name="address">
  <Form.Field.String name="city" />    {/* → address.city */}
  <Form.Field.String name="street" />  {/* → address.street */}
</Form.Group>

// Массив
<Form.Group.List name="phones">
  <Form.Field.Phone />
  <Form.Group.List.Button.Add>Добавить телефон</Form.Group.List.Button.Add>
</Form.Group.List>

Smart Autofill

Поля автоматически получают правильные autocomplete атрибуты (+30% конверсии, WCAG 1.3.5):

<Form.Field.String name="email" />      // → autocomplete="email"
<Form.Field.String name="firstName" />   // → autocomplete="given-name"
<Form.Field.Password name="password" />  // → autocomplete="current-password"

Override: autoComplete prop или .meta({ ui: { autocomplete: 'off' } }).

Автоматические constraints из Zod

const Schema = z.object({
  title: z.string().min(2).max(100),  // → minLength={2} maxLength={100}
  email: z.string().email(),          // → type="email"
  rating: z.number().min(1).max(10),  // → min={1} max={10}
})

// DRY: валидация и UI constraints в одном месте
<Form.Field.String name="title" />  {/* maxLength={100} из схемы */}

ZenStack интеграция

model Product {
  /// @form.title("Название продукта")
  /// @form.placeholder("Введите название")
  title String

  /// @form.title("Цена")
  /// @form.fieldType("currency")
  /// @form.props({ min: 0, currency: "RUB" })
  price Int
}
import { ProductCreateFormSchema } from '@/generated/form-schemas'
<Form.FromSchema schema={ProductCreateFormSchema} initialValue={data} onSubmit={save} />

Подробнее → docs/zenstack.md

Offline Support

<Form
  initialValue={data}
  offline={{
    actionType: 'UPDATE_PROFILE',
    onQueued: () => toast.info('Сохранено локально'),
    onSynced: () => toast.success('Синхронизировано'),
  }}
  onSubmit={handleSubmit}
>
  <Form.OfflineIndicator />
  <Form.Field.String name="name" />
  <Form.Button.Submit />
</Form>

Подробнее → docs/offline.md

Security

// Honeypot — ловушка для ботов
<Form honeypot={true} initialValue={data} onSubmit={handleSubmit}>
  <Form.Field.String name="email" />
  <Form.Button.Submit />
</Form>

// Rate Limiting — ограничение попыток submit
<Form rateLimit={{ maxSubmits: 3, windowMs: 60000 }} initialValue={data} onSubmit={handleSubmit}>
  ...
</Form>

// Secure File Upload — проверка MIME, удаление EXIF, переименование
<Form.Field.FileUpload
  name="document"
  security={{
    maxSize: '10MB',
    allowedTypes: ['image/jpeg', 'image/png', 'application/pdf'],
    stripMetadata: true,
    renameFile: true,
  }}
/>

Testing Utilities (v0.85.0)

import { expectFieldError, fillField, renderForm, submitForm } from '@lena/form-components/testing'

const { onSubmit } = renderForm(ContactForm)
await fillField('name', 'Иван')
await fillField('email', '[email protected]')
await submitForm()
expect(onSubmit).toHaveBeenCalled()

URL Prefill (v0.85.0)

import { generatePrefillUrl, useUrlPrefill } from '@lena/form-components'

// URL: /contact?name=Иван&[email protected]
const prefilled = useUrlPrefill({
  fields: ['name', 'email'],
  cleanUrl: true,
})

// Генерация маркетинговых ссылок
const url = generatePrefillUrl('/contact', { name: 'Иван', email: '[email protected]' })

DX фичи

// Аналитика форм — field-level tracking с 4 адаптерами
<Form analytics={{ adapter: umamiAdapter }}>
  <Form.Analytics.Panel />  {/* Dev-only live панель */}
</Form>

// Undo/Redo — Ctrl+Z/Ctrl+Y для длинных форм
const { undo, redo, canUndo, canRedo } = useFormHistory(form)
<Form.History.Controls />

// Маппинг серверных ошибок — автодетект Prisma/ZenStack/Zod
const mapped = mapServerErrors(error)
applyServerErrors(form, mapped)

// ReadOnly view — отображение данных из Zod-схемы
<FormReadOnlyView data={user} schema={UserSchema} compact />

// Skeleton — loading state из схемы
<FormSkeleton schema={UserSchema} showSubmit />

// Comparison — diff (было → стало)
<FormComparison original={old} current={new} schema={Schema} onlyChanged />

// Каскадный рендеринг
<FormDependsOn field="type" cases={{ person: <PersonFields />, company: <CompanyFields /> }} />

Установка

# Уже установлен в монорепозитории
import { Form } from '@lena/form-components'

Опциональные зависимости (npm)

| Пакет | Для чего | | --------------------------- | --------------------------------- | | @dnd-kit/* | Drag & drop сортировка в массивах | | use-mask-input | Phone, MaskedInput | | @tiptap/* | RichText редактор | | @uiw/react-json-view | Form.DebugValues (JSON инспектор) | | next-intl | i18n интеграция | | @marsidev/react-turnstile | CAPTCHA (Cloudflare Turnstile) |


Команды

nx build @lena/form-components    # Сборка
nx lint @lena/form-components     # Линтинг
nx test @lena/form-components     # Тесты

Провайдер адресов

Поля Address и City поддерживают подключаемые провайдеры геокодинга. DaData (Россия) встроен:

import { createForm, createDaDataProvider } from '@lena/form-components'

// Вариант 1: через createForm (рекомендуемый)
const AppForm = createForm({
  addressProvider: createDaDataProvider({ token: process.env.DADATA_TOKEN }),
})

<AppForm.Field.Address name="address" />
<AppForm.Field.City name="city" />

// Вариант 2: на конкретном поле
<Form.Field.Address name="address" provider={myProvider} />

// Вариант 3: обратная совместимость через token
<Form.Field.Address name="address" token="dadata-token" />

Для других сервисов — реализуйте интерфейс AddressProvider:

const myProvider: AddressProvider = {
  async getSuggestions(query, options) {
    const res = await fetch(`/api/geocode?q=${query}`)
    return res.json() // [{ label, value, data }]
  },
}

AI Tooling (MCP)

MCP сервер @letar/form-mcp предоставляет AI-ассистентам (Claude Code, Cursor, VS Code Copilot) полный контекст о библиотеке: 56 п��лей, паттерны форм, @form.* директивы.

{ "form-mcp": { "command": "npx", "args": ["-y", "@letar/form-mcp"] } }

Bundle Size

Библиотека поставляется как ESM с external dependencies. Все тяжёлые зависимости (Chakra, React, Tiptap, dnd-kit) — external и не включаются в bundle.

| Модуль | Размер (brotli) | Размер (raw) | | --------------------------------- | --------------- | ------------ | | @letar/forms (все 56 п��лей) | 20 KB | 109 KB | | @letar/forms/fields/text | < 1 KB | re-export | | @letar/forms/fields/number | < 1 KB | re-export | | @letar/forms/fields/datetime | < 1 KB | re-export | | @letar/forms/fields/selection | < 1 KB | re-export | | @letar/forms/fields/boolean | < 1 KB | re-export | | @letar/forms/fields/specialized | < 1 KB | re-export | | @letar/forms/offline | < 1 KB | 5 KB | | @letar/forms/i18n | < 1 KB | 13 KB |

Категорийные entry points (fields/*) позволяют импортировать только нужные поля:

// Полный импорт — все 56 п��лей
import { Form } from '@letar/forms'

// Категорийный импорт — только текстовые поля
import { FieldString, FieldTextarea } from '@letar/forms/fields/text'

Метрики проверяются в CI через size-limit.

Ре-рендеры

При вводе текста в одно поле формы из 10 полей — остальные 9 полей НЕ ре-рендерятся (0 лишних рендеров). TanStack Form обеспечивает field-level подписки — каждое поле изолировано.

Связанные документы


Версия: 1.3.0 Последнее обновление: 2026-04-10