Skip to content

Commit e73df42

Browse files
feat: add shadcn/ui registry for medusa-forms components
- Add main registry.json with shadcn/ui schema - Create registry files for all UI components (input, select, checkbox, textarea, datepicker, currency-input) - Create registry files for all controlled components with react-hook-form integration - Add field-wrapper, field-error, and label base components - Include proper dependency management between components - Add comprehensive documentation in REGISTRY.md This allows developers to install medusa-forms components using: npx shadcn@latest add --registry <url> <component-name>
1 parent dcd8e37 commit e73df42

17 files changed

+408
-0
lines changed

packages/medusa-forms/REGISTRY.md

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# Medusa Forms Registry
2+
3+
This package provides a custom shadcn/ui registry that allows developers to install medusa-forms components using the native shadcn CLI.
4+
5+
## Installation
6+
7+
You can install components from this registry using the shadcn CLI:
8+
9+
```bash
10+
npx shadcn@latest add --registry https://raw.githubusercontent.com/lambda-curry/medusa-forms/main/packages/medusa-forms/registry.json input
11+
```
12+
13+
## Available Components
14+
15+
### Base UI Components
16+
17+
- `field-wrapper` - Core wrapper component with error handling and labels
18+
- `field-error` - Error display component
19+
- `label` - Label component with tooltip support
20+
- `input` - Base input component
21+
- `select` - Base select component
22+
- `field-checkbox` - Base checkbox component
23+
- `textarea` - Base textarea component
24+
- `datepicker` - Base datepicker component
25+
- `currency-input` - Base currency input component
26+
27+
### Controlled Components (React Hook Form)
28+
29+
- `controlled-input` - Input with react-hook-form integration
30+
- `controlled-select` - Select with react-hook-form integration
31+
- `controlled-checkbox` - Checkbox with react-hook-form integration
32+
- `controlled-textarea` - Textarea with react-hook-form integration
33+
- `controlled-datepicker` - DatePicker with react-hook-form integration
34+
- `controlled-currency-input` - CurrencyInput with react-hook-form integration
35+
36+
## Usage Examples
37+
38+
### Installing a single component
39+
40+
```bash
41+
npx shadcn@latest add --registry https://raw.githubusercontent.com/lambda-curry/medusa-forms/main/packages/medusa-forms/registry.json controlled-input
42+
```
43+
44+
### Installing multiple components
45+
46+
```bash
47+
npx shadcn@latest add --registry https://raw.githubusercontent.com/lambda-curry/medusa-forms/main/packages/medusa-forms/registry.json controlled-input controlled-select controlled-checkbox
48+
```
49+
50+
### Using the components
51+
52+
```tsx
53+
import { ControlledInput } from '@/components/ui/controlled-input'
54+
import { ControlledSelect } from '@/components/ui/controlled-select'
55+
import { useForm, FormProvider } from 'react-hook-form'
56+
57+
function MyForm() {
58+
const methods = useForm()
59+
60+
return (
61+
<FormProvider {...methods}>
62+
<form>
63+
<ControlledInput
64+
name="email"
65+
label="Email"
66+
placeholder="Enter your email"
67+
/>
68+
69+
<ControlledSelect
70+
name="country"
71+
label="Country"
72+
options={[
73+
{ label: 'United States', value: 'us' },
74+
{ label: 'Canada', value: 'ca' },
75+
]}
76+
/>
77+
</form>
78+
</FormProvider>
79+
)
80+
}
81+
```
82+
83+
## Component Dependencies
84+
85+
The registry properly handles component dependencies:
86+
87+
- Controlled components depend on their base UI components
88+
- UI components depend on `field-wrapper` when needed
89+
- `field-wrapper` depends on `field-error` and `label`
90+
- All components use `@medusajs/ui` for base styling
91+
92+
## Architecture
93+
94+
### Field Wrapper Pattern
95+
96+
All form components use a consistent wrapper pattern:
97+
98+
- `FieldWrapper` provides consistent layout and error handling
99+
- `Label` component handles labels and tooltips
100+
- `FieldError` handles error message display
101+
- UI components wrap `@medusajs/ui` components
102+
103+
### Controlled Component Pattern
104+
105+
Controlled components use react-hook-form:
106+
107+
- Uses `Controller` from react-hook-form
108+
- Integrates with form context
109+
- Handles form validation and errors
110+
- Preserves component props and types
111+
112+
## Types and Interfaces
113+
114+
- `BasicFieldProps` - Common field properties
115+
- `FieldWrapperProps` - Wrapper component props
116+
- Component-specific props (`InputProps`, `SelectProps`, etc.)
117+
- React Hook Form integration types
118+

