Skip to content

Commit 63a0ed6

Browse files
refactor: consolidate to single controlled directory with shadcn compatibility
- Update existing controlled components to be shadcn-compatible - Remove duplicate src/registry/ directory - single source of truth - Components now use @medusajs/ui directly instead of internal wrappers - Add built-in labels, error handling with @hookform/error-message - Update registry.json to point to src/controlled/ components - Regenerate individual JSON registry files with updated content - Update documentation to reflect unified approach - Components serve both shadcn CLI and npm package usage
1 parent fb8e091 commit 63a0ed6

14 files changed

Lines changed: 567 additions & 842 deletions

packages/medusa-forms/REGISTRY.md

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ Before using these components, make sure you have:
149149
**Zero Maintenance**: No CLI package to maintain
150150
**Instant Adoption**: Works with existing shadcn projects
151151
**v0 Compatible**: Components work with v0.dev
152-
**Monorepo Structure**: Registry is properly nested within the medusa-forms package
152+
**Single Source of Truth**: Components serve both shadcn and npm package usage
153153

154154
## Registry Structure
155155

@@ -163,14 +163,7 @@ packages/medusa-forms/
163163
│ ├── controlled-textarea.json
164164
│ ├── controlled-datepicker.json
165165
│ └── controlled-currency-input.json
166-
├── src/registry/ # Registry-compatible component source files
167-
│ ├── controlled-input.tsx
168-
│ ├── controlled-select.tsx
169-
│ ├── controlled-checkbox.tsx
170-
│ ├── controlled-textarea.tsx
171-
│ ├── controlled-datepicker.tsx
172-
│ └── controlled-currency-input.tsx
173-
└── src/controlled/ # Original medusa-forms components (unchanged)
166+
└── src/controlled/ # Unified component source files (shadcn-compatible)
174167
├── ControlledInput.tsx
175168
├── ControlledSelect.tsx
176169
├── ControlledCheckbox.tsx
@@ -179,22 +172,24 @@ packages/medusa-forms/
179172
└── ControlledCurrencyInput.tsx
180173
```
181174

182-
## Relationship to Existing Components
175+
## Unified Approach
183176

184-
The registry components in `src/registry/` are **shadcn-compatible versions** of the existing medusa-forms components in `src/controlled/`. Key differences:
177+
The components in `src/controlled/` are now **shadcn-compatible** and serve dual purposes:
185178

186-
- **Registry components**: Use `@medusajs/ui` directly, include built-in labels and error handling, follow shadcn patterns
187-
- **Original components**: Use internal UI wrappers (`FieldWrapper`, etc.), part of the medusa-forms package API
179+
- **For shadcn CLI users**: Install via `npx shadcn@latest add [URL]`
180+
- **For npm package users**: Import directly from `@lambdacurry/medusa-forms`
188181

189-
Both sets of components serve different use cases:
190-
- Use **registry components** when installing via shadcn CLI into your project
191-
- Use **original components** when importing the `@lambdacurry/medusa-forms` package directly
182+
### Key Features:
183+
- **Direct @medusajs/ui usage**: Components use `@medusajs/ui` directly instead of internal wrappers
184+
- **Built-in labels and error handling**: Includes proper labeling and error display with `@hookform/error-message`
185+
- **shadcn patterns**: Follows shadcn/ui conventions for styling and structure
186+
- **Backward compatibility**: Maintains the same API for existing medusa-forms users
192187

193188
## Contributing
194189

195190
To add new components to the registry:
196191

197-
1. Create the shadcn-compatible component in `src/registry/`
192+
1. Create the component in `src/controlled/` following shadcn patterns
198193
2. Add it to the main `registry.json`
199194
3. Generate the individual JSON file in `registry/`
200195
4. Update this documentation

packages/medusa-forms/registry.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"registryDependencies": [],
1414
"files": [
1515
{
16-
"path": "packages/medusa-forms/src/registry/controlled-input.tsx",
16+
"path": "packages/medusa-forms/src/controlled/ControlledInput.tsx",
1717
"type": "registry:component"
1818
}
1919
]
@@ -27,7 +27,7 @@
2727
"registryDependencies": [],
2828
"files": [
2929
{
30-
"path": "packages/medusa-forms/src/registry/controlled-select.tsx",
30+
"path": "packages/medusa-forms/src/controlled/ControlledSelect.tsx",
3131
"type": "registry:component"
3232
}
3333
]
@@ -41,7 +41,7 @@
4141
"registryDependencies": [],
4242
"files": [
4343
{
44-
"path": "packages/medusa-forms/src/registry/controlled-checkbox.tsx",
44+
"path": "packages/medusa-forms/src/controlled/ControlledCheckbox.tsx",
4545
"type": "registry:component"
4646
}
4747
]
@@ -55,7 +55,7 @@
5555
"registryDependencies": [],
5656
"files": [
5757
{
58-
"path": "packages/medusa-forms/src/registry/controlled-textarea.tsx",
58+
"path": "packages/medusa-forms/src/controlled/ControlledTextArea.tsx",
5959
"type": "registry:component"
6060
}
6161
]
@@ -69,7 +69,7 @@
6969
"registryDependencies": [],
7070
"files": [
7171
{
72-
"path": "packages/medusa-forms/src/registry/controlled-datepicker.tsx",
72+
"path": "packages/medusa-forms/src/controlled/ControlledDatePicker.tsx",
7373
"type": "registry:component"
7474
}
7575
]
@@ -83,7 +83,7 @@
8383
"registryDependencies": [],
8484
"files": [
8585
{
86-
"path": "packages/medusa-forms/src/registry/controlled-currency-input.tsx",
86+
"path": "packages/medusa-forms/src/controlled/ControlledCurrencyInput.tsx",
8787
"type": "registry:component"
8888
}
8989
]
Lines changed: 92 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,112 @@
1+
"use client"
2+
3+
import type { ComponentProps } from 'react'
14
import {
25
Controller,
36
type ControllerProps,
47
type FieldValues,
58
type Path,
69
type RegisterOptions,
710
useFormContext,
8-
} from 'react-hook-form';
9-
import { FieldCheckbox, type FieldCheckboxProps } from '../ui/FieldCheckbox';
11+
} from 'react-hook-form'
12+
import { Checkbox, Label } from '@medusajs/ui'
13+
import { ErrorMessage } from '@hookform/error-message'
1014

11-
export type ControlledCheckboxProps<T extends FieldValues> = Omit<FieldCheckboxProps, 'name'> &
12-
Omit<ControllerProps, 'render'> & {
13-
name: Path<T>;
14-
rules?: Omit<RegisterOptions<T, Path<T>>, 'disabled' | 'valueAsNumber' | 'valueAsDate' | 'setValueAs'>;
15-
};
15+
export type ControlledCheckboxProps<T extends FieldValues> = Omit<ControllerProps, 'render'> & {
16+
name: Path<T>
17+
rules?: Omit<RegisterOptions<T, Path<T>>, 'disabled' | 'valueAsNumber' | 'valueAsDate' | 'setValueAs'>
18+
label?: string
19+
description?: string
20+
required?: boolean
21+
disabled?: boolean
22+
className?: string
23+
onChange?: (checked: boolean) => void
24+
} & ComponentProps<typeof Checkbox>
1625

26+
/**
27+
* A controlled checkbox component that integrates with react-hook-form.
28+
*
29+
* @example
30+
* ```tsx
31+
* import { useForm, FormProvider } from 'react-hook-form'
32+
* import { ControlledCheckbox } from '@/components/ui/controlled-checkbox'
33+
*
34+
* function MyForm() {
35+
* const methods = useForm()
36+
*
37+
* return (
38+
* <FormProvider {...methods}>
39+
* <form onSubmit={methods.handleSubmit(onSubmit)}>
40+
* <ControlledCheckbox
41+
* name="terms"
42+
* label="I agree to the terms and conditions"
43+
* rules={{ required: 'You must agree to the terms' }}
44+
* />
45+
* </form>
46+
* </FormProvider>
47+
* )
48+
* }
49+
* ```
50+
*/
1751
export const ControlledCheckbox = <T extends FieldValues>({
1852
name,
1953
rules,
2054
onChange,
55+
label,
56+
description,
57+
required,
2158
...props
2259
}: ControlledCheckboxProps<T>) => {
2360
const {
2461
control,
2562
formState: { errors },
26-
} = useFormContext<T>();
63+
} = useFormContext<T>()
2764

2865
return (
29-
<Controller
30-
control={control}
31-
name={name}
32-
rules={rules as Omit<RegisterOptions<T, Path<T>>, 'disabled' | 'valueAsNumber' | 'valueAsDate' | 'setValueAs'>}
33-
render={({ field }) => (
34-
<FieldCheckbox
35-
{...field}
36-
{...props}
37-
formErrors={errors}
38-
checked={field.value}
39-
onChange={(checked) => {
40-
if (onChange) onChange(checked);
41-
field.onChange(checked);
42-
}}
43-
/>
44-
)}
45-
/>
46-
);
47-
};
66+
<div className="space-y-2">
67+
<Controller
68+
control={control}
69+
name={name}
70+
rules={rules as Omit<RegisterOptions<T, Path<T>>, 'disabled' | 'valueAsNumber' | 'valueAsDate' | 'setValueAs'>}
71+
render={({ field }) => (
72+
<div className="flex items-start space-x-2">
73+
<Checkbox
74+
{...field}
75+
{...props}
76+
id={name}
77+
checked={field.value}
78+
onCheckedChange={(checked) => {
79+
const booleanValue = checked === true
80+
if (onChange) {
81+
onChange(booleanValue)
82+
}
83+
field.onChange(booleanValue)
84+
}}
85+
/>
86+
<div className="grid gap-1.5 leading-none">
87+
{label && (
88+
<Label htmlFor={name}>
89+
{label}
90+
{required && <span className="text-red-500 ml-1">*</span>}
91+
</Label>
92+
)}
93+
{description && (
94+
<p className="text-xs text-muted-foreground">
95+
{description}
96+
</p>
97+
)}
98+
</div>
99+
</div>
100+
)}
101+
/>
102+
<ErrorMessage
103+
errors={errors}
104+
name={name}
105+
render={({ message }) => (
106+
<p className="text-sm text-red-500">{message}</p>
107+
)}
108+
/>
109+
</div>
110+
)
111+
}
112+
Lines changed: 83 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,104 @@
1-
import type * as React from 'react';
1+
"use client"
2+
3+
import type { ComponentProps } from 'react'
24
import {
35
Controller,
46
type ControllerProps,
57
type FieldValues,
68
type Path,
79
type RegisterOptions,
810
useFormContext,
9-
} from 'react-hook-form';
10-
import { CurrencyInput, type CurrencyInputProps } from '../ui/CurrencyInput';
11+
} from 'react-hook-form'
12+
import { CurrencyInput, Label } from '@medusajs/ui'
13+
import { ErrorMessage } from '@hookform/error-message'
1114

12-
export type ControlledCurrencyInputProps<T extends FieldValues> = CurrencyInputProps &
13-
Omit<ControllerProps, 'render' | 'control'> & {
14-
name: Path<T>;
15-
};
15+
export type ControlledCurrencyInputProps<T extends FieldValues> = Omit<ControllerProps, 'render'> & {
16+
name: Path<T>
17+
rules?: Omit<RegisterOptions<T, Path<T>>, 'disabled' | 'valueAsNumber' | 'valueAsDate' | 'setValueAs'>
18+
label?: string
19+
placeholder?: string
20+
required?: boolean
21+
disabled?: boolean
22+
className?: string
23+
currency?: string
24+
onChange?: (value: number | undefined) => void
25+
} & ComponentProps<typeof CurrencyInput>
1626

27+
/**
28+
* A controlled currency input component that integrates with react-hook-form.
29+
*
30+
* @example
31+
* ```tsx
32+
* import { useForm, FormProvider } from 'react-hook-form'
33+
* import { ControlledCurrencyInput } from '@/components/ui/controlled-currency-input'
34+
*
35+
* function MyForm() {
36+
* const methods = useForm()
37+
*
38+
* return (
39+
* <FormProvider {...methods}>
40+
* <form onSubmit={methods.handleSubmit(onSubmit)}>
41+
* <ControlledCurrencyInput
42+
* name="price"
43+
* label="Price"
44+
* placeholder="Enter price"
45+
* currency="USD"
46+
* rules={{ required: 'Price is required' }}
47+
* />
48+
* </form>
49+
* </FormProvider>
50+
* )
51+
* }
52+
* ```
53+
*/
1754
export const ControlledCurrencyInput = <T extends FieldValues>({
1855
name,
1956
rules,
57+
onChange,
58+
label,
59+
required,
2060
...props
2161
}: ControlledCurrencyInputProps<T>) => {
22-
const { control } = useFormContext<T>();
62+
const {
63+
control,
64+
formState: { errors },
65+
} = useFormContext<T>()
2366

2467
return (
25-
<Controller<T>
26-
control={control}
27-
name={name}
28-
rules={rules as Omit<RegisterOptions<T, Path<T>>, 'disabled' | 'valueAsNumber' | 'valueAsDate' | 'setValueAs'>}
29-
render={({ field }) => {
30-
return (
68+
<div className="space-y-2">
69+
{label && (
70+
<Label htmlFor={name}>
71+
{label}
72+
{required && <span className="text-red-500 ml-1">*</span>}
73+
</Label>
74+
)}
75+
<Controller
76+
control={control}
77+
name={name}
78+
rules={rules as Omit<RegisterOptions<T, Path<T>>, 'disabled' | 'valueAsNumber' | 'valueAsDate' | 'setValueAs'>}
79+
render={({ field }) => (
3180
<CurrencyInput
3281
{...field}
3382
{...props}
34-
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
35-
field.onChange(e.target.value.replace(/[^0-9.-]+/g, ''));
83+
id={name}
84+
value={field.value}
85+
onValueChange={(value) => {
86+
if (onChange) {
87+
onChange(value)
88+
}
89+
field.onChange(value)
3690
}}
3791
/>
38-
);
39-
}}
40-
/>
41-
);
42-
};
92+
)}
93+
/>
94+
<ErrorMessage
95+
errors={errors}
96+
name={name}
97+
render={({ message }) => (
98+
<p className="text-sm text-red-500">{message}</p>
99+
)}
100+
/>
101+
</div>
102+
)
103+
}
104+

0 commit comments

Comments
 (0)