diff --git a/.cursor/rules/form-component-patterns.mdc b/.cursor/rules/form-component-patterns.mdc index 75c41cb5..5e61c546 100644 --- a/.cursor/rules/form-component-patterns.mdc +++ b/.cursor/rules/form-component-patterns.mdc @@ -1,11 +1,13 @@ --- type: Always -description: Rules for form component integration patterns in the lambda-curry/forms repository +description: Rules for form component integration patterns in the lambda-curry/forms repository (applies to Remix Hook Form components) --- You are an expert in React Hook Form, Remix Hook Form, Zod validation, and form component architecture for the lambda-curry/forms monorepo. -# Form Component Integration Patterns +# Form Component Integration Patterns (Remix Hook Form) + +**Note: These patterns apply to Remix Hook Form components in `packages/components/`. For Medusa Forms components using react-hook-form + @medusajs/ui, see the medusa-forms-patterns.mdc rules.** ## Core Principles - All form components must integrate seamlessly with Remix Hook Form @@ -215,4 +217,3 @@ export type { ComponentNameProps }; - Test component composition and customization Remember: Form components are the core of this library. Every form component should be intuitive, accessible, and integrate seamlessly with the Remix Hook Form + Zod validation pattern. - diff --git a/.cursor/rules/medusa-forms-patterns.mdc b/.cursor/rules/medusa-forms-patterns.mdc new file mode 100644 index 00000000..98549311 --- /dev/null +++ b/.cursor/rules/medusa-forms-patterns.mdc @@ -0,0 +1,252 @@ +--- +type: Auto Attached +description: Rules for Medusa Forms component development patterns using @medusajs/ui and react-hook-form +globs: ["packages/medusa-forms/**/*.{ts,tsx}", "apps/docs/src/medusa-forms/**/*.{ts,tsx}"] +--- + +You are an expert in React Hook Form, @medusajs/ui components, and Medusa design system integration for the lambda-curry/forms repository. + +# Medusa Forms Component Patterns + +## Core Architecture Principles +- Medusa Forms use **react-hook-form** directly (not remix-hook-form) +- All UI components are built on **@medusajs/ui** as the base design system +- Follow the **controlled/** and **ui/** directory separation pattern +- Use the **Controller** pattern for form integration +- Maintain **FieldWrapper** consistency for all form fields + +## Required Imports for Medusa Forms + +### For Controlled Components +```typescript +import { + Controller, + type ControllerProps, + type FieldValues, + type Path, + type RegisterOptions, + useFormContext, +} from 'react-hook-form'; +import { ComponentName, type Props as ComponentNameProps } from '../ui/ComponentName'; +``` + +### For UI Components +```typescript +import { ComponentName as MedusaComponentName } from '@medusajs/ui'; +import type * as React from 'react'; +import { FieldWrapper } from './FieldWrapper'; +import type { BasicFieldProps, MedusaComponentNameProps } from './types'; +``` + +## Directory Structure Convention +``` +packages/medusa-forms/src/ +├── controlled/ # Form-aware wrapper components using Controller +│ ├── ControlledInput.tsx +│ ├── ControlledCheckbox.tsx +│ ├── ControlledSelect.tsx +│ └── index.ts +└── ui/ # Base UI components using @medusajs/ui + ├── Input.tsx + ├── FieldCheckbox.tsx + ├── Select.tsx + ├── FieldWrapper.tsx + └── types.d.ts +``` + +## Controlled Component Pattern +All controlled components must follow this exact pattern: + +```typescript +type Props = ComponentNameProps & + Omit & { + name: Path; + rules?: Omit>, 'disabled' | 'valueAsNumber' | 'valueAsDate' | 'setValueAs'>; + }; + +export const ControlledComponentName = ({ + name, + rules, + onChange, + ...props +}: Props) => { + const { + control, + formState: { errors }, + } = useFormContext(); + + return ( + >, 'disabled' | 'valueAsNumber' | 'valueAsDate' | 'setValueAs'>} + render={({ field }) => ( + { + if (onChange) onChange(value); + field.onChange(value); + }} + /> + )} + /> + ); +}; +``` + +## UI Component Pattern +All UI components must use FieldWrapper and @medusajs/ui: + +```typescript +export type Props = MedusaComponentNameProps & + BasicFieldProps & { + ref?: React.Ref; // Adjust ref type based on component + }; + +const Wrapper = FieldWrapper; + +export const ComponentName: React.FC = ({ ref, ...props }) => ( + + {(inputProps) => } + +); +``` + +## FieldWrapper Integration +- **Always** use FieldWrapper for consistent label, error, and styling patterns +- Pass `formErrors` prop to enable automatic error display +- Use `labelClassName`, `wrapperClassName`, `errorClassName` for styling customization + +```typescript + + wrapperClassName={wrapperClassName} + errorClassName={errorClassName} + formErrors={formErrors} + {...props} +> + {(fieldProps) => ( + + )} + +``` + +## @medusajs/ui Component Integration + +### Input Components +```typescript +import { Input as MedusaInput } from '@medusajs/ui'; +// Use with FieldWrapper pattern +``` + +### Checkbox Components +```typescript +import { Checkbox as MedusaCheckbox } from '@medusajs/ui'; +// Special handling for checked state and onCheckedChange +``` + +### Select Components +```typescript +import { Select as MedusaSelect } from '@medusajs/ui'; +// Compound component pattern with Trigger, Content, Item +``` + +### Currency Input Components +```typescript +import { CurrencyInput as MedusaCurrencyInput } from '@medusajs/ui'; +// Special props: symbol, code, currency +``` + +### Date Picker Components +```typescript +import { DatePicker } from '@medusajs/ui'; +// Special props: dateFormat, minDate, maxDate, filterDate +``` + +## Type Safety Requirements +- Use generic types `` for all controlled components +- Properly type `Path` for name props +- Extend `BasicFieldProps` for all UI components +- Use proper ref types based on underlying HTML element + +## Error Handling Pattern +```typescript +// In controlled components +const { + control, + formState: { errors }, +} = useFormContext(); + +// Pass errors to UI component + +``` + +## Validation Integration +- Use `rules` prop for react-hook-form validation +- Support both built-in and custom validation rules +- Ensure error messages are user-friendly and specific + +```typescript + +``` + +## Accessibility Requirements +- All form fields must have proper labels via FieldWrapper +- Use ARIA attributes provided by @medusajs/ui components +- Ensure keyboard navigation works correctly +- Provide clear error announcements for screen readers + +## Component Naming Conventions +- Controlled components: `ControlledComponentName` (e.g., `ControlledInput`, `ControlledCheckbox`) +- UI components: `ComponentName` (e.g., `Input`, `FieldCheckbox`) +- Props interfaces: `ComponentNameProps` +- File names: PascalCase matching component name + +## Export Requirements +Always export both the component and its props type: +```typescript +export { ControlledComponentName }; +export type { Props as ControlledComponentNameProps }; +``` + +## Performance Considerations +- Use React.memo for expensive form components when needed +- Avoid unnecessary re-renders by properly structuring form state +- Consider field-level subscriptions for large forms + +## Testing Integration +- Components should work with existing Storybook patterns +- Test both valid and invalid form states +- Verify @medusajs/ui component integration +- Test component composition and customization + +## Common Patterns to Avoid +- **Don't** use remix-hook-form patterns (use react-hook-form directly) +- **Don't** create custom UI components when @medusajs/ui equivalents exist +- **Don't** bypass FieldWrapper for form fields +- **Don't** mix controlled and uncontrolled patterns +- **Don't** forget to handle both onChange and field.onChange in controlled components + +## Medusa Design System Compliance +- Follow Medusa UI spacing and sizing conventions +- Use Medusa color tokens and design patterns +- Ensure components work with Medusa themes +- Maintain consistency with Medusa component APIs + +Remember: Medusa Forms are specifically designed to integrate with the Medusa ecosystem. Always prioritize @medusajs/ui component usage and follow Medusa design system principles while maintaining the react-hook-form integration patterns. + diff --git a/.cursor/rules/medusa-stories-patterns.mdc b/.cursor/rules/medusa-stories-patterns.mdc new file mode 100644 index 00000000..1ec58236 --- /dev/null +++ b/.cursor/rules/medusa-stories-patterns.mdc @@ -0,0 +1,349 @@ +--- +type: Auto Attached +description: Rules for creating Storybook stories for Medusa Forms components using react-hook-form patterns +globs: ["apps/docs/src/medusa-forms/**/*.stories.{ts,tsx}"] +--- + +You are an expert in Storybook, React Hook Form, and @medusajs/ui component documentation for the lambda-curry/forms repository. + +# Medusa Forms Storybook Patterns + +## Core Story Principles +- Use **react-hook-form** directly with **FormProvider** and **useForm** +- Create comprehensive examples showcasing **@medusajs/ui** integration +- Follow **Medusa design system** patterns and conventions +- Provide **realistic use cases** and **validation examples** +- Ensure stories work independently without external dependencies + +## Required Imports for Medusa Stories +```typescript +import { ControlledComponentName } from '@lambdacurry/medusa-forms/controlled/ControlledComponentName'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import React from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; // When using validation +import { z } from 'zod'; // When using validation +``` + +## Story Meta Configuration +```typescript +const meta = { + title: 'Medusa Forms/Controlled ComponentName', + component: ControlledComponentName, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; +``` + +## Basic Story Pattern +Every component should have a basic usage story: + +```typescript +const BasicComponentForm = () => { + const form = useForm({ + defaultValues: { + fieldName: '', // Provide appropriate default + }, + }); + + return ( + +
+ +
+ Current value: {JSON.stringify(form.watch('fieldName'))} +
+
+
+ ); +}; + +export const BasicUsage: Story = { + render: () => , +}; +``` + +## Validation Story Pattern +Include validation examples with clear error states: + +```typescript +const validationSchema = z.object({ + fieldName: z.string().min(1, 'Field is required'), +}); + +const ValidationForm = () => { + const form = useForm({ + resolver: zodResolver(validationSchema), + defaultValues: { + fieldName: '', + }, + mode: 'onChange', // Show validation on change + }); + + const onSubmit = (data: any) => { + console.log('Form submitted:', data); + }; + + return ( + +
+ +
+ Form valid: {form.formState.isValid ? 'Yes' : 'No'} +
+ + +
+ ); +}; + +export const RequiredValidation: Story = { + render: () => , +}; +``` + +## Component-Specific Story Categories + +### Input Components +Required stories: +- **BasicUsage**: Simple input with value display +- **RequiredValidation**: Required field validation +- **CustomValidation**: Pattern/custom validation +- **ErrorState**: Manually triggered error state +- **DisabledState**: Disabled input demonstration + +### Checkbox Components +Required stories: +- **BasicUsage**: Simple checkbox +- **DefaultChecked**: Pre-checked state +- **DefaultUnchecked**: Unchecked state +- **RequiredValidation**: Required checkbox validation +- **MultipleCheckboxes**: Group management with select all + +### Select Components +Required stories: +- **BasicSingleSelect**: Simple dropdown +- **WithDefaultValue**: Pre-selected option +- **RequiredValidation**: Required selection +- **MultipleSelect**: Multi-selection mode +- **SearchableSelect**: With search functionality + +### Currency Input Components +Required stories: +- **USDCurrency**: Dollar symbol and formatting +- **EURCurrency**: Euro symbol and formatting +- **MinimumValueValidation**: Min value constraints +- **MaximumValueValidation**: Max value constraints +- **RangeValidation**: Min/max range validation + +### Date Picker Components +Required stories: +- **BasicDateSelection**: Simple date picker +- **RequiredFieldValidation**: Required date validation +- **DateFormatVariations**: Different format displays +- **DisabledDates**: Past/future date restrictions +- **MinMaxDateConstraints**: Date range limitations + +## Form Integration Examples +Always include a complete form integration story: + +```typescript +const CompleteFormExample = () => { + const form = useForm({ + defaultValues: { + // Include multiple field types + name: '', + email: '', + acceptTerms: false, + category: '', + }, + mode: 'onChange', + }); + + const onSubmit = (data: any) => { + alert(`Form submitted: ${JSON.stringify(data, null, 2)}`); + }; + + return ( + +
+ {/* Multiple component types */} + + + + + +
+ + +
+ +
+ ); +}; + +export const CompleteFormExample: Story = { + render: () => , +}; +``` + +## Story Data Patterns + +### Sample Options for Select Components +```typescript +const countryOptions = [ + { label: 'United States', value: 'us' }, + { label: 'Canada', value: 'ca' }, + { label: 'United Kingdom', value: 'uk' }, + // ... more options +]; + +const categoryOptions = [ + { label: 'Electronics', value: 'electronics' }, + { label: 'Clothing', value: 'clothing' }, + // ... more options +]; +``` + +### Validation Schemas +```typescript +// Simple validation +const basicSchema = z.object({ + field: z.string().min(1, 'Field is required'), +}); + +// Complex validation +const complexSchema = z.object({ + email: z.string().email('Invalid email address'), + password: z.string().min(8, 'Password must be at least 8 characters'), + confirmPassword: z.string(), +}).refine((data) => data.password === data.confirmPassword, { + message: "Passwords don't match", + path: ["confirmPassword"], +}); +``` + +## Error State Demonstrations +Show how errors appear and behave: + +```typescript +const ErrorStateForm = () => { + const form = useForm({ + defaultValues: { field: '' }, + }); + + // Manually trigger error for demonstration + React.useEffect(() => { + form.setError('field', { + type: 'manual', + message: 'This is an example error message' + }); + }, [form]); + + return ( + +
+ +
+
+ ); +}; +``` + +## Interactive State Display +Always show current form state for debugging: + +```typescript +
+ Current value: {JSON.stringify(form.watch('fieldName'))} +
+ +
+ Form valid: {form.formState.isValid ? 'Yes' : 'No'} +
+ +
+ Has errors: {Object.keys(form.formState.errors).length > 0 ? 'Yes' : 'No'} +
+``` + +## Styling Conventions +- Use consistent container widths: `w-[400px]` +- Apply consistent spacing: `space-y-4` +- Use Tailwind classes for quick styling +- Maintain Medusa design system color palette +- Ensure responsive design considerations + +## Story Naming Conventions +- Use PascalCase for story exports +- Be descriptive and specific +- Group related functionality +- Follow the pattern: `ComponentAction` (e.g., `BasicUsage`, `RequiredValidation`) + +## Args and Controls +For components with configurable props: + +```typescript +export const Configurable: Story = { + args: { + label: 'Configurable Field', + placeholder: 'Enter value...', + disabled: false, + }, + render: (args) => ( + + ), +}; +``` + +## Documentation Integration +- Use `tags: ['autodocs']` for automatic documentation +- Include JSDoc comments for complex examples +- Provide clear descriptions of what each story demonstrates +- Link to related Medusa UI documentation when relevant + +## Performance Considerations +- Keep story components lightweight +- Avoid unnecessary re-renders in story components +- Use React.memo for expensive story components +- Consider story loading performance for complex examples + +## Common Anti-Patterns to Avoid +- **Don't** use remix-hook-form patterns in stories +- **Don't** create stories without FormProvider wrapper +- **Don't** forget to show current form state +- **Don't** create stories that depend on external APIs +- **Don't** mix different form libraries in the same story +- **Don't** create overly complex stories that obscure the component's purpose + +## Testing Integration +- Stories should work with Storybook interaction testing +- Include play functions for automated testing when appropriate +- Ensure stories cover edge cases and error states +- Verify accessibility in story examples + +Remember: Medusa Forms stories should showcase the seamless integration between react-hook-form and @medusajs/ui components while providing practical, real-world examples that developers can reference and adapt. + diff --git a/.cursor/rules/monorepo-organization.mdc b/.cursor/rules/monorepo-organization.mdc index 299846f5..c444d744 100644 --- a/.cursor/rules/monorepo-organization.mdc +++ b/.cursor/rules/monorepo-organization.mdc @@ -1,6 +1,6 @@ --- description: Monorepo structure and import conventions for the lambda-curry/forms repository -globs: +globs: ["packages/**/*.{ts,tsx}", "apps/**/*.{ts,tsx}", "*.{ts,tsx,json}", "turbo.json", "package.json"] alwaysApply: false --- @@ -21,14 +21,21 @@ lambda-curry/forms/ ├── apps/ │ └── docs/ # Storybook documentation app │ ├── src/ -│ │ ├── remix-hook-form/ # Story files +│ │ ├── remix-hook-form/ # Remix Hook Form story files +│ │ ├── medusa-forms/ # Medusa Forms story files │ │ └── lib/ # Storybook utilities │ └── package.json ├── packages/ -│ └── components/ # Main component library +│ ├── components/ # Main component library (Remix Hook Form) +│ │ ├── src/ +│ │ │ ├── remix-hook-form/ # Form-aware components +│ │ │ ├── ui/ # Base UI components +│ │ │ └── index.ts # Main export file +│ │ └── package.json +│ └── medusa-forms/ # Medusa Forms component library │ ├── src/ -│ │ ├── remix-hook-form/ # Form-aware components -│ │ ├── ui/ # Base UI components +│ │ ├── controlled/ # Controlled components using react-hook-form +│ │ ├── ui/ # Base UI components using @medusajs/ui │ │ └── index.ts # Main export file │ └── package.json ├── package.json # Root package.json @@ -43,9 +50,13 @@ lambda-curry/forms/ import { TextField } from '@lambdacurry/forms/remix-hook-form'; import { Button } from '@lambdacurry/forms/ui'; +// ✅ Import from Medusa Forms package +import { ControlledInput } from '@lambdacurry/medusa-forms/controlled'; +import { Input } from '@lambdacurry/medusa-forms/ui'; + // ✅ Import from specific entry points import { TextField } from '@lambdacurry/forms/remix-hook-form/text-field'; -import { Button } from '@lambdacurry/forms/ui/button'; +import { ControlledInput } from '@lambdacurry/medusa-forms/controlled/ControlledInput'; ``` ### Internal Package Imports @@ -361,4 +372,3 @@ docs/ - Maintain backward compatibility when possible Remember: A well-organized monorepo makes development faster and more predictable. Every import should be intentional and follow the established patterns to maintain consistency across the codebase. - diff --git a/.cursor/rules/storybook-testing.mdc b/.cursor/rules/storybook-testing.mdc index bd4b8039..ac109880 100644 --- a/.cursor/rules/storybook-testing.mdc +++ b/.cursor/rules/storybook-testing.mdc @@ -3,7 +3,7 @@ type: Always description: Rules for writing Storybook Playwright tests in the lambda-curry/forms repository --- -You are an expert in Storybook, Playwright testing, React, TypeScript, Remix Hook Form, Zod validation, and the lambda-curry/forms monorepo architecture. +You are an expert in Storybook, Playwright testing, React, TypeScript, Remix Hook Form, react-hook-form, @medusajs/ui, Zod validation, and the lambda-curry/forms monorepo architecture. # Project Context This is a monorepo containing form components with comprehensive Storybook Playwright testing. The testing setup combines Storybook's component isolation with Playwright's browser automation to create real-world testing scenarios. @@ -13,7 +13,8 @@ This is a monorepo containing form components with comprehensive Storybook Playw - @storybook/test-runner for Playwright automation - @storybook/test for testing utilities (userEvent, expect, canvas) - React Router stub decorator for form handling -- Remix Hook Form + Zod for validation testing +- Remix Hook Form + Zod for validation testing (main components) +- react-hook-form + @medusajs/ui for Medusa Forms components - Yarn 4.7.0 with corepack - TypeScript throughout @@ -22,13 +23,18 @@ This is a monorepo containing form components with comprehensive Storybook Playw lambda-curry/forms/ ├── apps/docs/ # Storybook app │ ├── .storybook/ # Storybook configuration -│ ├── src/remix-hook-form/ # Story files with tests +│ ├── src/remix-hook-form/ # Remix Hook Form story files with tests +│ ├── src/medusa-forms/ # Medusa Forms story files with tests │ ├── simple-server.js # Custom static server for testing │ └── package.json # Test scripts -├── packages/components/ # Component library +├── packages/components/ # Main component library (Remix Hook Form) │ └── src/ │ ├── remix-hook-form/ # Form components │ └── ui/ # UI components +├── packages/medusa-forms/ # Medusa Forms component library +│ └── src/ +│ ├── controlled/ # Controlled components using react-hook-form +│ └── ui/ # UI components using @medusajs/ui └── .cursor/rules/ # Cursor rules directory ``` diff --git a/apps/docs/src/medusa-forms/ControlledCheckbox.stories.tsx b/apps/docs/src/medusa-forms/ControlledCheckbox.stories.tsx new file mode 100644 index 00000000..e75df06c --- /dev/null +++ b/apps/docs/src/medusa-forms/ControlledCheckbox.stories.tsx @@ -0,0 +1,396 @@ +import { ControlledCheckbox } from '@lambdacurry/medusa-forms/controlled/ControlledCheckbox'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import React from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; + +const meta = { + title: 'Medusa Forms/Controlled Checkbox', + component: ControlledCheckbox, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// Basic Usage Story +const BasicCheckboxForm = () => { + const form = useForm({ + defaultValues: { + acceptTerms: false, + }, + }); + + return ( + +
+ +
+ Current value: {form.watch('acceptTerms') ? 'true' : 'false'} +
+
+
+ ); +}; + +export const BasicUsage: Story = { + render: () => , +}; + +// Default Checked State Story +const DefaultCheckedForm = () => { + const form = useForm({ + defaultValues: { + newsletter: true, + }, + }); + + return ( + +
+ +
+ Current value: {form.watch('newsletter') ? 'true' : 'false'} +
+
+
+ ); +}; + +export const DefaultChecked: Story = { + render: () => , +}; + +// Default Unchecked State Story +const DefaultUncheckedForm = () => { + const form = useForm({ + defaultValues: { + marketing: false, + }, + }); + + return ( + +
+ +
+ Current value: {form.watch('marketing') ? 'true' : 'false'} +
+
+
+ ); +}; + +export const DefaultUnchecked: Story = { + render: () => , +}; + +// Required Field Validation Story +const RequiredValidationForm = () => { + const form = useForm({ + defaultValues: { + requiredField: false, + }, + mode: 'onChange', + }); + + const onSubmit = (data: any) => { + console.log('Form submitted:', data); + }; + + return ( + +
+ +
+ Form valid: {form.formState.isValid ? 'Yes' : 'No'} +
+ + +
+ ); +}; + +export const RequiredValidation: Story = { + render: () => , +}; + +// Custom Validation Message Story +const CustomValidationForm = () => { + const form = useForm({ + defaultValues: { + agreement: false, + }, + mode: 'onChange', + }); + + return ( + +
+ value === true || 'You must agree to the privacy policy' + }} + /> +
+ Has errors: {form.formState.errors.agreement ? 'Yes' : 'No'} +
+ {form.formState.errors.agreement && ( +
+ Error: {form.formState.errors.agreement.message} +
+ )} +
+
+ ); +}; + +export const CustomValidationMessage: Story = { + render: () => , +}; + +// Error State Display Story +const ErrorStateForm = () => { + const form = useForm({ + defaultValues: { + errorField: false, + }, + }); + + // Manually trigger an error for demonstration + React.useEffect(() => { + form.setError('errorField', { + type: 'manual', + message: 'This is an example error message' + }); + }, [form]); + + return ( + +
+ +
+ This checkbox demonstrates the error state styling +
+
+
+ ); +}; + +export const ErrorState: Story = { + render: () => , +}; + +// Disabled State Story +const DisabledStateForm = () => { + const form = useForm({ + defaultValues: { + disabledUnchecked: false, + disabledChecked: true, + }, + }); + + return ( + +
+ + +
+ These checkboxes are disabled and cannot be interacted with +
+
+
+ ); +}; + +export const DisabledState: Story = { + render: () => , +}; + +// Multiple Checkboxes with State Management Story +const MultipleCheckboxesForm = () => { + const form = useForm({ + defaultValues: { + option1: false, + option2: true, + option3: false, + selectAll: false, + }, + }); + + const watchedValues = form.watch(['option1', 'option2', 'option3']); + const allSelected = watchedValues.every(Boolean); + const someSelected = watchedValues.some(Boolean); + + React.useEffect(() => { + form.setValue('selectAll', allSelected); + }, [allSelected, form]); + + const handleSelectAll = (checked: boolean) => { + form.setValue('option1', checked); + form.setValue('option2', checked); + form.setValue('option3', checked); + form.setValue('selectAll', checked); + }; + + return ( + +
+ +
+ + + +
+
+ Selected: {watchedValues.filter(Boolean).length} of {watchedValues.length} +
+
+
+ ); +}; + +export const MultipleCheckboxes: Story = { + render: () => , +}; + +// Form Integration Example Story +const CompleteFormExampleComponent = () => { + const form = useForm({ + defaultValues: { + username: '', + email: '', + acceptTerms: false, + newsletter: false, + marketing: false, + }, + mode: 'onChange', + }); + + const onSubmit = (data: any) => { + alert(`Form submitted with data: ${JSON.stringify(data, null, 2)}`); + }; + + return ( + +
+
+ + +
+ +
+ + +
+ +
+ + + +
+ +
+ + +
+ +
+ Form valid: {form.formState.isValid ? 'Yes' : 'No'} +
+
+
+ ); +}; + +export const CompleteFormExample: Story = { + render: () => , +}; diff --git a/apps/docs/src/medusa-forms/ControlledCurrencyInput.stories.tsx b/apps/docs/src/medusa-forms/ControlledCurrencyInput.stories.tsx new file mode 100644 index 00000000..36f027d9 --- /dev/null +++ b/apps/docs/src/medusa-forms/ControlledCurrencyInput.stories.tsx @@ -0,0 +1,272 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import { ControlledCurrencyInput } from '@lambdacurry/medusa-forms/controlled/ControlledCurrencyInput'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { FormProvider, useForm } from 'react-hook-form'; +import { z } from 'zod'; + +const meta = { + title: 'Medusa Forms/Controlled Currency Input', + component: ControlledCurrencyInput, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + args: { + symbol: '$', + code: 'usd', + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +interface CurrencyFormData { + price: string; +} + +// Base wrapper component for stories +const CurrencyInputWithHookForm = ({ + currency = 'USD', + symbol = '$', + code = 'usd', + schema, + defaultValues = { price: '' }, + ...props +}: { + currency?: string; + symbol?: string; + code?: string; + schema?: z.ZodSchema; + defaultValues?: CurrencyFormData; + [key: string]: unknown; +}) => { + const form = useForm({ + resolver: schema ? zodResolver(schema) : undefined, + defaultValues, + }); + + return ( + +
+ +
+
+ ); +}; + +// 1. Different Currency Symbols +export const USDCurrency: Story = { + args: { + name: 'price', + symbol: '$', + code: 'usd', + }, + render: (args) => , +}; + +export const EURCurrency: Story = { + args: { + name: 'price', + symbol: '€', + code: 'eur', + }, + render: (args) => , +}; + +export const GBPCurrency: Story = { + args: { + name: 'price', + symbol: '£', + code: 'gbp', + }, + render: (args) => , +}; + +// 2. Validation with Min/Max Values +const minValidationSchema = z.object({ + price: z.string().refine((val) => { + const num = Number.parseFloat(val); + return !Number.isNaN(num) && num >= 10; + }, 'Minimum price is $10'), +}); + +export const MinimumValueValidation: Story = { + args: { + name: 'price', + symbol: '$', + code: 'usd', + }, + render: (args) => ( + + ), +}; + +const maxValidationSchema = z.object({ + price: z.string().refine((val) => { + const num = Number.parseFloat(val); + return !Number.isNaN(num) && num <= 1000; + }, 'Maximum price is $1000'), +}); + +export const MaximumValueValidation: Story = { + args: { + name: 'price', + symbol: '$', + code: 'usd', + }, + render: (args) => ( + + ), +}; + +const rangeValidationSchema = z.object({ + price: z.string().refine((val) => { + const num = Number.parseFloat(val); + return !Number.isNaN(num) && num >= 50 && num <= 500; + }, 'Price must be between $50 and $500'), +}); + +export const RangeValidation: Story = { + args: { + name: 'price', + symbol: '$', + code: 'usd', + }, + render: (args) => ( + + ), +}; + +// 3. Error Handling and Validation Messages +const requiredSchema = z.object({ + price: z + .string() + .min(1, 'Price is required') + .refine((val) => { + const num = Number.parseFloat(val); + return !Number.isNaN(num) && num > 0; + }, 'Price must be greater than 0'), +}); + +export const RequiredFieldValidation: Story = { + args: { + name: 'price', + symbol: '$', + code: 'usd', + }, + render: (args) => ( + + ), +}; + +const customValidationSchema = z.object({ + price: z.string().refine((val) => { + const num = Number.parseFloat(val); + return !Number.isNaN(num) && num >= 1 && num <= 100; + }, 'Custom error: Price must be between $1 and $100'), +}); + +export const CustomValidationMessage: Story = { + args: { + name: 'price', + symbol: '$', + code: 'usd', + }, + render: (args) => ( + + ), +}; + +// 4. Different Currency Codes +export const JPYCurrency: Story = { + args: { + name: 'price', + symbol: '¥', + code: 'jpy', + }, + render: (args) => ( + + ), +}; + +export const CADCurrency: Story = { + args: { + name: 'price', + symbol: 'C$', + code: 'cad', + }, + render: (args) => ( + + ), +}; + +export const AUDCurrency: Story = { + args: { + name: 'price', + symbol: 'A$', + code: 'aud', + }, + render: (args) => ( + + ), +}; diff --git a/apps/docs/src/medusa-forms/ControlledDatePicker.stories.tsx b/apps/docs/src/medusa-forms/ControlledDatePicker.stories.tsx new file mode 100644 index 00000000..e027610d --- /dev/null +++ b/apps/docs/src/medusa-forms/ControlledDatePicker.stories.tsx @@ -0,0 +1,273 @@ +import { ControlledDatePicker } from '@lambdacurry/medusa-forms/controlled/ControlledDatePicker'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { FormProvider, useForm } from 'react-hook-form'; + +const meta = { + title: 'Medusa Forms/Controlled Date Picker', + component: ControlledDatePicker, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// 1. Basic Date Selection +const BasicDateSelectionComponent = () => { + const form = useForm({ + defaultValues: { + birthDate: '', + }, + }); + + return ( + +
+ +
+
+ ); +}; + +export const BasicDateSelection: Story = { + args: { + name: 'birthDate', + label: 'Birth Date', + placeholder: 'Select your birth date', + }, + render: () => , +}; + +// 2. Required Field Validation +const RequiredFieldValidationComponent = () => { + const form = useForm({ + defaultValues: { + requiredDate: '', + }, + }); + + const onSubmit = (data: unknown) => { + console.log('Form submitted:', data); + }; + + return ( + +
+ + + {form.formState.errors.requiredDate && ( +

{form.formState.errors.requiredDate.message}

+ )} + +
+ ); +}; + +export const RequiredFieldValidation: Story = { + args: { + name: 'requiredDate', + label: 'Required Date', + placeholder: 'This field is required', + }, + render: () => , +}; + +// 3. Date Format Variations +const DateFormatVariationsComponent = () => { + const form = useForm({ + defaultValues: { + usFormat: '', + euroFormat: '', + isoFormat: '', + }, + }); + + return ( + +
+ + + +
+
+ ); +}; + +export const DateFormatVariations: Story = { + args: { + name: 'usFormat', + label: 'US Format (MM/DD/YYYY)', + placeholder: 'MM/DD/YYYY', + }, + render: () => , +}; + +// 4. Disabled Dates +const DisabledDatesComponent = () => { + const form = useForm({ + defaultValues: { + noPastDates: '', + noFutureDates: '', + specificDisabled: '', + }, + }); + + const today = new Date(); + const oneWeekAgo = new Date(); + oneWeekAgo.setDate(today.getDate() - 7); + const oneWeekFromNow = new Date(); + oneWeekFromNow.setDate(today.getDate() + 7); + + return ( + +
+ + + +
+
+ ); +}; + +export const DisabledDates: Story = { + args: { + name: 'noPastDates', + label: 'No Past Dates', + placeholder: 'Future dates only', + }, + render: () => , +}; + +// 5. Min/Max Date Constraints +const MinMaxDateConstraintsComponent = () => { + const form = useForm({ + defaultValues: { + constrainedDate: '', + businessDays: '', + ageRestricted: '', + }, + }); + + const today = new Date(); + const minDate = new Date(); + minDate.setDate(today.getDate() + 1); // Tomorrow + const maxDate = new Date(); + maxDate.setDate(today.getDate() + 30); // 30 days from now + + const eighteenYearsAgo = new Date(); + eighteenYearsAgo.setFullYear(today.getFullYear() - 18); + const hundredYearsAgo = new Date(); + hundredYearsAgo.setFullYear(today.getFullYear() - 100); + + return ( + +
+ { + if (!value) return true; + const selectedDate = new Date(value); + if (selectedDate < minDate) return 'Date must be tomorrow or later'; + if (selectedDate > maxDate) return 'Date must be within 30 days'; + return true; + }, + }} + /> + { + const day = date.getDay(); + return day !== 0 && day !== 6; // Exclude Sunday (0) and Saturday (6) + }} + /> + { + if (!value) return true; + const selectedDate = new Date(value); + const age = today.getFullYear() - selectedDate.getFullYear(); + const monthDiff = today.getMonth() - selectedDate.getMonth(); + const dayDiff = today.getDate() - selectedDate.getDate(); + + let calculatedAge = age; + if (monthDiff < 0 || (monthDiff === 0 && dayDiff < 0)) { + calculatedAge--; + } + + return calculatedAge >= 18 || 'Must be 18 years or older'; + }, + }} + /> + {form.formState.errors.constrainedDate && ( +

{form.formState.errors.constrainedDate.message}

+ )} + {form.formState.errors.ageRestricted && ( +

{form.formState.errors.ageRestricted.message}

+ )} +
+
+ ); +}; + +export const MinMaxDateConstraints: Story = { + args: { + name: 'constrainedDate', + label: 'Date Range (Tomorrow to 30 days)', + placeholder: 'Select within range', + }, + render: () => , +}; diff --git a/apps/docs/src/medusa-forms/ControlledSelect.stories.tsx b/apps/docs/src/medusa-forms/ControlledSelect.stories.tsx new file mode 100644 index 00000000..8bd43485 --- /dev/null +++ b/apps/docs/src/medusa-forms/ControlledSelect.stories.tsx @@ -0,0 +1,482 @@ +import { ControlledSelect } from '@lambdacurry/medusa-forms/controlled/ControlledSelect'; +import { Select } from '@lambdacurry/medusa-forms/ui/Select'; +import { Button } from '@lambdacurry/medusa-forms/ui/button'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { useEffect, useState } from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; + +const meta = { + title: 'Medusa Forms/Controlled Select', + component: ControlledSelect, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// Sample options for stories +const countryOptions = [ + { label: 'United States', value: 'us' }, + { label: 'Canada', value: 'ca' }, + { label: 'United Kingdom', value: 'uk' }, + { label: 'Germany', value: 'de' }, + { label: 'France', value: 'fr' }, + { label: 'Japan', value: 'jp' }, + { label: 'Australia', value: 'au' }, +]; + +const categoryOptions = [ + { label: 'Electronics', value: 'electronics' }, + { label: 'Clothing', value: 'clothing' }, + { label: 'Books', value: 'books' }, + { label: 'Home & Garden', value: 'home-garden' }, + { label: 'Sports', value: 'sports' }, +]; + +// 1. Basic Single Select +export const BasicSingleSelect: Story = { + args: { + name: 'country', + label: 'Country', + options: countryOptions, + }, + render: () => { + const form = useForm({ + defaultValues: { + country: '', + }, + }); + + return ( + +
+ +
+ Form Value: {JSON.stringify(form.watch(), null, 2)} +
+
+
+ ); + }, +}; + +// 2. Single Select with Default Value +export const WithDefaultValue: Story = { + args: { + name: 'country', + label: 'Country (with default)', + options: countryOptions, + }, + render: () => { + const form = useForm({ + defaultValues: { + country: 'us', + }, + }); + + return ( + +
+ +
+ Form Value: {JSON.stringify(form.watch(), null, 2)} +
+
+
+ ); + }, +}; + +// 3. Required Field Validation +export const SelectRequiredValidation: Story = { + args: { + name: 'requiredCountry', + label: 'Country (Required)', + options: countryOptions, + }, + render: () => { + const form = useForm({ + defaultValues: { + requiredCountry: '', + }, + }); + + const onSubmit = (data: unknown) => { + alert(`Form submitted with: ${JSON.stringify(data)}`); + }; + + return ( + +
+ +
+ + {form.formState.errors.requiredCountry && ( +
{form.formState.errors.requiredCountry.message}
+ )} +
+
+ Form Value: {JSON.stringify(form.watch(), null, 2)} +
+ +
+ ); + }, +}; + +// 4. Loading States +export const LoadingState: Story = { + args: { + name: 'country', + label: 'Country (Loading State)', + options: countryOptions, + }, + render: () => { + const [isLoading, setIsLoading] = useState(false); + const form = useForm({ + defaultValues: { + country: '', + }, + }); + + const toggleLoading = () => { + setIsLoading(!isLoading); + }; + + return ( + +
+ +
+ +
+
+ Form Value: {JSON.stringify(form.watch(), null, 2)} +
+ Loading: {isLoading.toString()} +
+
+
+ ); + }, +}; + +// Plan options with custom rendering data +const planOptions = [ + { + value: 'premium', + label: 'Premium Plan', + icon: '⭐', + iconColor: 'text-yellow-500', + price: '$29/mo', + }, + { + value: 'standard', + label: 'Standard Plan', + icon: '💎', + iconColor: 'text-blue-500', + price: '$19/mo', + }, + { + value: 'basic', + label: 'Basic Plan', + icon: '🌱', + iconColor: 'text-green-500', + price: '$9/mo', + }, +] as const; + +// Reusable component for plan option rendering +const PlanOption = ({ + icon, + iconColor, + label, + price, +}: { icon: string; iconColor: string; label: string; price: string }) => ( +
+ {icon} + {label} + ({price}) +
+); + +// 5. Custom Option Rendering (using children) +export const CustomOptionRendering: Story = { + args: { + name: 'customSelect', + label: 'Custom Options', + options: planOptions.map(({ value, label }) => ({ value, label })), + }, + render: () => { + const form = useForm({ + defaultValues: { + customSelect: '', + }, + }); + + const currentValue = form.watch('customSelect'); + const selectedPlan = planOptions.find((plan) => plan.value === currentValue); + + return ( + +
+ + + + {selectedPlan ? ( + + ) : ( + Select a plan... + )} + + + + {planOptions.map((plan) => ( + + + + ))} + + +
+ Form Value: {JSON.stringify(form.watch(), null, 2)} +
+
+
+ ); + }, +}; + +// 6. Disabled State +export const SelectDisabledState: Story = { + args: { + name: 'disabledSelect', + label: 'Disabled Select', + options: countryOptions, + }, + render: () => { + const form = useForm({ + defaultValues: { + disabledSelect: 'us', + }, + }); + + return ( + +
+ +
+ Form Value: {JSON.stringify(form.watch(), null, 2)} +
+
+
+ ); + }, +}; + +// 7. Error State +export const SelectErrorState: Story = { + args: { + name: 'errorSelect', + label: 'Select with Error', + options: countryOptions, + }, + render: () => { + const form = useForm({ + defaultValues: { + errorSelect: '', + }, + }); + + // Manually set an error for demonstration - use useEffect to avoid infinite re-renders + useEffect(() => { + form.setError('errorSelect', { + type: 'manual', + message: 'This field has an error', + }); + }, [form]); + + return ( + +
+ + {form.formState.errors.errorSelect && ( +
{form.formState.errors.errorSelect.message}
+ )} +
+ Form Value: {JSON.stringify(form.watch(), null, 2)} +
+
+
+ ); + }, +}; + +// 8. Small Size Variant +export const SmallSize: Story = { + args: { + name: 'smallSelect', + label: 'Small Size Select', + options: countryOptions, + }, + render: () => { + const form = useForm({ + defaultValues: { + smallSelect: '', + }, + }); + + return ( + +
+ +
+ Form Value: {JSON.stringify(form.watch(), null, 2)} +
+
+
+ ); + }, +}; + +// 9. Complex Form Integration +export const ComplexFormIntegration: Story = { + args: { + name: 'country', + label: 'Country', + options: countryOptions, + }, + render: () => { + const form = useForm({ + defaultValues: { + country: '', + category: '', + plan: '', + priority: '', + }, + }); + + const onSubmit = (data: unknown) => { + alert(`Complete form submitted: ${JSON.stringify(data, null, 2)}`); + }; + + return ( + +
+ + + + + + + + +
+ + + +
+ +
+ Form Values: +
{JSON.stringify(form.watch(), null, 2)}
+
+ +
+ Form Errors: +
{JSON.stringify(form.formState.errors, null, 2)}
+
+ +
+ ); + }, +}; + +// 10. Interactive Demo with onChange Handler +export const InteractiveDemo: Story = { + args: { + name: 'interactiveSelect', + label: 'Interactive Select', + options: countryOptions, + }, + render: () => { + const [selectedValue, setSelectedValue] = useState(''); + const form = useForm({ + defaultValues: { + interactiveSelect: '', + }, + }); + + const handleSelectChange = (value: unknown) => { + setSelectedValue(value as string); + console.log('Select value changed:', value); + }; + + return ( + +
+ +
+ External State Value: {selectedValue || 'None'} +
+
+ Form Value: {JSON.stringify(form.watch(), null, 2)} +
+
+
+ ); + }, +}; diff --git a/apps/docs/src/medusa-forms/ControlledTextArea.stories.tsx b/apps/docs/src/medusa-forms/ControlledTextArea.stories.tsx new file mode 100644 index 00000000..a4282e25 --- /dev/null +++ b/apps/docs/src/medusa-forms/ControlledTextArea.stories.tsx @@ -0,0 +1,413 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import { ControlledInput } from '@lambdacurry/medusa-forms/controlled/ControlledInput'; +import { ControlledTextArea } from '@lambdacurry/medusa-forms/controlled/ControlledTextArea'; +import { Button } from '@medusajs/ui'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { FormProvider, useForm } from 'react-hook-form'; +import { z } from 'zod'; + +const meta = { + title: 'Medusa Forms/Controlled Text Area', + component: ControlledTextArea, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// Basic Usage Story +const BasicUsageForm = () => { + const form = useForm({ + defaultValues: { + description: '', + }, + }); + + return ( + +
+ +
+
+ ); +}; + +export const BasicUsage: Story = { + args: { + name: 'description', + label: 'Description', + placeholder: 'Enter your description here...', + rows: 4, + }, + render: () => , + parameters: { + docs: { + description: { + story: 'A basic textarea with react-hook-form integration for multi-line text input.', + }, + }, + }, +}; + +// Character Limits Story +const CharacterLimitsSchema = z.object({ + bio: z.string().max(150, 'Bio must be 150 characters or less'), +}); + +const CharacterLimitsForm = () => { + const form = useForm({ + resolver: zodResolver(CharacterLimitsSchema), + defaultValues: { + bio: '', + }, + }); + + const bioValue = form.watch('bio'); + const characterCount = bioValue?.length || 0; + const maxLength = 150; + + return ( + +
+ +
+ {characterCount}/{maxLength} characters +
+
+
+ ); +}; + +export const CharacterLimits: Story = { + args: { + name: 'bio', + label: 'Bio', + placeholder: 'Tell us about yourself...', + rows: 4, + maxLength: 150, + }, + render: () => , + parameters: { + docs: { + description: { + story: 'Textarea with character count validation, counter display, and limit enforcement.', + }, + }, + }, +}; + +// Required Field Validation Story +const RequiredFieldSchema = z.object({ + feedback: z.string().min(1, 'Feedback is required').min(10, 'Feedback must be at least 10 characters'), +}); + +const RequiredFieldForm = () => { + const form = useForm({ + resolver: zodResolver(RequiredFieldSchema), + defaultValues: { + feedback: '', + }, + }); + + const onSubmit = (data: unknown) => { + console.log('Form submitted:', data); + }; + + return ( + +
+ + + +
+ ); +}; + +export const RequiredFieldValidation: Story = { + args: { + name: 'feedback', + label: 'Feedback', + placeholder: 'Please provide your feedback...', + rows: 5, + }, + render: () => , + parameters: { + docs: { + description: { + story: 'Required field validation with error state display and custom validation messages.', + }, + }, + }, +}; + +// Auto-resize Functionality Story +const AutoResizeForm = () => { + const form = useForm({ + defaultValues: { + content: '', + }, + }); + + return ( + +
+ { + const target = e.target as HTMLTextAreaElement; + target.style.height = 'auto'; + target.style.height = `${Math.min(target.scrollHeight, 200)}px`; + }} + /> +
+ This textarea automatically adjusts its height based on content (min: 60px, max: 200px) +
+
+
+ ); +}; + +export const AutoResizeFunctionality: Story = { + args: { + name: 'content', + label: 'Auto-resize Content', + placeholder: 'Start typing and watch the textarea grow...', + rows: 2, + }, + render: () => , + parameters: { + docs: { + description: { + story: 'Dynamic height adjustment with content-based resizing and min/max height constraints.', + }, + }, + }, +}; + +// Validation Error States Story +const ValidationErrorSchema = z.object({ + message: z + .string() + .min(1, 'Message is required') + .min(20, 'Message must be at least 20 characters') + .max(500, 'Message must be less than 500 characters') + .refine((val) => !val.includes('spam'), 'Message cannot contain spam'), +}); + +const ValidationErrorForm = () => { + const form = useForm({ + resolver: zodResolver(ValidationErrorSchema), + defaultValues: { + message: '', + }, + mode: 'onChange', // Validate on change for immediate feedback + }); + + const onSubmit = (data: unknown) => { + console.log('Form submitted:', data); + }; + + const hasError = !!form.formState.errors.message; + const messageValue = form.watch('message'); + + return ( + +
+
+ !value.includes('spam') || 'Message cannot contain spam', + }} + /> +
+
+ {form.formState.errors.message && ( + {form.formState.errors.message.message} + )} +
+
{messageValue?.length || 0}/500
+
+
+ +
+