packages/medusa-forms/registry.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"$schema": "https://ui.shadcn.com/schema.json",
3+
"name": "medusa-forms",
4+
"description": "Controlled form fields for Medusa Admin and Medusa UI",
5+
"url": "https://raw.githubusercontent.com/lambda-curry/medusa-forms/main/packages/medusa-forms",
6+
"style": "default",
7+
"tailwind": {
8+
"config": "tailwind.config.js",
9+
"css": "src/styles/globals.css",
10+
"baseColor": "slate",
11+
"cssVariables": true
12+
},
13+
"aliases": {
14+
"components": "src/components",
15+
"utils": "src/lib/utils"
16+
}
17+
}
18+
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"name": "controlled-checkbox",
3+
"type": "registry:ui",
4+
"description": "A checkbox component with react-hook-form integration",
5+
"dependencies": [
6+
"react-hook-form"
7+
],
8+
"registryDependencies": [
9+
"field-checkbox"
10+
],
11+
"files": [
12+
{
13+
"name": "controlled-checkbox.tsx",
14+
"content": "import {\n Controller,\n type ControllerProps,\n type FieldValues,\n type Path,\n type RegisterOptions,\n useFormContext,\n} from 'react-hook-form';\nimport { FieldCheckbox, type FieldCheckboxProps } from '../ui/FieldCheckbox';\n\nexport type ControlledCheckboxProps<T extends FieldValues> = Omit<FieldCheckboxProps, 'name'> &\n Omit<ControllerProps, 'render'> & {\n name: Path<T>;\n rules?: Omit<RegisterOptions<T, Path<T>>, 'disabled' | 'valueAsNumber' | 'valueAsDate' | 'setValueAs'>;\n };\n\nexport const ControlledCheckbox = <T extends FieldValues>({\n name,\n rules,\n onChange,\n ...props\n}: ControlledCheckboxProps<T>) => {\n const {\n control,\n formState: { errors },\n } = useFormContext<T>();\n\n return (\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 <FieldCheckbox\n {...field}\n {...props}\n formErrors={errors}\n checked={field.value}\n onChange={(checked) => {\n if (onChange) onChange(checked);\n field.onChange(checked);\n }}\n />\n )}\n />\n );\n};\n\n"
15+
}
16+
]
17+
}
18+
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"name": "controlled-currency-input",
3+
"type": "registry:ui",
4+
"description": "A currency-input component with react-hook-form integration",
5+
"dependencies": [
6+
"react-hook-form"
7+
],
8+
"registryDependencies": [
9+
"currency-input"
10+
],
11+
"files": [
12+
{
13+
"name": "controlled-currency-input.tsx",
14+
"content": "import type * as React 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, type CurrencyInputProps } from '../ui/CurrencyInput';\n\nexport type ControlledCurrencyInputProps<T extends FieldValues> = CurrencyInputProps &\n Omit<ControllerProps, 'render' | 'control'> & {\n name: Path<T>;\n };\n\nexport const ControlledCurrencyInput = <T extends FieldValues>({\n name,\n rules,\n ...props\n}: ControlledCurrencyInputProps<T>) => {\n const { control } = useFormContext<T>();\n\n return (\n <Controller<T>\n control={control}\n name={name}\n rules={rules as Omit<RegisterOptions<T, Path<T>>, 'disabled' | 'valueAsNumber' | 'valueAsDate' | 'setValueAs'>}\n render={({ field }) => {\n return (\n <CurrencyInput\n {...field}\n {...props}\n onChange={(e: React.ChangeEvent<HTMLInputElement>) => {\n field.onChange(e.target.value.replace(/[^0-9.-]+/g, ''));\n }}\n />\n );\n }}\n />\n );\n};\n\n"
15+
}
16+
]
17+
}
18+
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"name": "controlled-datepicker",
3+
"type": "registry:ui",
4+
"description": "A datepicker component with react-hook-form integration",
5+
"dependencies": [
6+
"react-hook-form"
7+
],
8+
"registryDependencies": [
9+
"datepicker"
10+
],
11+
"files": [
12+
{
13+
"name": "controlled-datepicker.tsx",
14+
"content": "import {\n Controller,\n type ControllerProps,\n type FieldValues,\n type Path,\n type RegisterOptions,\n useFormContext,\n} from 'react-hook-form';\nimport { DatePickerInput, type DatePickerProps } from '../ui/DatePicker';\n\nexport type ControlledDatePickerProps<T extends FieldValues> = DatePickerProps &\n Omit<ControllerProps, 'render' | 'control'> & {\n name: Path<T>;\n };\n\nexport const ControlledDatePicker = <T extends FieldValues>({\n name,\n rules,\n ...props\n}: ControlledDatePickerProps<T>) => {\n const { control } = useFormContext<T>();\n return (\n <Controller<T>\n control={control}\n name={name}\n rules={rules as Omit<RegisterOptions<T, Path<T>>, 'disabled' | 'valueAsNumber' | 'valueAsDate' | 'setValueAs'>}\n render={({ field }) => <DatePickerInput {...field} {...props} />}\n />\n );\n};\n\n"
15+
}
16+
]
17+
}
18+
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"name": "controlled-input",
3+
"type": "registry:ui",
4+
"description": "An input component with react-hook-form integration",
5+
"dependencies": [
6+
"react-hook-form"
7+
],
8+
"registryDependencies": [
9+
"input"
10+
],
11+
"files": [
12+
{
13+
"name": "controlled-input.tsx",
14+
"content": "import 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 { Input, type InputProps } from '../ui/Input';\n\nexport type ControlledInputProps<T extends FieldValues> = InputProps &\n Omit<ControllerProps, 'render'> & {\n name: Path<T>;\n rules?: Omit<RegisterOptions<T, Path<T>>, 'disabled' | 'valueAsNumber' | 'valueAsDate' | 'setValueAs'>;\n } & ComponentProps<typeof Input> &\n Omit<ControllerProps<T>, 'render'>;\n\nexport const ControlledInput = <T extends FieldValues>({\n name,\n rules,\n onChange,\n ...props\n}: ControlledInputProps<T>) => {\n const {\n control,\n formState: { errors },\n } = useFormContext<T>();\n\n return (\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 <Input\n {...field}\n {...props}\n labelClassName={props.labelClassName}\n formErrors={errors}\n onChange={(evt) => {\n if (onChange) {\n onChange(evt);\n }\n field.onChange(evt);\n }}\n />\n )}\n />\n );\n};\n\n"
15+
}
16+
]
17+
}
18+
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"name": "controlled-select",
3+
"type": "registry:ui",
4+
"description": "A select component with react-hook-form integration",
5+
"dependencies": [
6+
"react-hook-form"
7+
],
8+
"registryDependencies": [
9+
"select"
10+
],
11+
"files": [
12+
{
13+
"name": "controlled-select.tsx",
14+
"content": "import type * as React from 'react';\nimport {\n Controller,\n type ControllerProps,\n type FieldPathValue,\n type FieldValues,\n type Path,\n type RegisterOptions,\n useFormContext,\n} from 'react-hook-form';\nimport { Select, type SelectProps } from '../ui/Select';\n\nexport type ControlledSelectProps<T extends FieldValues> = SelectProps &\n Omit<ControllerProps, 'render'> & {\n name: Path<T>;\n onBlur?: () => void;\n onChange?: (value: unknown) => void;\n } & (\n | {\n options: { label: React.ReactNode; value: FieldPathValue<T, Path<T>> }[];\n children?: never;\n }\n | {\n options?: never;\n children: React.ReactNode;\n }\n );\n\nexport const ControlledSelect = <T extends FieldValues>({\n name,\n rules,\n children,\n options,\n onChange,\n onBlur,\n ...props\n}: ControlledSelectProps<T>) => {\n const { control } = useFormContext<T>();\n return (\n <Controller<T>\n control={control}\n name={name}\n rules={rules as Omit<RegisterOptions<T, Path<T>>, 'disabled' | 'valueAsNumber' | 'valueAsDate' | 'setValueAs'>}\n render={({ field }) => {\n const handleChange = (value: unknown) => {\n if (typeof onChange === 'function') onChange(value);\n field.onChange(value);\n };\n\n if (options) {\n return (\n <Select {...({ ...field, ...props, onValueChange: handleChange } as SelectProps)}>\n <Select.Trigger>\n <Select.Value />\n </Select.Trigger>\n <Select.Content>\n {options.map((option) => (\n <Select.Item key={option.value} value={option.value}>\n {option.label}\n </Select.Item>\n ))}\n </Select.Content>\n </Select>\n );\n }\n\n return <Select {...({ ...field, ...props, onValueChange: handleChange } as SelectProps)}>{children}</Select>;\n }}\n />\n );\n};\n\n"
15+
}
16+
]
17+
}
18+
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"name": "controlled-textarea",
3+
"type": "registry:ui",
4+
"description": "A textarea component with react-hook-form integration",
5+
"dependencies": [
6+
"react-hook-form"
7+
],
8+
"registryDependencies": [
9+
"textarea"
10+
],
11+
"files": [
12+
{
13+
"name": "controlled-textarea.tsx",
14+
"content": "import type * as React 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 { TextArea, type TextAreaProps } from '../ui/TextArea';\n\nexport type ControlledTextAreaProps<T extends FieldValues> = TextAreaProps &\n Omit<ControllerProps, 'render'> & {\n name: Path<T>;\n rules?: RegisterOptions<T, Path<T>>;\n } & React.ComponentProps<typeof TextArea> &\n Omit<ControllerProps<T>, 'render'>;\n\nexport const ControlledTextArea = <T extends FieldValues>({ name, rules, ...props }: ControlledTextAreaProps<T>) => {\n const { control } = useFormContext<T>();\n return (\n <Controller<T>\n control={control}\n name={name}\n rules={rules}\n render={({ field }) => <TextArea {...field} {...props} />}\n />\n );\n};\n\n"
15+
}
16+
]
17+
}
18+
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"name": "currency-input",
3+
"type": "registry:ui",
4+
"description": "currency-input component",
5+
"dependencies": [
6+
"@medusajs/ui"
7+
],
8+
"registryDependencies": [
9+
"field-wrapper"
10+
],
11+
"files": [
12+
{
13+
"name": "currency-input.tsx",
14+
"content": "import { CurrencyInput as MedusaCurrencyInput } from '@medusajs/ui';\nimport { forwardRef } from 'react';\nimport { FieldWrapper } from './FieldWrapper';\nimport type { BasicFieldProps, MedusaCurrencyInputProps } from './types';\n\nexport type CurrencyInputProps = MedusaCurrencyInputProps & BasicFieldProps;\n\nconst Wrapper = FieldWrapper<CurrencyInputProps>;\n\nexport const CurrencyInput = forwardRef<HTMLInputElement, CurrencyInputProps>((props, ref) => (\n <Wrapper {...props}>{(inputProps) => <MedusaCurrencyInput {...inputProps} ref={ref} />}</Wrapper>\n));\n\nCurrencyInput.displayName = 'CurrencyInput';\n"
15+
}
16+
]
17+
}
18+
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"name": "datepicker",
3+
"type": "registry:ui",
4+
"description": "datepicker component",
5+
"dependencies": [
6+
"@medusajs/ui"
7+
],
8+
"registryDependencies": [
9+
"field-wrapper"
10+
],
11+
"files": [
12+
{
13+
"name": "datepicker.tsx",
14+
"content": "import { DatePicker } from '@medusajs/ui';\nimport { forwardRef } from 'react';\nimport { FieldWrapper } from './FieldWrapper';\nimport type { BasicFieldProps, DatePickerProps as MedusaDatePickerProps } from './types';\n\nexport type DatePickerProps = MedusaDatePickerProps & BasicFieldProps;\n\nconst Wrapper = FieldWrapper<DatePickerProps>;\n\nexport const DatePickerInput = forwardRef<HTMLInputElement, DatePickerProps>((props, ref) => {\n return <Wrapper {...props}>{(inputProps) => <DatePicker {...{ ...inputProps, ref }} />}</Wrapper>;\n});\n\nDatePickerInput.displayName = 'DatePickerInput';\n"
15+
}
16+
]
17+
}
18+

0 commit comments

Comments
 (0)