npm package: @saastro/forms
Description: Sistema de formularios dinámicos para React + Zod + React Hook Form
Author: SAASTRO
Latest version: 0.1.3

# @saastro/forms

Dynamic form system for React with Zod validation, React Hook Form, and a plugin system.

## Features

- 30+ field types (text, select, date, slider, OTP, button-card, etc.)
- **Zero-config mode**: Just pass your shadcn components inline
- Multi-step forms with conditional navigation
- Zod schema validation per field
- Plugin system (localStorage, analytics, autosave)
- Modular submit actions (HTTP, webhook, email)
- Dependency injection for UI components
- Builder pattern API
- Responsive 12-column grid layout
- TypeScript strict mode

## Installation

```bash
npm install @saastro/forms
# or
bun add @saastro/forms
```

### Peer Dependencies

```bash
npm install react react-dom react-hook-form zod date-fns react-day-picker
```

### shadcn/ui Components

Install only the components you need. For a basic text form:

```bash
npx shadcn@latest add input button label field form
```

For all available field types:

```bash
npx shadcn@latest add input button label textarea select checkbox radio-group popover calendar tooltip slider switch separator dialog command input-otp field form
```

### Tailwind CSS 4

If you're using Tailwind CSS 4, add the following `@source` directive to your main CSS file so Tailwind can detect the classes used by this package:

```css
@source "node_modules/@saastro/forms/dist/**/*.js";
```

## Quick Start (Zero-Config with Auto-Discovery)

The simplest way to use `@saastro/forms` — auto-discover all your shadcn components with Vite's glob:

```tsx
import { Form, FormBuilder } from '@saastro/forms';

// Auto-discover all shadcn components at build time
const uiComponents = import.meta.glob('@/components/ui/*.tsx', { eager: true });

const config = FormBuilder.create('contact')
  .addField('name', (f) => f.type('text').label('Name').required().minLength(2))
  .addField('email', (f) => f.type('email').label('Email').required().email())
  .addStep('main', ['name', 'email'])
  .buttons({ submit: { type: 'submit', label: 'Send' } })
  .build();

export function ContactForm() {
  return (
    <Form
      config={config}
      components={uiComponents}
      onSubmit={(values) => console.log('Submitted:', values)}
    />
  );
}
```

That's it! No FormProvider, no barrel file, no manual component registration. The Form auto-discovers components by parsing the glob result.

### Alternative: Explicit Components

If you prefer explicit imports or need to override specific components:

```tsx
import { Form, FormBuilder } from '@saastro/forms';
import { Input, Button, Label } from '@/components/ui';
import { Field, FieldLabel, FieldDescription, FieldError } from '@/components/ui/field';
import { FormField, FormControl } from '@/components/ui/form';

<Form
  config={config}
  components={{
    Input,
    Button,
    Label,
    Field,
    FieldLabel,
    FieldDescription,
    FieldError,
    FormField,
    FormControl,
  }}
/>;
```

### Benefits of Zero-Config

1. **One line of setup** — just `import.meta.glob` and you're done
2. **Auto-discovers components** — no manual registration needed
3. **Missing component fallback** — shows helpful warnings with install commands
4. **No Provider boilerplate** — just render `<Form />`
5. **Override per-form** — mix glob with explicit overrides

## Quick Start (Provider Mode)

For apps with many forms, you can still use the provider pattern:

```tsx
import { Form, FormBuilder, ComponentProvider, createComponentRegistry } from '@saastro/forms';
import * as shadcn from '@/lib/form-components'; // Your barrel file

const registry = createComponentRegistry(shadcn);

export function App() {
  return (
    <ComponentProvider components={registry}>
      <ContactForm />
      <SignupForm />
      <CheckoutForm />
    </ComponentProvider>
  );
}

function ContactForm() {
  const config = FormBuilder.create('contact')
    .addField('name', (f) => f.type('text').label('Name').schema(z.string().min(2)))
    .addStep('main', ['name'])
    .build();

  return <Form config={config} />;
}
```

## Which Components Do I Need?

Each field type requires certain components. When a component is missing, you'll see a helpful warning with install instructions.

| Field Type             | Required Components                                                       |
| ---------------------- | ------------------------------------------------------------------------- |
| `text`, `email`, `tel` | Input, Label, Field, FieldLabel, FieldError, FormField, FormControl       |
| `textarea`             | Textarea, Label, Field, FieldLabel, FieldError, FormField, FormControl    |
| `select`               | Select, SelectTrigger, SelectContent, SelectItem, SelectValue, Field, ... |
| `checkbox`, `switch`   | Checkbox/Switch, Label, Field, ...                                        |
| `date`                 | Calendar, Popover, PopoverTrigger, PopoverContent, Button, Field, ...     |
| `combobox`             | Command, CommandInput, CommandList, Popover, Button, Field, ...           |