+ Validation Rules: +

+
    +
  • Required field
  • +
  • Minimum 20 characters
  • +
  • Maximum 500 characters
  • +
  • Cannot contain the word "spam"
  • +
+
+ + +
+
+ ); +}; + +export const ValidationErrorStates: Story = { + args: { + name: 'message', + label: 'Message', + placeholder: 'Enter your message (20-500 characters, no spam)...', + rows: 6, + }, + render: () => , + parameters: { + docs: { + description: { + story: 'Various error scenarios with error message display and field highlighting.', + }, + }, + }, +}; + +// Comprehensive Form Example +const ComprehensiveSchema = z.object({ + title: z.string().min(1, 'Title is required').max(100, 'Title must be less than 100 characters'), + description: z.string().min(1, 'Description is required').min(50, 'Description must be at least 50 characters'), + notes: z.string().optional(), +}); + +const ComprehensiveForm = () => { + const form = useForm({ + resolver: zodResolver(ComprehensiveSchema), + defaultValues: { + title: '', + description: '', + notes: '', + }, + mode: 'onChange', + }); + + const onSubmit = (data: unknown) => { + console.log('Comprehensive form submitted:', data); + }; + + return ( + +
+
+ +
+ +
+ +
+ +
+ +
+ +
+ + +
+ +
+ Form Values: +
{JSON.stringify(form.watch(), null, 2)}
+
+ + {form.formState.isSubmitted && form.formState.isValid && ( +
+ Form submitted successfully! Check the console for the data. +
+ )} +
+
+ ); +}; + +export const ComprehensiveExample: Story = { + args: { + name: 'title', + label: 'Title', + placeholder: 'Enter a title...', + }, + render: () => , + parameters: { + docs: { + description: { + story: + 'A comprehensive form example showing multiple ControlledTextArea components with different validation rules and states.', + }, + }, + }, +}; diff --git a/apps/docs/src/medusa-forms/FormIntegrationExamples.stories.tsx b/apps/docs/src/medusa-forms/FormIntegrationExamples.stories.tsx new file mode 100644 index 00000000..f167bd47 --- /dev/null +++ b/apps/docs/src/medusa-forms/FormIntegrationExamples.stories.tsx @@ -0,0 +1,673 @@ +import { ControlledCheckbox } from '@lambdacurry/medusa-forms/controlled/ControlledCheckbox'; +import { ControlledCurrencyInput } from '@lambdacurry/medusa-forms/controlled/ControlledCurrencyInput'; +import { ControlledDatePicker } from '@lambdacurry/medusa-forms/controlled/ControlledDatePicker'; +import { ControlledInput } from '@lambdacurry/medusa-forms/controlled/ControlledInput'; +import { ControlledSelect } from '@lambdacurry/medusa-forms/controlled/ControlledSelect'; +import { ControlledTextArea } from '@lambdacurry/medusa-forms/controlled/ControlledTextArea'; +import { Button } from '@medusajs/ui'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { useState } from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; + +const meta = { + title: 'Medusa Forms/Form Integration Examples', + component: () => null, // No single component + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// Registration Form Types +interface RegistrationFormData { + firstName: string; + lastName: string; + email: string; + password: string; + confirmPassword: string; + dateOfBirth: Date | null; + country: string; + agreeToTerms: boolean; + subscribeNewsletter: boolean; +} + +// Product Creation Form Types +interface ProductFormData { + name: string; + description: string; + price: string; + category: string; + sku: string; + weight: string; + dimensions: string; + launchDate: Date | null; + isActive: boolean; + isFeatured: boolean; + tags: string; +} + +// Stories +export const CompleteRegistrationFormExample: Story = { + name: 'Complete Registration Form', + render: () => { + const [isSubmitting, setIsSubmitting] = useState(false); + const [submitResult, setSubmitResult] = useState(null); + + const form = useForm({ + defaultValues: { + firstName: '', + lastName: '', + email: '', + password: '', + confirmPassword: '', + dateOfBirth: null, + country: '', + agreeToTerms: false, + subscribeNewsletter: false, + }, + mode: 'onSubmit', + }); + + const { + handleSubmit, + watch, + formState: { errors, isValid }, + } = form; + const password = watch('password'); + + const onSubmit = async (data: RegistrationFormData) => { + setIsSubmitting(true); + setSubmitResult(null); + + // Simulate API call + await new Promise((resolve) => setTimeout(resolve, 2000)); + + setIsSubmitting(false); + setSubmitResult(`Registration successful for ${data.firstName} ${data.lastName}!`); + console.log('Registration data:', data); + }; + + const countryOptions = [ + { label: 'United States', value: 'US' }, + { label: 'Canada', value: 'CA' }, + { label: 'United Kingdom', value: 'UK' }, + { label: 'Germany', value: 'DE' }, + { label: 'France', value: 'FR' }, + { label: 'Australia', value: 'AU' }, + ]; + + return ( + +
+
+

Create Account

+

Join us today and get started

+
+ +
+ {/* Personal Information */} +
+

Personal Information

+ +
+ + + +
+ + + + { + if (!value) return 'Date of birth is required'; + const age = new Date().getFullYear() - new Date(value).getFullYear(); + return age >= 18 || 'You must be at least 18 years old'; + }, + }} + /> + + +
+ + {/* Account Security */} +
+

