Skip to content

Commit a9a2741

Browse files
feat: add shadcn/ui custom registry for medusa-forms components
- Add main registry.json configuration - Create registry-compatible versions of all medusa-forms components: - ControlledInput - ControlledSelect - ControlledCheckbox - ControlledTextarea - ControlledDatePicker - ControlledCurrencyInput - Generate individual JSON registry files for each component - Add comprehensive documentation in REGISTRY.md - Components can now be installed via: npx shadcn@latest add [URL]
1 parent 73c46a8 commit a9a2741

14 files changed

Lines changed: 1035 additions & 0 deletions

REGISTRY.md

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
# Medusa Forms shadcn/ui Registry
2+
3+
This repository provides a custom shadcn/ui registry for medusa-forms components, allowing developers to install them using the native shadcn CLI.
4+
5+
## Installation
6+
7+
You can install individual medusa-forms components using the shadcn CLI:
8+
9+
```bash
10+
# Install individual components
11+
npx shadcn@latest add https://raw.githubusercontent.com/lambda-curry/forms/main/registry/controlled-input.json
12+
npx shadcn@latest add https://raw.githubusercontent.com/lambda-curry/forms/main/registry/controlled-select.json
13+
npx shadcn@latest add https://raw.githubusercontent.com/lambda-curry/forms/main/registry/controlled-checkbox.json
14+
npx shadcn@latest add https://raw.githubusercontent.com/lambda-curry/forms/main/registry/controlled-textarea.json
15+
npx shadcn@latest add https://raw.githubusercontent.com/lambda-curry/forms/main/registry/controlled-datepicker.json
16+
npx shadcn@latest add https://raw.githubusercontent.com/lambda-curry/forms/main/registry/controlled-currency-input.json
17+
```
18+
19+
## Available Components
20+
21+
### ControlledInput
22+
A controlled input component with react-hook-form integration.
23+
24+
```tsx
25+
import { ControlledInput } from '@/components/ui/controlled-input'
26+
27+
<ControlledInput
28+
name="email"
29+
label="Email"
30+
placeholder="Enter your email"
31+
rules={{ required: 'Email is required' }}
32+
/>
33+
```
34+
35+
### ControlledSelect
36+
A controlled select component with react-hook-form integration.
37+
38+
```tsx
39+
import { ControlledSelect } from '@/components/ui/controlled-select'
40+
41+
const options = [
42+
{ label: 'Option 1', value: 'option1' },
43+
{ label: 'Option 2', value: 'option2' },
44+
]
45+
46+
<ControlledSelect
47+
name="category"
48+
label="Category"
49+
placeholder="Select a category"
50+
options={options}
51+
rules={{ required: 'Category is required' }}
52+
/>
53+
```
54+
55+
### ControlledCheckbox
56+
A controlled checkbox component with react-hook-form integration.
57+
58+
```tsx
59+
import { ControlledCheckbox } from '@/components/ui/controlled-checkbox'
60+
61+
<ControlledCheckbox
62+
name="terms"
63+
label="I agree to the terms and conditions"
64+
rules={{ required: 'You must agree to the terms' }}
65+
/>
66+
```
67+
68+
### ControlledTextarea
69+
A controlled textarea component with react-hook-form integration.
70+
71+
```tsx
72+
import { ControlledTextarea } from '@/components/ui/controlled-textarea'
73+
74+
<ControlledTextarea
75+
name="description"
76+
label="Description"
77+
placeholder="Enter a description"
78+
rows={4}
79+
rules={{ required: 'Description is required' }}
80+
/>
81+
```
82+
83+
### ControlledDatePicker
84+
A controlled date picker component with react-hook-form integration.
85+
86+
```tsx
87+
import { ControlledDatePicker } from '@/components/ui/controlled-datepicker'
88+
89+
<ControlledDatePicker
90+
name="birthDate"
91+
label="Birth Date"
92+
placeholder="Select your birth date"
93+
rules={{ required: 'Birth date is required' }}
94+
/>
95+
```
96+
97+
### ControlledCurrencyInput
98+
A controlled currency input component with react-hook-form integration.
99+
100+
```tsx
101+
import { ControlledCurrencyInput } from '@/components/ui/controlled-currency-input'
102+
103+
<ControlledCurrencyInput
104+
name="price"
105+
label="Price"
106+
placeholder="Enter price"
107+
currency="USD"
108+
rules={{ required: 'Price is required' }}
109+
/>
110+
```
111+
112+
## Prerequisites
113+
114+
Before using these components, make sure you have:
115+
116+
1. **shadcn/ui initialized** in your project:
117+
```bash
118+
npx shadcn@latest init
119+
```
120+
121+
2. **Required dependencies** installed:
122+
```bash
123+
npm install @medusajs/ui react-hook-form @hookform/error-message
124+
```
125+
126+
3. **FormProvider setup** in your application:
127+
```tsx
128+
import { useForm, FormProvider } from 'react-hook-form'
129+
130+
function MyForm() {
131+
const methods = useForm()
132+
133+
return (
134+
<FormProvider {...methods}>
135+
<form onSubmit={methods.handleSubmit(onSubmit)}>
136+
{/* Your controlled components here */}
137+
</form>
138+
</FormProvider>
139+
)
140+
}
141+
```
142+
143+
## Benefits
144+
145+
**No Custom CLI**: Leverage existing shadcn infrastructure
146+
**Familiar UX**: Developers already know `npx shadcn@latest add`
147+
**Auto Dependencies**: shadcn handles npm package installation
148+
**TypeScript Support**: Built-in TypeScript compatibility
149+
**Zero Maintenance**: No CLI package to maintain
150+
**Instant Adoption**: Works with existing shadcn projects
151+
**v0 Compatible**: Components work with v0.dev
152+
153+
## Registry Structure
154+
155+
```
156+
lambda-curry/forms/
157+
├── registry.json # Main registry configuration
158+
├── registry/ # Component registry files
159+
│ ├── controlled-input.json
160+
│ ├── controlled-select.json
161+
│ ├── controlled-checkbox.json
162+
│ ├── controlled-textarea.json
163+
│ ├── controlled-datepicker.json
164+
│ └── controlled-currency-input.json
165+
└── src/registry/ # Component source files
166+
├── controlled-input.tsx
167+
├── controlled-select.tsx
168+
├── controlled-checkbox.tsx
169+
├── controlled-textarea.tsx
170+
├── controlled-datepicker.tsx
171+
└── controlled-currency-input.tsx
172+
```
173+
174+
## Contributing
175+
176+
To add new components to the registry:
177+
178+
1. Create the component in `src/registry/`
179+
2. Add it to the main `registry.json`
180+
3. Generate the individual JSON file in `registry/`
181+
4. Update this documentation
182+
183+
## License
184+
185+
This project is licensed under the same license as the main forms repository.
186+