Use `getRequiredComponents(config)` to programmatically check:

```tsx
import { getRequiredComponents, getInstallCommand } from '@saastro/forms';

const required = getRequiredComponents(config);
// ['Input', 'Button', 'Label', 'Field', ...]

const installCmd = getInstallCommand(required);
// 'npx shadcn@latest add input button label field form'
```

## Multi-Step Form

```tsx
const config = FormBuilder.create('signup')
  .layout('manual')
  .columns(2)
  .addField('email', (f) => f.type('email').label('Email').schema(z.string().email()))
  .addField('password', (f) => f.type('text').label('Password').schema(z.string().min(8)))
  .addField('name', (f) => f.type('text').label('Full Name').schema(z.string().min(2)))
  .addField('plan', (f) =>
    f
      .type('button-radio')
      .label('Choose Plan')
      .schema(z.string())
      .options([
        { label: 'Free', value: 'free' },
        { label: 'Pro', value: 'pro' },
      ]),
  )
  .addStep('account', ['email', 'password'])
  .addStep('profile', ['name', 'plan'])
  .initialStep('account')
  .buttons({
    submit: { type: 'submit', label: 'Create Account' },
    next: { type: 'next', label: 'Next' },
    back: { type: 'back', label: 'Back' },
  })
  .build();
```

## Plugins

```tsx
import { FormBuilder, PluginManager, localStoragePlugin, analyticsPlugin } from '@saastro/forms';

const plugins = new PluginManager();
plugins.register(localStoragePlugin({ key: 'my-form' }));
plugins.register(analyticsPlugin());

const config = FormBuilder.create('my-form')
  .usePlugins(plugins)
  // ... fields and steps
  .build();
```

## Submit Actions

```tsx
const config = FormBuilder.create('form')
  // ... fields and steps
  .build();

// Add submit actions to config
config.submitActions = {
  sendToAPI: {
    id: 'sendToAPI',
    action: {
      name: 'Send to API',
      type: 'http',
      config: {
        url: 'https://api.example.com/submit',
        method: 'POST',
      },
    },
    trigger: { type: 'onSubmit' },
    order: 1,
  },
};
```

## Conditional Fields

```tsx
// Hide field based on another field's value
.addField("company", (f) =>
  f.type("text")
    .label("Company")
    .schema(z.string().optional())
    .hidden((values) => values.type !== "business")
)

// Responsive visibility
.addField("sidebar", (f) =>
  f.type("html")
    .hidden({ default: "hidden", lg: "visible" })
)

// Declarative conditions
.addField("discount", (f) =>
  f.type("text")
    .label("Discount Code")
    .schema(z.string().optional())
    .hidden({
      operator: "and",
      conditions: [
        { field: "plan", operator: "equals", value: "free" },
      ],
    })
)
```

## Component Override

Override specific components per-form, even when using a provider:

```tsx
import { MyCustomDatePicker } from './components/MyCustomDatePicker';

<Form
  config={config}
  components={{
    Calendar: MyCustomDatePicker, // Override just the Calendar
  }}
/>;
```

## Field Types

| Type                   | Description                           |
| ---------------------- | ------------------------------------- |
| `text`, `email`, `tel` | Text inputs                           |
| `textarea`             | Multi-line text                       |
| `select`               | Dropdown select                       |
| `native-select`        | Native HTML select                    |
| `combobox`             | Searchable select (Popover + Command) |
| `command`              | Command palette select                |
| `checkbox`, `switch`   | Boolean toggles                       |
| `radio`                | Radio group                           |
| `button-radio`         | Button-style radio                    |
| `button-checkbox`      | Button-style multi-select             |
| `button-card`          | Card-style selection                  |
| `checkbox-group`       | Multiple checkboxes                   |
| `switch-group`         | Multiple switches                     |
| `date`                 | Date picker (simple or popover)       |
| `daterange`            | Date range picker                     |
| `slider`               | Range slider                          |
| `otp`                  | OTP input                             |
| `input-group`          | Input with prefix/suffix              |
| `html`                 | Raw HTML content                      |

## API Reference

### Form Props

```tsx
interface FormProps {
  /** Form configuration */
  config: FormConfig;
  /** Component overrides (zero-config mode) */
  components?: PartialComponentRegistry;
  /** Called on successful submit */
  onSubmit?: (values: Record<string, unknown>) => void | Promise<void>;
  /** Called on submit error */
  onError?: (error: Error) => void;
  /** CSS class for form element */
  className?: string;
}
```

### Utility Functions

```tsx
// Get components needed for a form
getRequiredComponents(config: FormConfig): ComponentName[]

// Get missing components
getMissingComponents(required: ComponentName[], provided: Partial<...>): ComponentName[]

// Generate install command
getInstallCommand(missing: ComponentName[]): string
```

## License

MIT