Account Security

+ + + + value === password || 'Passwords do not match', + }} + /> +
+ + {/* Preferences */} +
+

Preferences

+ + + + +
+ + {/* Submit Section */} +
+ + + {submitResult && ( +
+ {submitResult} +
+ )} + + {Object.keys(errors).length > 0 && ( +
+ Please fix the errors above before submitting. +
+ )} +
+
+
+
+ ); + }, + parameters: { + docs: { + description: { + story: ` +A comprehensive registration form demonstrating multiple controlled components working together: + +**Features:** +- **Form Validation**: Real-time validation with react-hook-form +- **Multiple Input Types**: Text inputs, email, password, date picker, select, checkboxes +- **Complex Validation Rules**: Password strength, email format, age verification, password confirmation +- **Error Handling**: Individual field errors and form-level error summary +- **Submit States**: Loading states and success feedback +- **Responsive Layout**: Grid layouts and proper spacing + +**Components Used:** +- ControlledInput (text, email, password) +- ControlledDatePicker (date of birth with age validation) +- ControlledSelect (country selection) +- ControlledCheckbox (terms agreement, newsletter subscription) + +**Validation Scenarios:** +- Required fields +- Email format validation +- Password strength requirements +- Password confirmation matching +- Age verification (18+) +- Terms agreement requirement + `, + }, + }, + }, +}; + +export const ProductCreationFormExample: Story = { + name: 'Product Creation Form', + render: () => { + const [isSubmitting, setIsSubmitting] = useState(false); + const [submitResult, setSubmitResult] = useState(null); + + const form = useForm({ + defaultValues: { + name: '', + description: '', + price: '', + category: '', + sku: '', + weight: '', + dimensions: '', + launchDate: null, + isActive: true, + isFeatured: false, + tags: '', + }, + mode: 'onSubmit', + }); + + const { + handleSubmit, + formState: { errors, isValid }, + } = form; + + const onSubmit = async (data: ProductFormData) => { + setIsSubmitting(true); + setSubmitResult(null); + + // Simulate API call + await new Promise((resolve) => setTimeout(resolve, 2000)); + + setIsSubmitting(false); + setSubmitResult(`Product "${data.name}" created successfully!`); + console.log('Product data:', data); + }; + + const categoryOptions = [ + { label: 'Electronics', value: 'electronics' }, + { label: 'Clothing', value: 'clothing' }, + { label: 'Home & Garden', value: 'home-garden' }, + { label: 'Sports & Outdoors', value: 'sports-outdoors' }, + { label: 'Books', value: 'books' }, + { label: 'Toys & Games', value: 'toys-games' }, + ]; + + return ( + +
+
+

Create New Product

+

Add a new product to your catalog

+
+ +
+ {/* Basic Information */} +
+

Basic Information

+ + + + + +
+ + + +
+
+ + {/* Pricing & Inventory */} +
+

Pricing & Inventory

+ +
+ { + const numValue = Number.parseFloat(value); + return (numValue > 0 && numValue <= 10000) || 'Price must be between $0.01 and $10,000'; + }, + }} + /> + + +
+ + +
+ + {/* Launch & Visibility */} +
+

Launch & Visibility

+ + { + if (!value) return 'Launch date is required'; + const today = new Date(); + today.setHours(0, 0, 0, 0); + return new Date(value) >= today || 'Launch date cannot be in the past'; + }, + }} + /> + +
+ + + +
+ + { + if (!value) return true; // Tags are optional + const tags = value.split(',').map((tag) => tag.trim()); + return tags.length <= 10 || 'Maximum 10 tags allowed'; + }, + }} + /> +
+ + {/* Submit Section */} +
+
+ + + +
+ + {submitResult && ( +
+ {submitResult} +
+ )} + + {Object.keys(errors).length > 0 && ( +
+

Please fix the following errors:

+
    + {Object.entries(errors).map(([field, error]) => ( +
  • + {field}: {error?.message} +
  • + ))} +
+
+ )} +
+
+
+
+ ); + }, + parameters: { + docs: { + description: { + story: ` +A comprehensive product creation form showcasing all controlled components in a real-world e-commerce context: + +**Features:** +- **Advanced Validation**: Complex business rules and data validation +- **Multiple Data Types**: Text, numbers, currency, dates, boolean flags +- **Rich Text Input**: Multi-line descriptions with character limits +- **Currency Handling**: Proper currency input with validation +- **Date Constraints**: Future date validation for launch dates +- **Dynamic Feedback**: Real-time validation and error reporting +- **Form Reset**: Ability to clear and reset the entire form + +**Components Used:** +- ControlledInput (text, number inputs with various validation rules) +- ControlledTextArea (product description, tags) +- ControlledCurrencyInput (pricing with currency formatting) +- ControlledSelect (category selection) +- ControlledDatePicker (launch date with future date validation) +- ControlledCheckbox (product status flags) + +**Complex Validation Examples:** +- SKU format validation (uppercase, numbers, hyphens only) +- Price range validation ($0.01 - $10,000) +- Dimensions format validation (L x W x H pattern) +- Future date validation for launch dates +- Tag count limits (max 10 tags) +- Character limits for descriptions + `, + }, + }, + }, +}; + +export const FormValidationShowcase: Story = { + name: 'Form Validation Showcase', + render: () => { + const form = useForm({ + defaultValues: { + requiredField: '', + emailField: '', + numberField: '', + dateField: null, + selectField: '', + checkboxField: false, + }, + mode: 'onSubmit', + }); + + return ( + +
+

Validation Examples

+

Try interacting with these fields to see validation in action

+ + + + + + { + const num = Number.parseFloat(value); + return (num >= 10 && num <= 1000) || 'Amount must be between $10 and $1,000'; + }, + }} + /> + + { + if (!value) return 'Date is required'; + return new Date(value) > new Date() || 'Date must be in the future'; + }, + }} + /> + + + + + +
+ +
+
+
+ ); + }, + parameters: { + docs: { + description: { + story: ` +A focused demonstration of various validation scenarios across all controlled components: + +**Validation Types Demonstrated:** +- **Required Fields**: Basic required field validation +- **Format Validation**: Email pattern matching +- **Range Validation**: Currency amounts with min/max limits +- **Date Validation**: Future date constraints +- **Selection Validation**: Required dropdown selections +- **Boolean Validation**: Required checkbox agreements + +This example shows how each component handles validation states and error messages consistently. + `, + }, + }, + }, +}; diff --git a/packages/components/src/ui/button.tsx b/packages/components/src/ui/button.tsx index b9bbd8e4..bfc941c7 100644 --- a/packages/components/src/ui/button.tsx +++ b/packages/components/src/ui/button.tsx @@ -1,6 +1,6 @@ import { Slot } from '@radix-ui/react-slot'; import { type VariantProps, cva } from 'class-variance-authority'; -import * as React from 'react'; +import type * as React from 'react'; import { cn } from './utils'; const buttonVariants = cva( @@ -37,15 +37,9 @@ export interface ButtonProps export function Button({ className, variant, size, asChild = false, ...props }: ButtonProps) { const Comp = asChild ? Slot : 'button'; - return ( - - ); + return ; } Button.displayName = 'Button'; -export { buttonVariants }; \ No newline at end of file +export { buttonVariants }; diff --git a/packages/medusa-forms/src/controlled/ControlledCurrencyInput.tsx b/packages/medusa-forms/src/controlled/ControlledCurrencyInput.tsx index 59897b67..cec66e5e 100644 --- a/packages/medusa-forms/src/controlled/ControlledCurrencyInput.tsx +++ b/packages/medusa-forms/src/controlled/ControlledCurrencyInput.tsx @@ -1,3 +1,4 @@ +import type * as React from 'react'; import { Controller, type ControllerProps, diff --git a/packages/medusa-forms/src/controlled/ControlledSelect.tsx b/packages/medusa-forms/src/controlled/ControlledSelect.tsx index dfcb9f7b..70d5e5db 100644 --- a/packages/medusa-forms/src/controlled/ControlledSelect.tsx +++ b/packages/medusa-forms/src/controlled/ControlledSelect.tsx @@ -1,3 +1,4 @@ +import type * as React from 'react'; import { Controller, type ControllerProps, @@ -49,10 +50,9 @@ export const ControlledSelect = ({ if (options) { return (