registry.json

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
{
2+
"$schema": "https://ui.shadcn.com/schema/registry.json",
3+
"name": "medusa-forms",
4+
"homepage": "https://forms.lambdacurry.com",
5+
"description": "A collection of form components built with @medusajs/ui and react-hook-form",
6+
"items": [
7+
{
8+
"name": "controlled-input",
9+
"type": "registry:ui",
10+
"title": "Controlled Input",
11+
"description": "A controlled input component with react-hook-form integration",
12+
"dependencies": ["@medusajs/ui", "react-hook-form", "@hookform/error-message"],
13+
"registryDependencies": [],
14+
"files": [
15+
{
16+
"path": "src/registry/controlled-input.tsx",
17+
"type": "registry:component"
18+
}
19+
]
20+
},
21+
{
22+
"name": "controlled-select",
23+
"type": "registry:ui",
24+
"title": "Controlled Select",
25+
"description": "A controlled select component with react-hook-form integration",
26+
"dependencies": ["@medusajs/ui", "react-hook-form", "@hookform/error-message"],
27+
"registryDependencies": [],
28+
"files": [
29+
{
30+
"path": "src/registry/controlled-select.tsx",
31+
"type": "registry:component"
32+
}
33+
]
34+
},
35+
{
36+
"name": "controlled-checkbox",
37+
"type": "registry:ui",
38+
"title": "Controlled Checkbox",
39+
"description": "A controlled checkbox component with react-hook-form integration",
40+
"dependencies": ["@medusajs/ui", "react-hook-form", "@hookform/error-message"],
41+
"registryDependencies": [],
42+
"files": [
43+
{
44+
"path": "src/registry/controlled-checkbox.tsx",
45+
"type": "registry:component"
46+
}
47+
]
48+
},
49+
{
50+
"name": "controlled-textarea",
51+
"type": "registry:ui",
52+
"title": "Controlled TextArea",
53+
"description": "A controlled textarea component with react-hook-form integration",
54+
"dependencies": ["@medusajs/ui", "react-hook-form", "@hookform/error-message"],
55+
"registryDependencies": [],
56+
"files": [
57+
{
58+
"path": "src/registry/controlled-textarea.tsx",
59+
"type": "registry:component"
60+
}
61+
]
62+
},
63+
{
64+
"name": "controlled-datepicker",
65+
"type": "registry:ui",
66+
"title": "Controlled DatePicker",
67+
"description": "A controlled date picker component with react-hook-form integration",
68+
"dependencies": ["@medusajs/ui", "react-hook-form", "@hookform/error-message"],
69+
"registryDependencies": [],
70+
"files": [
71+
{
72+
"path": "src/registry/controlled-datepicker.tsx",
73+
"type": "registry:component"
74+
}
75+
]
76+
},
77+
{
78+
"name": "controlled-currency-input",
79+
"type": "registry:ui",
80+
"title": "Controlled Currency Input",
81+
"description": "A controlled currency input component with react-hook-form integration",
82+
"dependencies": ["@medusajs/ui", "react-hook-form", "@hookform/error-message"],
83+
"registryDependencies": [],
84+
"files": [
85+
{
86+
"path": "src/registry/controlled-currency-input.tsx",
87+
"type": "registry:component"
88+
}
89+
]
90+
}
91+
]
92+
}
93+

