diff --git a/packages/medusa-forms/REGISTRY.md b/packages/medusa-forms/REGISTRY.md new file mode 100644 index 0000000..9df9924 --- /dev/null +++ b/packages/medusa-forms/REGISTRY.md @@ -0,0 +1,144 @@ +# Medusa Forms Registry + +This package provides a custom shadcn/ui registry that allows developers to install medusa-forms components using the native shadcn CLI. + +## Installation + +You can install components from this registry using the shadcn CLI: + +```bash +npx shadcn@latest add --registry https://raw.githubusercontent.com/lambda-curry/medusa-forms/main/packages/medusa-forms/registry.json input +``` + +## Registry Maintenance + +The registry files are automatically generated from source components using a build script. This ensures the registry stays in sync with the actual component code without manual maintenance. + +### Regenerating Registry Files + +To regenerate the registry files after making changes to components: + +```bash +yarn build:registry +``` + +This script: +- Scans component source files in `src/ui/` and `src/controlled/` +- Generates registry JSON files with file references (not inline content) +- Updates the main `registry.json` with proper metadata +- Validates that all referenced source files exist + +### Adding New Components + +To add a new component to the registry: + +1. Create your component in the appropriate directory (`src/ui/` or `src/controlled/`) +2. Add the component configuration to `scripts/build-registry.js` in the `COMPONENTS` object +3. Run `yarn build:registry` to generate the registry files +4. Commit both the source component and generated registry files + +## Available Components + +### Base UI Components + +- `field-wrapper` - Core wrapper component with error handling and labels +- `field-error` - Error display component +- `label` - Label component with tooltip support +- `input` - Base input component +- `select` - Base select component +- `field-checkbox` - Base checkbox component +- `textarea` - Base textarea component +- `datepicker` - Base datepicker component +- `currency-input` - Base currency input component + +### Controlled Components (React Hook Form) + +- `controlled-input` - Input with react-hook-form integration +- `controlled-select` - Select with react-hook-form integration +- `controlled-checkbox` - Checkbox with react-hook-form integration +- `controlled-textarea` - Textarea with react-hook-form integration +- `controlled-datepicker` - DatePicker with react-hook-form integration +- `controlled-currency-input` - CurrencyInput with react-hook-form integration + +## Usage Examples + +### Installing a single component + +```bash +npx shadcn@latest add --registry https://raw.githubusercontent.com/lambda-curry/medusa-forms/main/packages/medusa-forms/registry.json controlled-input +``` + +### Installing multiple components + +```bash +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 +``` + +### Using the components + +```tsx +import { ControlledInput } from '@/components/ui/controlled-input' +import { ControlledSelect } from '@/components/ui/controlled-select' +import { useForm, FormProvider } from 'react-hook-form' + +function MyForm() { + const methods = useForm() + + return ( + +
+ + + + +
+ ) +} +``` + +## Component Dependencies + +The registry properly handles component dependencies: + +- Controlled components depend on their base UI components +- UI components depend on `field-wrapper` when needed +- `field-wrapper` depends on `field-error` and `label` +- All components use `@medusajs/ui` for base styling + +## Architecture + +### Field Wrapper Pattern + +All form components use a consistent wrapper pattern: + +- `FieldWrapper` provides consistent layout and error handling +- `Label` component handles labels and tooltips +- `FieldError` handles error message display +- UI components wrap `@medusajs/ui` components + +### Controlled Component Pattern + +Controlled components use react-hook-form: + +- Uses `Controller` from react-hook-form +- Integrates with form context +- Handles form validation and errors +- Preserves component props and types + +## Types and Interfaces + +- `BasicFieldProps` - Common field properties +- `FieldWrapperProps` - Wrapper component props +- Component-specific props (`InputProps`, `SelectProps`, etc.) +- React Hook Form integration types diff --git a/packages/medusa-forms/package.json b/packages/medusa-forms/package.json index cc89ff4..b4557e4 100644 --- a/packages/medusa-forms/package.json +++ b/packages/medusa-forms/package.json @@ -40,6 +40,7 @@ "scripts": { "prepublishOnly": "yarn run build", "build": "vite build", + "build:registry": "node scripts/build-registry.js", "lint": "biome check .", "lint:fix": "biome check --apply .", "type-check": "tsc --noEmit" diff --git a/packages/medusa-forms/registry.json b/packages/medusa-forms/registry.json new file mode 100644 index 0000000..e769ba0 --- /dev/null +++ b/packages/medusa-forms/registry.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "name": "medusa-forms", + "description": "Controlled form fields for Medusa Admin and Medusa UI", + "url": "https://raw.githubusercontent.com/lambda-curry/medusa-forms/main/packages/medusa-forms", + "style": "default", + "tailwind": { + "config": "tailwind.config.js", + "css": "src/styles/globals.css", + "baseColor": "slate", + "cssVariables": true + }, + "aliases": { + "components": "src/components", + "utils": "src/lib/utils" + } +} \ No newline at end of file diff --git a/packages/medusa-forms/registry/controlled-checkbox.json b/packages/medusa-forms/registry/controlled-checkbox.json new file mode 100644 index 0000000..8dde63b --- /dev/null +++ b/packages/medusa-forms/registry/controlled-checkbox.json @@ -0,0 +1,17 @@ +{ + "name": "controlled-checkbox", + "type": "registry:ui", + "description": "Checkbox component with react-hook-form integration", + "dependencies": [ + "react-hook-form" + ], + "registryDependencies": [ + "field-checkbox" + ], + "files": [ + { + "name": "controlled-checkbox.tsx", + "path": "src/controlled/ControlledCheckbox.tsx" + } + ] +} \ No newline at end of file diff --git a/packages/medusa-forms/registry/controlled-currency-input.json b/packages/medusa-forms/registry/controlled-currency-input.json new file mode 100644 index 0000000..0fe2a25 --- /dev/null +++ b/packages/medusa-forms/registry/controlled-currency-input.json @@ -0,0 +1,17 @@ +{ + "name": "controlled-currency-input", + "type": "registry:ui", + "description": "CurrencyInput component with react-hook-form integration", + "dependencies": [ + "react-hook-form" + ], + "registryDependencies": [ + "currency-input" + ], + "files": [ + { + "name": "controlled-currency-input.tsx", + "path": "src/controlled/ControlledCurrencyInput.tsx" + } + ] +} \ No newline at end of file diff --git a/packages/medusa-forms/registry/controlled-datepicker.json b/packages/medusa-forms/registry/controlled-datepicker.json new file mode 100644 index 0000000..43f4742 --- /dev/null +++ b/packages/medusa-forms/registry/controlled-datepicker.json @@ -0,0 +1,17 @@ +{ + "name": "controlled-datepicker", + "type": "registry:ui", + "description": "DatePicker component with react-hook-form integration", + "dependencies": [ + "react-hook-form" + ], + "registryDependencies": [ + "datepicker" + ], + "files": [ + { + "name": "controlled-datepicker.tsx", + "path": "src/controlled/ControlledDatePicker.tsx" + } + ] +} \ No newline at end of file diff --git a/packages/medusa-forms/registry/controlled-input.json b/packages/medusa-forms/registry/controlled-input.json new file mode 100644 index 0000000..325fe2f --- /dev/null +++ b/packages/medusa-forms/registry/controlled-input.json @@ -0,0 +1,17 @@ +{ + "name": "controlled-input", + "type": "registry:ui", + "description": "Input component with react-hook-form integration", + "dependencies": [ + "react-hook-form" + ], + "registryDependencies": [ + "input" + ], + "files": [ + { + "name": "controlled-input.tsx", + "path": "src/controlled/ControlledInput.tsx" + } + ] +} \ No newline at end of file diff --git a/packages/medusa-forms/registry/controlled-select.json b/packages/medusa-forms/registry/controlled-select.json new file mode 100644 index 0000000..1771722 --- /dev/null +++ b/packages/medusa-forms/registry/controlled-select.json @@ -0,0 +1,17 @@ +{ + "name": "controlled-select", + "type": "registry:ui", + "description": "Select component with react-hook-form integration", + "dependencies": [ + "react-hook-form" + ], + "registryDependencies": [ + "select" + ], + "files": [ + { + "name": "controlled-select.tsx", + "path": "src/controlled/ControlledSelect.tsx" + } + ] +} \ No newline at end of file diff --git a/packages/medusa-forms/registry/controlled-textarea.json b/packages/medusa-forms/registry/controlled-textarea.json new file mode 100644 index 0000000..d3ecb01 --- /dev/null +++ b/packages/medusa-forms/registry/controlled-textarea.json @@ -0,0 +1,17 @@ +{ + "name": "controlled-textarea", + "type": "registry:ui", + "description": "Textarea component with react-hook-form integration", + "dependencies": [ + "react-hook-form" + ], + "registryDependencies": [ + "textarea" + ], + "files": [ + { + "name": "controlled-textarea.tsx", + "path": "src/controlled/ControlledTextArea.tsx" + } + ] +} \ No newline at end of file diff --git a/packages/medusa-forms/registry/currency-input.json b/packages/medusa-forms/registry/currency-input.json new file mode 100644 index 0000000..a8de433 --- /dev/null +++ b/packages/medusa-forms/registry/currency-input.json @@ -0,0 +1,17 @@ +{ + "name": "currency-input", + "type": "registry:ui", + "description": "Base currency input component", + "dependencies": [ + "@medusajs/ui" + ], + "registryDependencies": [ + "field-wrapper" + ], + "files": [ + { + "name": "currency-input.tsx", + "path": "src/ui/CurrencyInput.tsx" + } + ] +} \ No newline at end of file diff --git a/packages/medusa-forms/registry/datepicker.json b/packages/medusa-forms/registry/datepicker.json new file mode 100644 index 0000000..558709f --- /dev/null +++ b/packages/medusa-forms/registry/datepicker.json @@ -0,0 +1,17 @@ +{ + "name": "datepicker", + "type": "registry:ui", + "description": "Base datepicker component", + "dependencies": [ + "@medusajs/ui" + ], + "registryDependencies": [ + "field-wrapper" + ], + "files": [ + { + "name": "datepicker.tsx", + "path": "src/ui/DatePicker.tsx" + } + ] +} \ No newline at end of file diff --git a/packages/medusa-forms/registry/field-checkbox.json b/packages/medusa-forms/registry/field-checkbox.json new file mode 100644 index 0000000..8dcc94d --- /dev/null +++ b/packages/medusa-forms/registry/field-checkbox.json @@ -0,0 +1,18 @@ +{ + "name": "field-checkbox", + "type": "registry:ui", + "description": "Base checkbox component", + "dependencies": [ + "@medusajs/ui" + ], + "registryDependencies": [ + "field-wrapper", + "label" + ], + "files": [ + { + "name": "fieldcheckbox.tsx", + "path": "src/ui/FieldCheckbox.tsx" + } + ] +} \ No newline at end of file diff --git a/packages/medusa-forms/registry/field-error.json b/packages/medusa-forms/registry/field-error.json new file mode 100644 index 0000000..a679075 --- /dev/null +++ b/packages/medusa-forms/registry/field-error.json @@ -0,0 +1,15 @@ +{ + "name": "field-error", + "type": "registry:ui", + "description": "Error display component", + "dependencies": [ + "@medusajs/ui" + ], + "registryDependencies": [], + "files": [ + { + "name": "error.tsx", + "path": "src/ui/Error.tsx" + } + ] +} \ No newline at end of file diff --git a/packages/medusa-forms/registry/field-wrapper.json b/packages/medusa-forms/registry/field-wrapper.json new file mode 100644 index 0000000..baaf516 --- /dev/null +++ b/packages/medusa-forms/registry/field-wrapper.json @@ -0,0 +1,22 @@ +{ + "name": "field-wrapper", + "type": "registry:ui", + "description": "Core wrapper component with error handling and labels", + "dependencies": [ + "@medusajs/ui" + ], + "registryDependencies": [ + "field-error", + "label" + ], + "files": [ + { + "name": "fieldwrapper.tsx", + "path": "src/ui/FieldWrapper.tsx" + }, + { + "name": "types.d.ts", + "path": "src/ui/types.d.ts" + } + ] +} \ No newline at end of file diff --git a/packages/medusa-forms/registry/input.json b/packages/medusa-forms/registry/input.json new file mode 100644 index 0000000..82b8e85 --- /dev/null +++ b/packages/medusa-forms/registry/input.json @@ -0,0 +1,17 @@ +{ + "name": "input", + "type": "registry:ui", + "description": "Base input component", + "dependencies": [ + "@medusajs/ui" + ], + "registryDependencies": [ + "field-wrapper" + ], + "files": [ + { + "name": "input.tsx", + "path": "src/ui/Input.tsx" + } + ] +} \ No newline at end of file diff --git a/packages/medusa-forms/registry/label.json b/packages/medusa-forms/registry/label.json new file mode 100644 index 0000000..c5aeb7c --- /dev/null +++ b/packages/medusa-forms/registry/label.json @@ -0,0 +1,15 @@ +{ + "name": "label", + "type": "registry:ui", + "description": "Label component with tooltip support", + "dependencies": [ + "@medusajs/ui" + ], + "registryDependencies": [], + "files": [ + { + "name": "label.tsx", + "path": "src/ui/Label.tsx" + } + ] +} \ No newline at end of file diff --git a/packages/medusa-forms/registry/select.json b/packages/medusa-forms/registry/select.json new file mode 100644 index 0000000..aca86f4 --- /dev/null +++ b/packages/medusa-forms/registry/select.json @@ -0,0 +1,17 @@ +{ + "name": "select", + "type": "registry:ui", + "description": "Base select component", + "dependencies": [ + "@medusajs/ui" + ], + "registryDependencies": [ + "field-wrapper" + ], + "files": [ + { + "name": "select.tsx", + "path": "src/ui/Select.tsx" + } + ] +} \ No newline at end of file diff --git a/packages/medusa-forms/registry/textarea.json b/packages/medusa-forms/registry/textarea.json new file mode 100644 index 0000000..231aeb0 --- /dev/null +++ b/packages/medusa-forms/registry/textarea.json @@ -0,0 +1,17 @@ +{ + "name": "textarea", + "type": "registry:ui", + "description": "Base textarea component", + "dependencies": [ + "@medusajs/ui" + ], + "registryDependencies": [ + "field-wrapper" + ], + "files": [ + { + "name": "textarea.tsx", + "path": "src/ui/TextArea.tsx" + } + ] +} \ No newline at end of file diff --git a/packages/medusa-forms/scripts/build-registry.js b/packages/medusa-forms/scripts/build-registry.js new file mode 100644 index 0000000..5dbf56b --- /dev/null +++ b/packages/medusa-forms/scripts/build-registry.js @@ -0,0 +1,225 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); + +// Component metadata configuration +const COMPONENTS = { + // Base UI Components + 'field-wrapper': { + description: 'Core wrapper component with error handling and labels', + dependencies: ['@medusajs/ui'], + registryDependencies: ['field-error', 'label'], + files: [ + { name: 'fieldwrapper.tsx', path: 'src/ui/FieldWrapper.tsx' }, + { name: 'types.d.ts', path: 'src/ui/types.d.ts' } + ] + }, + 'field-error': { + description: 'Error display component', + dependencies: ['@medusajs/ui'], + registryDependencies: [], + files: [ + { name: 'error.tsx', path: 'src/ui/Error.tsx' } + ] + }, + 'label': { + description: 'Label component with tooltip support', + dependencies: ['@medusajs/ui'], + registryDependencies: [], + files: [ + { name: 'label.tsx', path: 'src/ui/Label.tsx' } + ] + }, + 'input': { + description: 'Base input component', + dependencies: ['@medusajs/ui'], + registryDependencies: ['field-wrapper'], + files: [ + { name: 'input.tsx', path: 'src/ui/Input.tsx' } + ] + }, + 'select': { + description: 'Base select component', + dependencies: ['@medusajs/ui'], + registryDependencies: ['field-wrapper'], + files: [ + { name: 'select.tsx', path: 'src/ui/Select.tsx' } + ] + }, + 'field-checkbox': { + description: 'Base checkbox component', + dependencies: ['@medusajs/ui'], + registryDependencies: ['field-wrapper', 'label'], + files: [ + { name: 'fieldcheckbox.tsx', path: 'src/ui/FieldCheckbox.tsx' } + ] + }, + 'textarea': { + description: 'Base textarea component', + dependencies: ['@medusajs/ui'], + registryDependencies: ['field-wrapper'], + files: [ + { name: 'textarea.tsx', path: 'src/ui/TextArea.tsx' } + ] + }, + 'datepicker': { + description: 'Base datepicker component', + dependencies: ['@medusajs/ui'], + registryDependencies: ['field-wrapper'], + files: [ + { name: 'datepicker.tsx', path: 'src/ui/DatePicker.tsx' } + ] + }, + 'currency-input': { + description: 'Base currency input component', + dependencies: ['@medusajs/ui'], + registryDependencies: ['field-wrapper'], + files: [ + { name: 'currency-input.tsx', path: 'src/ui/CurrencyInput.tsx' } + ] + }, + + // Controlled Components + 'controlled-input': { + description: 'Input component with react-hook-form integration', + dependencies: ['react-hook-form'], + registryDependencies: ['input'], + files: [ + { name: 'controlled-input.tsx', path: 'src/controlled/ControlledInput.tsx' } + ] + }, + 'controlled-select': { + description: 'Select component with react-hook-form integration', + dependencies: ['react-hook-form'], + registryDependencies: ['select'], + files: [ + { name: 'controlled-select.tsx', path: 'src/controlled/ControlledSelect.tsx' } + ] + }, + 'controlled-checkbox': { + description: 'Checkbox component with react-hook-form integration', + dependencies: ['react-hook-form'], + registryDependencies: ['field-checkbox'], + files: [ + { name: 'controlled-checkbox.tsx', path: 'src/controlled/ControlledCheckbox.tsx' } + ] + }, + 'controlled-textarea': { + description: 'Textarea component with react-hook-form integration', + dependencies: ['react-hook-form'], + registryDependencies: ['textarea'], + files: [ + { name: 'controlled-textarea.tsx', path: 'src/controlled/ControlledTextArea.tsx' } + ] + }, + 'controlled-datepicker': { + description: 'DatePicker component with react-hook-form integration', + dependencies: ['react-hook-form'], + registryDependencies: ['datepicker'], + files: [ + { name: 'controlled-datepicker.tsx', path: 'src/controlled/ControlledDatePicker.tsx' } + ] + }, + 'controlled-currency-input': { + description: 'CurrencyInput component with react-hook-form integration', + dependencies: ['react-hook-form'], + registryDependencies: ['currency-input'], + files: [ + { name: 'controlled-currency-input.tsx', path: 'src/controlled/ControlledCurrencyInput.tsx' } + ] + } +}; + +function generateRegistryItem(componentName, config) { + return { + name: componentName, + type: 'registry:ui', + description: config.description, + dependencies: config.dependencies, + registryDependencies: config.registryDependencies, + files: config.files.map(file => ({ + name: file.name, + path: file.path + })) + }; +} + +function ensureDirectoryExists(dirPath) { + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + } +} + +function buildRegistry() { + console.log('🏗️ Building medusa-forms registry...'); + + const registryDir = path.join(__dirname, '../registry'); + ensureDirectoryExists(registryDir); + + // Generate individual component registry files + Object.entries(COMPONENTS).forEach(([componentName, config]) => { + const registryItem = generateRegistryItem(componentName, config); + const filePath = path.join(registryDir, `${componentName}.json`); + + fs.writeFileSync(filePath, JSON.stringify(registryItem, null, 2)); + console.log(`✅ Generated ${componentName}.json`); + }); + + // Update main registry.json with component list + const mainRegistry = { + "$schema": "https://ui.shadcn.com/schema.json", + "name": "medusa-forms", + "description": "Controlled form fields for Medusa Admin and Medusa UI", + "url": "https://raw.githubusercontent.com/lambda-curry/medusa-forms/main/packages/medusa-forms", + "style": "default", + "tailwind": { + "config": "tailwind.config.js", + "css": "src/styles/globals.css", + "baseColor": "slate", + "cssVariables": true + }, + "aliases": { + "components": "src/components", + "utils": "src/lib/utils" + } + }; + + const mainRegistryPath = path.join(__dirname, '../registry.json'); + fs.writeFileSync(mainRegistryPath, JSON.stringify(mainRegistry, null, 2)); + console.log('✅ Updated main registry.json'); + + console.log(`🎉 Registry build complete! Generated ${Object.keys(COMPONENTS).length} component files.`); +} + +// Validate that source files exist +function validateSourceFiles() { + console.log('🔍 Validating source files...'); + + let allValid = true; + Object.entries(COMPONENTS).forEach(([componentName, config]) => { + config.files.forEach(file => { + const fullPath = path.join(__dirname, '..', file.path); + if (!fs.existsSync(fullPath)) { + console.error(`❌ Missing source file: ${file.path} for component ${componentName}`); + allValid = false; + } + }); + }); + + if (!allValid) { + console.error('❌ Some source files are missing. Please check the file paths.'); + process.exit(1); + } + + console.log('✅ All source files validated'); +} + +// Main execution +if (require.main === module) { + validateSourceFiles(); + buildRegistry(); +} + +module.exports = { buildRegistry, COMPONENTS }; +