registry/controlled-checkbox.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "controlled-checkbox",
3+
"type": "registry:ui",
4+
"description": "A checkbox component with react-hook-form integration",
5+
"dependencies": [
6+
"@medusajs/ui",
7+
"react-hook-form",
8+
"@hookform/error-message"
9+
],
10+
"files": [
11+
{
12+
"name": "controlled-checkbox.tsx",
13+
"content": "\"use client\"\n\nimport type { ComponentProps } from 'react'\nimport {\n Controller,\n type ControllerProps,\n type FieldValues,\n type Path,\n type RegisterOptions,\n useFormContext,\n} from 'react-hook-form'\nimport { Checkbox } from '@medusajs/ui'\nimport { ErrorMessage } from '@hookform/error-message'\n\nexport type ControlledCheckboxProps<T extends FieldValues> = Omit<ControllerProps, 'render'> & {\n name: Path<T>\n rules?: Omit<RegisterOptions<T, Path<T>>, 'disabled' | 'valueAsNumber' | 'valueAsDate' | 'setValueAs'>\n label?: string\n description?: string\n required?: boolean\n disabled?: boolean\n className?: string\n onChange?: (checked: boolean) => void\n} & ComponentProps<typeof Checkbox>\n\n/**\n * A controlled checkbox component that integrates with react-hook-form.\n * \n * @example\n * ```tsx\n * import { useForm, FormProvider } from 'react-hook-form'\n * import { ControlledCheckbox } from '@/components/ui/controlled-checkbox'\n * \n * function MyForm() {\n * const methods = useForm()\n * \n * return (\n * <FormProvider {...methods}>\n * <form onSubmit={methods.handleSubmit(onSubmit)}>\n * <ControlledCheckbox\n * name=\"terms\"\n * label=\"I agree to the terms and conditions\"\n * rules={{ required: 'You must agree to the terms' }}\n * />\n * </form>\n * </FormProvider>\n * )\n * }\n * ```\n */\nexport const ControlledCheckbox = <T extends FieldValues>({\n name,\n rules,\n onChange,\n label,\n description,\n required,\n ...props\n}: ControlledCheckboxProps<T>) => {\n const {\n control,\n formState: { errors },\n } = useFormContext<T>()\n\n return (\n <div className=\"space-y-2\">\n <Controller\n control={control}\n name={name}\n rules={rules as Omit<RegisterOptions<T, Path<T>>, 'disabled' | 'valueAsNumber' | 'valueAsDate' | 'setValueAs'>}\n render={({ field }) => (\n <div className=\"flex items-start space-x-2\">\n <Checkbox\n {...field}\n {...props}\n id={name}\n checked={field.value}\n onCheckedChange={(checked) => {\n const booleanValue = checked === true\n if (onChange) {\n onChange(booleanValue)\n }\n field.onChange(booleanValue)\n }}\n />\n <div className=\"grid gap-1.5 leading-none\">\n {label && (\n <label\n htmlFor={name}\n className=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\"\n >\n {label}\n {required && <span className=\"text-red-500 ml-1\">*</span>}\n </label>\n )}\n {description && (\n <p className=\"text-xs text-muted-foreground\">\n {description}\n </p>\n )}\n </div>\n </div>\n )}\n />\n <ErrorMessage\n errors={errors}\n name={name}\n render={({ message }) => (\n <p className=\"text-sm text-red-500\">{message}</p>\n )}\n />\n </div>\n )\n}\n\n"
14+
}
15+
]
16+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "controlled-currency-input",
3+
"type": "registry:ui",
4+
"description": "A currency-input component with react-hook-form integration",
5+
"dependencies": [
6+
"@medusajs/ui",
7+
"react-hook-form",
8+
"@hookform/error-message"
9+
],
10+
"files": [
11+
{
12+
"name": "controlled-currency-input.tsx",
13+
"content": "\"use client\"\n\nimport type { ComponentProps } from 'react'\nimport {\n Controller,\n type ControllerProps,\n type FieldValues,\n type Path,\n type RegisterOptions,\n useFormContext,\n} from 'react-hook-form'\nimport { CurrencyInput } from '@medusajs/ui'\nimport { ErrorMessage } from '@hookform/error-message'\n\nexport type ControlledCurrencyInputProps<T extends FieldValues> = Omit<ControllerProps, 'render'> & {\n name: Path<T>\n rules?: Omit<RegisterOptions<T, Path<T>>, 'disabled' | 'valueAsNumber' | 'valueAsDate' | 'setValueAs'>\n label?: string\n placeholder?: string\n required?: boolean\n disabled?: boolean\n className?: string\n currency?: string\n onChange?: (value: number | undefined) => void\n} & ComponentProps<typeof CurrencyInput>\n\n/**\n * A controlled currency input component that integrates with react-hook-form.\n * \n * @example\n * ```tsx\n * import { useForm, FormProvider } from 'react-hook-form'\n * import { ControlledCurrencyInput } from '@/components/ui/controlled-currency-input'\n * \n * function MyForm() {\n * const methods = useForm()\n * \n * return (\n * <FormProvider {...methods}>\n * <form onSubmit={methods.handleSubmit(onSubmit)}>\n * <ControlledCurrencyInput\n * name=\"price\"\n * label=\"Price\"\n * placeholder=\"Enter price\"\n * currency=\"USD\"\n * rules={{ required: 'Price is required' }}\n * />\n * </form>\n * </FormProvider>\n * )\n * }\n * ```\n */\nexport const ControlledCurrencyInput = <T extends FieldValues>({\n name,\n rules,\n onChange,\n label,\n required,\n ...props\n}: ControlledCurrencyInputProps<T>) => {\n const {\n control,\n formState: { errors },\n } = useFormContext<T>()\n\n return (\n <div className=\"space-y-2\">\n {label && (\n <label htmlFor={name} className=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\">\n {label}\n {required && <span className=\"text-red-500 ml-1\">*</span>}\n </label>\n )}\n <Controller\n control={control}\n name={name}\n rules={rules as Omit<RegisterOptions<T, Path<T>>, 'disabled' | 'valueAsNumber' | 'valueAsDate' | 'setValueAs'>}\n render={({ field }) => (\n <CurrencyInput\n {...field}\n {...props}\n id={name}\n value={field.value}\n onValueChange={(value) => {\n if (onChange) {\n onChange(value)\n }\n field.onChange(value)\n }}\n />\n )}\n />\n <ErrorMessage\n errors={errors}\n name={name}\n render={({ message }) => (\n <p className=\"text-sm text-red-500\">{message}</p>\n )}\n />\n </div>\n )\n}\n\n"
14+
}
15+
]
16+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "controlled-datepicker",
3+
"type": "registry:ui",
4+
"description": "A datepicker component with react-hook-form integration",
5+
"dependencies": [
6+
"@medusajs/ui",
7+
"react-hook-form",
8+
"@hookform/error-message"
9+
],
10+
"files": [
11+
{
12+
"name": "controlled-datepicker.tsx",
13+
"content": "\"use client\"\n\nimport type { ComponentProps } from 'react'\nimport {\n Controller,\n type ControllerProps,\n type FieldValues,\n type Path,\n type RegisterOptions,\n useFormContext,\n} from 'react-hook-form'\nimport { DatePicker } from '@medusajs/ui'\nimport { ErrorMessage } from '@hookform/error-message'\n\nexport type ControlledDatePickerProps<T extends FieldValues> = Omit<ControllerProps, 'render'> & {\n name: Path<T>\n rules?: Omit<RegisterOptions<T, Path<T>>, 'disabled' | 'valueAsNumber' | 'valueAsDate' | 'setValueAs'>\n label?: string\n placeholder?: string\n required?: boolean\n disabled?: boolean\n className?: string\n onChange?: (date: Date | undefined) => void\n} & ComponentProps<typeof DatePicker>\n\n/**\n * A controlled date picker component that integrates with react-hook-form.\n * \n * @example\n * ```tsx\n * import { useForm, FormProvider } from 'react-hook-form'\n * import { ControlledDatePicker } from '@/components/ui/controlled-datepicker'\n * \n * function MyForm() {\n * const methods = useForm()\n * \n * return (\n * <FormProvider {...methods}>\n * <form onSubmit={methods.handleSubmit(onSubmit)}>\n * <ControlledDatePicker\n * name=\"birthDate\"\n * label=\"Birth Date\"\n * placeholder=\"Select your birth date\"\n * rules={{ required: 'Birth date is required' }}\n * />\n * </form>\n * </FormProvider>\n * )\n * }\n * ```\n */\nexport const ControlledDatePicker = <T extends FieldValues>({\n name,\n rules,\n onChange,\n label,\n placeholder,\n required,\n ...props\n}: ControlledDatePickerProps<T>) => {\n const {\n control,\n formState: { errors },\n } = useFormContext<T>()\n\n return (\n <div className=\"space-y-2\">\n {label && (\n <label htmlFor={name} className=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\">\n {label}\n {required && <span className=\"text-red-500 ml-1\">*</span>}\n </label>\n )}\n <Controller\n control={control}\n name={name}\n rules={rules as Omit<RegisterOptions<T, Path<T>>, 'disabled' | 'valueAsNumber' | 'valueAsDate' | 'setValueAs'>}\n render={({ field }) => (\n <DatePicker\n {...field}\n {...props}\n value={field.value}\n onChange={(date) => {\n if (onChange) {\n onChange(date)\n }\n field.onChange(date)\n }}\n placeholder={placeholder}\n />\n )}\n />\n <ErrorMessage\n errors={errors}\n name={name}\n render={({ message }) => (\n <p className=\"text-sm text-red-500\">{message}</p>\n )}\n />\n </div>\n )\n}\n\n"
14+
}
15+
]
16+
}

0 commit comments

Comments
 (0)