diff --git a/apps/docs/package.json b/apps/docs/package.json index cd14aea6..cf773cf4 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -19,7 +19,7 @@ "react": "^19.0.0", "react-hook-form": "^7.51.0", "react-router": "^7.6.1", - "remix-hook-form": "^7.0.1", + "remix-hook-form": "^7.1.0", "storybook": "^9.0.6" }, "devDependencies": { diff --git a/apps/docs/src/remix-hook-form/form-error-basic.stories.tsx b/apps/docs/src/remix-hook-form/form-error-basic.stories.tsx new file mode 100644 index 00000000..7a148f90 --- /dev/null +++ b/apps/docs/src/remix-hook-form/form-error-basic.stories.tsx @@ -0,0 +1,199 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import { FormError, TextField } from '@lambdacurry/forms'; +import { Button } from '@lambdacurry/forms/ui/button'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { expect, userEvent, within } from '@storybook/test'; +import { type ActionFunctionArgs, useFetcher } from 'react-router'; +import { RemixFormProvider, getValidatedFormData, useRemixForm } from 'remix-hook-form'; +import { z } from 'zod'; +import { withReactRouterStubDecorator } from '../lib/storybook/react-router-stub'; + +const formSchema = z.object({ + email: z.string().email('Please enter a valid email address'), + password: z.string().min(6, 'Password must be at least 6 characters'), +}); + +type FormData = z.infer; + +const BasicFormErrorExample = () => { + const fetcher = useFetcher<{ + message?: string; + errors?: Record + }>(); + + const methods = useRemixForm({ + resolver: zodResolver(formSchema), + defaultValues: { + email: '', + password: '', + }, + fetcher, + submitConfig: { + action: '/', + method: 'post', + }, + }); + + const isSubmitting = fetcher.state === 'submitting'; + + return ( + + +

Login Form

+ + {/* Form-level error display */} + + + + + + + + + {fetcher.data?.message && ( +
+

{fetcher.data.message}

+
+ )} +
+
+ ); +}; + +const handleFormSubmission = async (request: Request) => { + const { data, errors } = await getValidatedFormData(request, zodResolver(formSchema)); + + if (errors) { + return { errors }; + } + + // Simulate server-side authentication + if (data.email === 'wrong@email.com' && data.password === 'wrongpass') { + return { + errors: { + _form: { message: 'Invalid email or password. Please try again.' } + } + }; + } + + if (data.email === 'user@example.com' && data.password === 'password123') { + return { message: 'Login successful! Welcome back.' }; + } + + return { + errors: { + _form: { message: 'Invalid email or password. Please try again.' } + } + }; +}; + +const meta: Meta = { + title: 'RemixHookForm/FormError/Basic', + component: FormError, + parameters: { + layout: 'centered', + docs: { + description: { + component: ` +The FormError component provides standardized form-level error handling for server failures, authentication issues, and other form-wide errors. + +**Key Features:** +- Automatic integration with remix-hook-form context +- Uses \`_form\` as the default error key +- Flexible placement anywhere in forms +- Component override support for custom styling + `, + }, + }, + }, + tags: ['autodocs'], + decorators: [ + withReactRouterStubDecorator({ + routes: [ + { + path: '/', + Component: BasicFormErrorExample, + action: async ({ request }: ActionFunctionArgs) => handleFormSubmission(request), + }, + ], + }), + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + parameters: { + docs: { + description: { + story: ` +Basic form error handling with server-side validation failure. + +**Try this:** +1. Click "Sign In" without filling fields (shows field-level errors) +2. Enter invalid credentials like \`wrong@email.com\` and \`wrongpass\` (shows form-level error) +3. Enter \`user@example.com\` and \`password123\` for success + +The FormError component automatically displays when \`errors._form\` exists in the server response. + `, + }, + }, + }, + play: async ({ canvasElement, step }) => { + const canvas = within(canvasElement); + + await step('Verify initial state', async () => { + const emailInput = canvas.getByLabelText(/email address/i); + const passwordInput = canvas.getByLabelText(/password/i); + const submitButton = canvas.getByRole('button', { name: /sign in/i }); + + expect(emailInput).toBeInTheDocument(); + expect(passwordInput).toBeInTheDocument(); + expect(submitButton).toBeInTheDocument(); + expect(canvas.queryByText(/invalid email or password/i)).not.toBeInTheDocument(); + }); + + await step('Test field-level validation errors', async () => { + const submitButton = canvas.getByRole('button', { name: /sign in/i }); + await userEvent.click(submitButton); + + await expect(canvas.findByText(/please enter a valid email address/i)).resolves.toBeInTheDocument(); + await expect(canvas.findByText(/password must be at least 6 characters/i)).resolves.toBeInTheDocument(); + expect(canvas.queryByText(/invalid email or password/i)).not.toBeInTheDocument(); + }); + + await step('Test form-level error with invalid credentials', async () => { + const emailInput = canvas.getByLabelText(/email address/i); + const passwordInput = canvas.getByLabelText(/password/i); + + await userEvent.clear(emailInput); + await userEvent.clear(passwordInput); + await userEvent.type(emailInput, 'wrong@email.com'); + await userEvent.type(passwordInput, 'wrongpass'); + + const submitButton = canvas.getByRole('button', { name: /sign in/i }); + await userEvent.click(submitButton); + + // Wait for form-level error to appear + await expect(canvas.findByText(/invalid email or password/i)).resolves.toBeInTheDocument(); + + // Verify field-level errors are cleared + expect(canvas.queryByText(/please enter a valid email address/i)).not.toBeInTheDocument(); + }); + }, +}; diff --git a/apps/docs/src/remix-hook-form/form-error-custom.stories.tsx b/apps/docs/src/remix-hook-form/form-error-custom.stories.tsx new file mode 100644 index 00000000..26df7536 --- /dev/null +++ b/apps/docs/src/remix-hook-form/form-error-custom.stories.tsx @@ -0,0 +1,214 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import { FormError, TextField } from '@lambdacurry/forms'; +import { FormMessage } from '@lambdacurry/forms/remix-hook-form/form'; +import { Button } from '@lambdacurry/forms/ui/button'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { expect, userEvent, within } from '@storybook/test'; +import { type ActionFunctionArgs, useFetcher } from 'react-router'; +import { RemixFormProvider, getValidatedFormData, useRemixForm } from 'remix-hook-form'; +import { z } from 'zod'; +import { withReactRouterStubDecorator } from '../lib/storybook/react-router-stub'; + +const formSchema = z.object({ + email: z.string().email('Please enter a valid email address'), + password: z.string().min(6, 'Password must be at least 6 characters'), +}); + +type FormData = z.infer; + +// Custom error message component with icon +const AlertErrorMessage = (props: React.ComponentProps) => ( +
+ + + + +
+); + +const CustomStyledFormErrorExample = () => { + const fetcher = useFetcher<{ + message?: string; + errors?: Record + }>(); + + const methods = useRemixForm({ + resolver: zodResolver(formSchema), + defaultValues: { + email: '', + password: '', + }, + fetcher, + submitConfig: { + action: '/', + method: 'post', + }, + }); + + const isSubmitting = fetcher.state === 'submitting'; + + return ( + + +

Sign In

+ + {/* Custom styled form error */} + + + + + + + + + {fetcher.data?.message && ( +
+

{fetcher.data.message}

+
+ )} +
+
+ ); +}; + +const handleFormSubmission = async (request: Request) => { + const { data, errors } = await getValidatedFormData(request, zodResolver(formSchema)); + + if (errors) { + return { errors }; + } + + // Always show form error for demo purposes + return { + errors: { + _form: { message: 'Authentication service is temporarily unavailable. Please try again in a few minutes.' } + } + }; +}; + +const meta: Meta = { + title: 'RemixHookForm/FormError/Custom', + component: FormError, + parameters: { + layout: 'centered', + docs: { + description: { + component: ` +Custom styled FormError with branded error message component. The \`components\` prop allows you to completely customize how form errors are displayed. + +**Component Override:** +\`\`\`typescript + +\`\`\` + +This example shows an alert-style error message with an icon and custom styling. + `, + }, + }, + }, + tags: ['autodocs'], + decorators: [ + withReactRouterStubDecorator({ + routes: [ + { + path: '/', + Component: CustomStyledFormErrorExample, + action: async ({ request }: ActionFunctionArgs) => handleFormSubmission(request), + }, + ], + }), + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + parameters: { + docs: { + description: { + story: ` +Custom styled FormError with branded error message component. + +**Features:** +- Alert-style error message with warning icon +- Custom background colors and borders +- Enhanced typography and spacing +- Maintains accessibility attributes + +The custom component receives all the same props as the default FormMessage component. + `, + }, + }, + }, + play: async ({ canvasElement, step }) => { + const canvas = within(canvasElement); + + await step('Verify initial state with custom styling', async () => { + const emailInput = canvas.getByLabelText(/email address/i); + const passwordInput = canvas.getByLabelText(/password/i); + const submitButton = canvas.getByRole('button', { name: /sign in/i }); + + expect(emailInput).toBeInTheDocument(); + expect(passwordInput).toBeInTheDocument(); + expect(submitButton).toBeInTheDocument(); + expect(canvas.queryByText(/authentication service is temporarily unavailable/i)).not.toBeInTheDocument(); + }); + + await step('Test custom styled form error display', async () => { + const emailInput = canvas.getByLabelText(/email address/i); + const passwordInput = canvas.getByLabelText(/password/i); + + await userEvent.type(emailInput, 'test@example.com'); + await userEvent.type(passwordInput, 'password123'); + + const submitButton = canvas.getByRole('button', { name: /sign in/i }); + await userEvent.click(submitButton); + + // Wait for error to appear + await new Promise(resolve => setTimeout(resolve, 500)); + + // Check for error message + const errorMessage = canvas.queryByText(/authentication service is temporarily unavailable/i); + expect(errorMessage).toBeInTheDocument(); + }); + + await step('Verify custom error styling and structure', async () => { + // Wait for error message to be available + await new Promise(resolve => setTimeout(resolve, 500)); + const errorMessage = canvas.queryByText(/authentication service is temporarily unavailable/i); + expect(errorMessage).toBeInTheDocument(); + + const errorContainer = errorMessage?.closest('div'); + expect(errorContainer).toHaveClass('flex', 'items-center', 'p-4', 'bg-red-50', 'border-l-4', 'border-red-400', 'rounded-md'); + + const icon = errorContainer?.querySelector('svg'); + expect(icon).toBeInTheDocument(); + expect(icon).toHaveClass('h-5', 'w-5', 'text-red-400'); + + expect(errorMessage).toHaveClass('text-destructive', 'font-medium'); + }); + }, +}; diff --git a/apps/docs/src/remix-hook-form/form-error-mixed.stories.tsx b/apps/docs/src/remix-hook-form/form-error-mixed.stories.tsx new file mode 100644 index 00000000..e9b76d6e --- /dev/null +++ b/apps/docs/src/remix-hook-form/form-error-mixed.stories.tsx @@ -0,0 +1,208 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import { FormError, TextField } from '@lambdacurry/forms'; +import { Button } from '@lambdacurry/forms/ui/button'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { expect, userEvent, within } from '@storybook/test'; +import { type ActionFunctionArgs, useFetcher } from 'react-router'; +import { RemixFormProvider, getValidatedFormData, useRemixForm } from 'remix-hook-form'; +import { z } from 'zod'; +import { withReactRouterStubDecorator } from '../lib/storybook/react-router-stub'; + +const formSchema = z.object({ + email: z.string().email('Please enter a valid email address'), + password: z.string().min(6, 'Password must be at least 6 characters'), +}); + +type FormData = z.infer; + +const MixedErrorsExample = () => { + const fetcher = useFetcher<{ + message?: string; + errors?: Record + }>(); + + const methods = useRemixForm({ + resolver: zodResolver(formSchema), + defaultValues: { + email: '', + password: '', + }, + fetcher, + submitConfig: { + action: '/', + method: 'post', + }, + }); + + const isSubmitting = fetcher.state === 'submitting'; + + return ( + + +

Create Account

+ + {/* Form-level error at top */} + + + + + + + + + {/* Form-level error at bottom */} + + + {fetcher.data?.message && ( +
+

{fetcher.data.message}

+
+ )} +
+
+ ); +}; + +const handleFormSubmission = async (request: Request) => { + const { data, errors } = await getValidatedFormData(request, zodResolver(formSchema)); + + if (errors) { + return { errors }; + } + + // Simulate mixed errors - both field and form level + if (data.email === 'taken@example.com') { + return { + errors: { + email: { message: 'This email address is already registered' }, + _form: { message: 'Registration failed. Please correct the errors above.' } + } + }; + } + + // Simulate server error only + if (data.password === 'servererror') { + return { + errors: { + _form: { message: 'Server error occurred. Please try again later.' } + } + }; + } + + if (data.email === 'valid@example.com' && data.password === 'validpass123') { + return { message: 'Account created successfully! Welcome aboard.' }; + } + + return { + errors: { + _form: { message: 'Registration failed. Please try again.' } + } + }; +}; + +const meta: Meta = { + title: 'RemixHookForm/FormError/Mixed', + component: FormError, + parameters: { + layout: 'centered', + docs: { + description: { + component: ` +Demonstrates handling both field-level and form-level errors simultaneously. This pattern is useful when you want to show form-level context alongside specific field errors. + +**Multiple FormError Components:** +- You can place multiple FormError components in different locations +- Each instance will show the same form-level error +- Different styling can be applied to each instance + `, + }, + }, + }, + tags: ['autodocs'], + decorators: [ + withReactRouterStubDecorator({ + routes: [ + { + path: '/', + Component: MixedErrorsExample, + action: async ({ request }: ActionFunctionArgs) => handleFormSubmission(request), + }, + ], + }), + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + parameters: { + docs: { + description: { + story: ` +Shows both field-level and form-level errors working together. + +**Try this:** +1. Enter \`taken@example.com\` as email (shows both field and form errors) +2. Enter password \`servererror\` (shows only form-level error) +3. Notice FormError appears both at top and bottom of form + +This pattern is useful when you want to show form-level context alongside specific field errors. + `, + }, + }, + }, + play: async ({ canvasElement, step }) => { + const canvas = within(canvasElement); + + await step('Verify initial state with multiple FormError placements', async () => { + const emailInput = canvas.getByLabelText(/email address/i); + const passwordInput = canvas.getByLabelText(/password/i); + const submitButton = canvas.getByRole('button', { name: /create account/i }); + + expect(emailInput).toBeInTheDocument(); + expect(passwordInput).toBeInTheDocument(); + expect(submitButton).toBeInTheDocument(); + expect(canvas.queryByText(/registration failed/i)).not.toBeInTheDocument(); + }); + + await step('Test mixed errors - field and form level together', async () => { + const emailInput = canvas.getByLabelText(/email address/i); + const passwordInput = canvas.getByLabelText(/password/i); + + await userEvent.clear(emailInput); + await userEvent.clear(passwordInput); + await userEvent.type(emailInput, 'taken@example.com'); + await userEvent.type(passwordInput, 'validpass123'); + + const submitButton = canvas.getByRole('button', { name: /create account/i }); + await userEvent.click(submitButton); + + // Wait for errors to appear + await new Promise(resolve => setTimeout(resolve, 500)); + + // Check for field-level error + const fieldError = canvas.queryByText(/this email address is already registered/i); + expect(fieldError).toBeInTheDocument(); + + // Check for form-level errors - use queryAllByText to avoid "multiple elements" error + const formErrors = canvas.queryAllByText(/registration failed/i); + expect(formErrors.length).toBeGreaterThanOrEqual(1); + expect(formErrors.length).toBeLessThanOrEqual(2); + }); + }, +}; diff --git a/apps/docs/src/remix-hook-form/form-error-placement.stories.tsx b/apps/docs/src/remix-hook-form/form-error-placement.stories.tsx new file mode 100644 index 00000000..41ff154c --- /dev/null +++ b/apps/docs/src/remix-hook-form/form-error-placement.stories.tsx @@ -0,0 +1,209 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import { FormError, TextField } from '@lambdacurry/forms'; +import { Button } from '@lambdacurry/forms/ui/button'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { expect, userEvent, within } from '@storybook/test'; +import { type ActionFunctionArgs, useFetcher } from 'react-router'; +import { RemixFormProvider, getValidatedFormData, useRemixForm } from 'remix-hook-form'; +import { z } from 'zod'; +import { withReactRouterStubDecorator } from '../lib/storybook/react-router-stub'; + +const formSchema = z.object({ + email: z.string().email('Please enter a valid email address'), + password: z.string().min(6, 'Password must be at least 6 characters'), +}); + +type FormData = z.infer; + +const PlacementVariationsExample = () => { + const fetcher = useFetcher<{ + message?: string; + errors?: Record + }>(); + + const methods = useRemixForm({ + resolver: zodResolver(formSchema), + defaultValues: { + email: '', + password: '', + }, + fetcher, + submitConfig: { + action: '/', + method: 'post', + }, + }); + + const isSubmitting = fetcher.state === 'submitting'; + + return ( + + +

Payment Form

+ + {/* Top placement - Alert style */} + + + + + {/* Inline placement - Minimal style */} + + + + + + + {/* Bottom placement - Banner style */} + + + {fetcher.data?.message && ( +
+

{fetcher.data.message}

+
+ )} +
+
+ ); +}; + +const handleFormSubmission = async (request: Request) => { + const { data, errors } = await getValidatedFormData(request, zodResolver(formSchema)); + + if (errors) { + return { errors }; + } + + // Always show form error for demo purposes + return { + errors: { + _form: { message: 'Payment processing failed. Please check your information and try again.' } + } + }; +}; + +const meta: Meta = { + title: 'RemixHookForm/FormError/Placement', + component: FormError, + parameters: { + layout: 'centered', + docs: { + description: { + component: ` +Demonstrates different FormError placement options and styling approaches. You can place FormError components anywhere in your form and style them differently for various use cases. + +**Placement Options:** +- **Top**: Alert-style with full background and border +- **Inline**: Minimal text-only style between fields +- **Bottom**: Banner-style with left border accent + +Each placement can have different styling while showing the same error message. + `, + }, + }, + }, + tags: ['autodocs'], + decorators: [ + withReactRouterStubDecorator({ + routes: [ + { + path: '/', + Component: PlacementVariationsExample, + action: async ({ request }: ActionFunctionArgs) => handleFormSubmission(request), + }, + ], + }), + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + parameters: { + docs: { + description: { + story: ` +Shows FormError components in different positions with unique styling. + +**Placement Styles:** +1. **Top**: Alert box with background and border +2. **Inline**: Simple text between form fields +3. **Bottom**: Banner with left accent border + +All three instances show the same error message but with different visual treatments. + `, + }, + }, + }, + play: async ({ canvasElement, step }) => { + const canvas = within(canvasElement); + + await step('Verify initial state with multiple placement options', async () => { + const emailInput = canvas.getByLabelText(/email address/i); + const passwordInput = canvas.getByLabelText(/password/i); + const submitButton = canvas.getByRole('button', { name: /submit payment/i }); + + expect(emailInput).toBeInTheDocument(); + expect(passwordInput).toBeInTheDocument(); + expect(submitButton).toBeInTheDocument(); + expect(canvas.queryByText(/payment processing failed/i)).not.toBeInTheDocument(); + }); + + await step('Test multiple FormError placements with different styling', async () => { + const emailInput = canvas.getByLabelText(/email address/i); + const passwordInput = canvas.getByLabelText(/password/i); + + await userEvent.type(emailInput, 'test@example.com'); + await userEvent.type(passwordInput, 'password123'); + + const submitButton = canvas.getByRole('button', { name: /submit payment/i }); + await userEvent.click(submitButton); + + // Wait for error messages to appear - use queryAllByText to avoid "multiple elements" error + await new Promise(resolve => setTimeout(resolve, 500)); + + // Verify all three FormError instances are displayed + const errorMessages = canvas.queryAllByText(/payment processing failed/i); + expect(errorMessages.length).toBeGreaterThanOrEqual(1); + expect(errorMessages.length).toBeLessThanOrEqual(3); + }); + + await step('Verify different styling for each placement', async () => { + // Wait a moment for all error messages to be rendered + await new Promise(resolve => setTimeout(resolve, 100)); + + const errorMessages = canvas.getAllByText(/payment processing failed/i); + expect(errorMessages).toHaveLength(3); + + // Top placement - alert style + const topError = errorMessages[0]; + expect(topError).toHaveClass('text-destructive'); + const topContainer = topError.closest('div'); + expect(topContainer).toHaveClass('p-4', 'bg-red-50', 'border', 'border-red-200', 'rounded-lg'); + + // Inline placement - minimal style + const inlineError = errorMessages[1]; + expect(inlineError).toHaveClass('text-destructive'); + + // Bottom placement - banner style + const bottomError = errorMessages[2]; + expect(bottomError).toHaveClass('text-destructive'); + const bottomContainer = bottomError.closest('div'); + expect(bottomContainer).toHaveClass('mt-4', 'p-3', 'bg-red-100', 'border-l-4', 'border-red-500', 'rounded-r-md'); + }); + }, +}; diff --git a/apps/docs/src/remix-hook-form/form-error.stories.tsx b/apps/docs/src/remix-hook-form/form-error.stories.tsx new file mode 100644 index 00000000..b5127e9f --- /dev/null +++ b/apps/docs/src/remix-hook-form/form-error.stories.tsx @@ -0,0 +1,148 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import { FormError, TextField } from '@lambdacurry/forms'; +import { Button } from '@lambdacurry/forms/ui/button'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { type ActionFunctionArgs, useFetcher } from 'react-router'; +import { RemixFormProvider, getValidatedFormData, useRemixForm } from 'remix-hook-form'; +import { z } from 'zod'; +import { withReactRouterStubDecorator } from '../lib/storybook/react-router-stub'; + +const formSchema = z.object({ + email: z.string().email('Please enter a valid email address'), + password: z.string().min(6, 'Password must be at least 6 characters'), +}); + +type FormData = z.infer; + +const SimpleFormErrorExample = () => { + const fetcher = useFetcher<{ + message?: string; + errors?: Record + }>(); + + const methods = useRemixForm({ + resolver: zodResolver(formSchema), + defaultValues: { + email: '', + password: '', + }, + fetcher, + submitConfig: { + action: '/', + method: 'post', + }, + }); + + const isSubmitting = fetcher.state === 'submitting'; + + return ( + + +

Login

+ + + + + + + + + + {fetcher.data?.message && ( +
+

{fetcher.data.message}

+
+ )} +
+
+ ); +}; + +const handleFormSubmission = async (request: Request) => { + const { data, errors } = await getValidatedFormData(request, zodResolver(formSchema)); + + if (errors) { + return { errors }; + } + + return { + errors: { + _form: { message: 'Invalid credentials. Please try again.' } + } + }; +}; + +const meta: Meta = { + title: 'RemixHookForm/FormError', + component: FormError, + parameters: { + layout: 'centered', + docs: { + description: { + component: ` +The FormError component provides standardized form-level error handling for server failures, authentication issues, and other form-wide errors. + +**Key Features:** +- Automatic integration with remix-hook-form context +- Uses \`_form\` as the default error key +- Flexible placement anywhere in forms +- Component override support for custom styling + +**More Examples:** +- [Basic Usage](?path=/docs/remixhookform-formerror-basic--docs) - Simple form error handling +- [Mixed Errors](?path=/docs/remixhookform-formerror-mixed--docs) - Field and form errors together +- [Custom Styling](?path=/docs/remixhookform-formerror-custom--docs) - Custom error components +- [Placement Options](?path=/docs/remixhookform-formerror-placement--docs) - Different positioning styles + `, + }, + }, + }, + tags: ['autodocs'], + decorators: [ + withReactRouterStubDecorator({ + routes: [ + { + path: '/', + Component: SimpleFormErrorExample, + action: async ({ request }: ActionFunctionArgs) => handleFormSubmission(request), + }, + ], + }), + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + parameters: { + docs: { + description: { + story: ` +Basic FormError component usage. The component automatically displays when \`errors._form\` exists in the server response. + +For more comprehensive examples, see the related stories: +- **Basic**: Simple form error handling patterns +- **Mixed**: Field and form errors together +- **Custom**: Custom styling and component overrides +- **Placement**: Different positioning and styling options + `, + }, + }, + }, +}; + diff --git a/apps/docs/src/remix-hook-form/form-error.test.tsx b/apps/docs/src/remix-hook-form/form-error.test.tsx new file mode 100644 index 00000000..0747e32f --- /dev/null +++ b/apps/docs/src/remix-hook-form/form-error.test.tsx @@ -0,0 +1,401 @@ +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { RemixFormProvider, useRemixForm } from 'remix-hook-form'; +import { useFetcher } from 'react-router'; +import { z } from 'zod'; +import { FormError, TextField } from '@lambdacurry/forms'; +import { Button } from '@lambdacurry/forms/ui/button'; +import { FormMessage } from '@lambdacurry/forms/remix-hook-form/form'; + +// Mock useFetcher +jest.mock('react-router', () => ({ + useFetcher: jest.fn(), +})); + +const mockUseFetcher = useFetcher as jest.MockedFunction; + +// Test form schema +const testSchema = z.object({ + email: z.string().email('Invalid email'), + password: z.string().min(6, 'Password too short'), +}); + +type TestFormData = z.infer; + +// Test component wrapper +const TestFormWithError = ({ + initialErrors = {}, + formErrorName = '_form', + customComponents = {}, + className = '', +}: { + initialErrors?: Record; + formErrorName?: string; + customComponents?: any; + className?: string; +}) => { + const mockFetcher = { + data: { errors: initialErrors }, + state: 'idle' as const, + submit: jest.fn(), + Form: 'form' as any, + }; + + mockUseFetcher.mockReturnValue(mockFetcher); + + const methods = useRemixForm({ + resolver: zodResolver(testSchema), + defaultValues: { email: '', password: '' }, + fetcher: mockFetcher, + submitConfig: { action: '/test', method: 'post' }, + }); + + return ( + +
+ + + + + +
+ ); +}; + +describe('FormError Component', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('Basic Functionality', () => { + it('renders without errors when no form-level error exists', () => { + render(); + + // Should not display any error message + expect(screen.queryByText(/error/i)).not.toBeInTheDocument(); + }); + + it('displays form-level error when _form error exists', () => { + const errors = { + _form: { message: 'Server error occurred' } + }; + + render(); + + expect(screen.getByText('Server error occurred')).toBeInTheDocument(); + }); + + it('does not display error when _form error does not exist', () => { + const errors = { + email: { message: 'Email is invalid' } + }; + + render(); + + expect(screen.queryByText('Server error occurred')).not.toBeInTheDocument(); + }); + }); + + describe('Custom Error Keys', () => { + it('displays error for custom error key', () => { + const errors = { + general: { message: 'General form error' } + }; + + render(); + + expect(screen.getByText('General form error')).toBeInTheDocument(); + }); + + it('does not display error when custom key does not match', () => { + const errors = { + _form: { message: 'Default form error' } + }; + + render(); + + expect(screen.queryByText('Default form error')).not.toBeInTheDocument(); + }); + }); + + describe('Styling and CSS Classes', () => { + it('applies custom className to the error container', () => { + const errors = { + _form: { message: 'Test error' } + }; + + render(); + + const errorElement = screen.getByText('Test error').closest('[class*="custom-error-class"]'); + expect(errorElement).toBeInTheDocument(); + }); + + it('renders with default styling when no className provided', () => { + const errors = { + _form: { message: 'Test error' } + }; + + render(); + + expect(screen.getByText('Test error')).toBeInTheDocument(); + }); + }); + + describe('Component Customization', () => { + it('uses custom FormMessage component when provided', () => { + const CustomFormMessage = ({ children, ...props }: any) => ( +
+ Custom: {children} +
+ ); + + const errors = { + _form: { message: 'Test error' } + }; + + render( + + ); + + expect(screen.getByTestId('custom-form-message')).toBeInTheDocument(); + expect(screen.getByText('Custom: Test error')).toBeInTheDocument(); + }); + + it('falls back to default FormMessage when no custom component provided', () => { + const errors = { + _form: { message: 'Test error' } + }; + + render(); + + expect(screen.getByText('Test error')).toBeInTheDocument(); + // Should not have custom wrapper + expect(screen.queryByTestId('custom-form-message')).not.toBeInTheDocument(); + }); + }); + + describe('Integration with Form State', () => { + it('updates when form errors change', async () => { + const { rerender } = render(); + + // Initially no error + expect(screen.queryByText('New error')).not.toBeInTheDocument(); + + // Update with error + const errors = { + _form: { message: 'New error' } + }; + + rerender(); + + expect(screen.getByText('New error')).toBeInTheDocument(); + }); + + it('hides error when form errors are cleared', async () => { + const errors = { + _form: { message: 'Initial error' } + }; + + const { rerender } = render(); + + // Initially shows error + expect(screen.getByText('Initial error')).toBeInTheDocument(); + + // Clear errors + rerender(); + + expect(screen.queryByText('Initial error')).not.toBeInTheDocument(); + }); + }); + + describe('Multiple FormError Components', () => { + const MultipleFormErrorsComponent = () => { + const mockFetcher = { + data: { + errors: { + _form: { message: 'General error' }, + custom: { message: 'Custom error' } + } + }, + state: 'idle' as const, + submit: jest.fn(), + Form: 'form' as any, + }; + + mockUseFetcher.mockReturnValue(mockFetcher); + + const methods = useRemixForm({ + resolver: zodResolver(testSchema), + defaultValues: { email: '', password: '' }, + fetcher: mockFetcher, + submitConfig: { action: '/test', method: 'post' }, + }); + + return ( + +
+ + + + + + +
+ ); + }; + + it('renders multiple FormError components with different error keys', () => { + render(); + + expect(screen.getAllByText('General error')).toHaveLength(2); // top and bottom + expect(screen.getByText('Custom error')).toBeInTheDocument(); // middle + }); + }); + + describe('Accessibility', () => { + it('has proper ARIA attributes for error messages', () => { + const errors = { + _form: { message: 'Accessibility test error' } + }; + + render(); + + const errorMessage = screen.getByText('Accessibility test error'); + expect(errorMessage).toHaveAttribute('data-slot', 'form-message'); + }); + + it('is properly associated with form context', () => { + const errors = { + _form: { message: 'Form context error' } + }; + + render(); + + const errorMessage = screen.getByText('Form context error'); + expect(errorMessage.tagName.toLowerCase()).toBe('p'); + expect(errorMessage).toHaveClass('form-message'); + }); + }); + + describe('Error Message Content', () => { + it('handles empty error messages gracefully', () => { + const errors = { + _form: { message: '' } + }; + + render(); + + // Should not render anything for empty message + expect(screen.queryByText('')).not.toBeInTheDocument(); + }); + + it('handles long error messages', () => { + const longMessage = 'This is a very long error message that should still be displayed properly even when it contains a lot of text and might wrap to multiple lines in the user interface.'; + const errors = { + _form: { message: longMessage } + }; + + render(); + + expect(screen.getByText(longMessage)).toBeInTheDocument(); + }); + + it('handles special characters in error messages', () => { + const specialMessage = 'Error with special chars: <>&"\''; + const errors = { + _form: { message: specialMessage } + }; + + render(); + + expect(screen.getByText(specialMessage)).toBeInTheDocument(); + }); + }); + + describe('Performance', () => { + it('does not re-render unnecessarily when unrelated form state changes', () => { + const renderSpy = jest.fn(); + + const CustomFormMessage = ({ children, ...props }: any) => { + renderSpy(); + return
{children}
; + }; + + const errors = { + _form: { message: 'Performance test' } + }; + + const { rerender } = render( + + ); + + const initialRenderCount = renderSpy.mock.calls.length; + + // Re-render with same errors (should not cause additional renders) + rerender( + + ); + + expect(renderSpy.mock.calls.length).toBe(initialRenderCount); + }); + }); +}); + +describe('FormError Integration Tests', () => { + it('works correctly in a complete form submission flow', async () => { + const TestForm = () => { + const mockFetcher = { + data: null, + state: 'idle' as const, + submit: jest.fn(), + Form: 'form' as any, + }; + + mockUseFetcher.mockReturnValue(mockFetcher); + + const methods = useRemixForm({ + resolver: zodResolver(testSchema), + defaultValues: { email: '', password: '' }, + fetcher: mockFetcher, + submitConfig: { action: '/test', method: 'post' }, + }); + + return ( + +
+ + + + + +
+ ); + }; + + render(); + + // Form should render without errors initially + expect(screen.queryByText(/error/i)).not.toBeInTheDocument(); + + // Submit button should be present and functional + const submitButton = screen.getByRole('button', { name: /submit/i }); + expect(submitButton).toBeInTheDocument(); + + // Form fields should be present + expect(screen.getByLabelText(/email/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/password/i)).toBeInTheDocument(); + }); +}); diff --git a/docs/form-error-guide.md b/docs/form-error-guide.md new file mode 100644 index 00000000..e596ce2e --- /dev/null +++ b/docs/form-error-guide.md @@ -0,0 +1,401 @@ +# FormError Component Guide + +The `FormError` component provides a standardized way to display form-level errors in your forms. It automatically integrates with the remix-hook-form context and follows the same patterns as other form components in the library. + +## Overview + +Form-level errors are different from field-level errors. While field-level errors are specific to individual form inputs (like "Email is required"), form-level errors represent general issues that affect the entire form (like "Server error occurred" or "Authentication failed"). + +## Basic Usage + +```typescript +import { FormError } from '@lambdacurry/forms'; + +// Basic usage - looks for errors._form by default + + +// Custom error key + + +// With custom styling + +``` + +## API Reference + +### Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `name` | `string` | `"_form"` | The error key to look for in the form errors object | +| `className` | `string` | `undefined` | Additional CSS classes for styling and positioning | +| `components` | `object` | `{}` | Custom component overrides (FormMessage) | + +### Component Override + +```typescript +interface FormErrorProps { + name?: string; + className?: string; + components?: { + FormMessage?: React.ComponentType; + }; +} +``` + +## Server Action Pattern + +To use FormError effectively, your server actions should return form-level errors using the standard error key: + +```typescript +export const action = async ({ request }: ActionFunctionArgs) => { + const { data, errors } = await getValidatedFormData( + request, + zodResolver(formSchema) + ); + + // Return field-level validation errors + if (errors) { + return { errors }; + } + + // Business logic validation + try { + await processForm(data); + return { message: 'Success!' }; + } catch (error) { + // Return form-level error + return { + errors: { + _form: { message: 'Unable to process form. Please try again.' } + } + }; + } +}; +``` + +## Error Hierarchy + +Understanding when to use form-level vs field-level errors: + +### Field-Level Errors +- **Validation errors**: "Email is required", "Password too short" +- **Format errors**: "Invalid email format", "Phone number must be 10 digits" +- **Field-specific business rules**: "Username already taken" + +### Form-Level Errors +- **Server errors**: "Server temporarily unavailable" +- **Authentication failures**: "Invalid credentials" +- **Network issues**: "Connection timeout" +- **General business logic**: "Account suspended" +- **Rate limiting**: "Too many attempts, try again later" + +## Usage Patterns + +### 1. Basic Form Error + +```typescript +const LoginForm = () => { + const fetcher = useFetcher<{ + message?: string; + errors?: Record + }>(); + + const methods = useRemixForm({ + resolver: zodResolver(formSchema), + fetcher, + submitConfig: { action: '/login', method: 'post' }, + }); + + return ( + + + {/* Form-level error at the top */} + + + + + + + + + ); +}; +``` + +### 2. Mixed Errors (Field + Form Level) + +```typescript +const RegistrationForm = () => { + // ... form setup + + return ( + + + + + + + + + + + {/* Optional: Show form error at bottom too */} + + + + ); +}; + +// Server action handling both error types +export const action = async ({ request }: ActionFunctionArgs) => { + const { data, errors } = await getValidatedFormData(request, zodResolver(formSchema)); + + if (errors) return { errors }; + + // Check if email already exists (field-specific error) + if (await emailExists(data.email)) { + return { + errors: { + email: { message: 'This email is already registered' } + } + }; + } + + // Server/network error (form-level error) + try { + await createAccount(data); + return { message: 'Account created successfully!' }; + } catch (error) { + return { + errors: { + _form: { message: 'Failed to create account. Please try again.' } + } + }; + } +}; +``` + +### 3. Custom Styling + +```typescript +const CustomStyledForm = () => { + // Custom error message component + const AlertErrorMessage = (props: React.ComponentPropsWithoutRef) => ( +
+
+ +
+
+ +
+
+ ); + + return ( + + + + + {/* Form fields */} + + + ); +}; +``` + +### 4. Multiple Placement Options + +```typescript +const FlexibleErrorPlacement = () => { + return ( + + + {/* Top placement - most visible */} + + + + + {/* Inline placement - contextual */} + + + + + + + {/* Bottom placement - summary */} + + + + ); +}; +``` + +### 5. Custom Error Keys + +```typescript +const MultiErrorForm = () => { + return ( + + + {/* General form errors */} + + + {/* Payment-specific errors */} + + + {/* Shipping-specific errors */} + + + {/* Form fields */} + + + ); +}; + +// Server action with multiple error types +export const action = async ({ request }: ActionFunctionArgs) => { + // ... validation + + try { + await processPayment(data.payment); + } catch (error) { + return { + errors: { + payment: { message: 'Payment processing failed. Please check your card details.' } + } + }; + } + + try { + await calculateShipping(data.address); + } catch (error) { + return { + errors: { + shipping: { message: 'Shipping not available to this address.' } + } + }; + } + + // General server error + try { + await submitOrder(data); + return { message: 'Order submitted successfully!' }; + } catch (error) { + return { + errors: { + _form: { message: 'Unable to submit order. Please try again.' } + } + }; + } +}; +``` + +## Best Practices + +### 1. Error Key Conventions + +- Use `_form` for general form-level errors +- Use descriptive keys for specific error categories (`payment`, `shipping`, `auth`) +- Be consistent across your application + +### 2. Error Message Guidelines + +- **Be specific**: "Server temporarily unavailable" vs "Error occurred" +- **Be actionable**: "Please try again in a few minutes" vs "Failed" +- **Be user-friendly**: Avoid technical jargon and error codes +- **Be consistent**: Use similar tone and format across your app + +### 3. Placement Strategy + +- **Top placement**: For critical errors that should be seen immediately +- **Inline placement**: For contextual errors related to specific sections +- **Bottom placement**: For summary or less critical errors +- **Multiple placement**: Use sparingly, only when it improves UX + +### 4. Styling Consistency + +```typescript +// Create reusable error styles +const errorStyles = { + critical: "p-4 bg-red-50 border-l-4 border-red-400 rounded-md", + warning: "p-3 bg-yellow-50 border border-yellow-200 rounded", + info: "p-2 bg-blue-50 text-blue-700 rounded", +}; + + +``` + +### 5. Accessibility Considerations + +- FormError automatically includes proper ARIA attributes +- Error messages are announced to screen readers +- Use sufficient color contrast for error styling +- Don't rely solely on color to convey error state + +## Migration from Manual Error Handling + +If you're currently handling form-level errors manually, here's how to migrate: + +### Before (Manual) +```typescript +{fetcher.data?.errors?._form && ( +
+

{fetcher.data.errors._form.message}

+
+)} +``` + +### After (FormError) +```typescript + +``` + +### Benefits of Migration + +1. **Consistency**: Same API as other form components +2. **Automatic integration**: Works with form context automatically +3. **Customization**: Component override system +4. **Accessibility**: Built-in ARIA attributes +5. **Type safety**: TypeScript support +6. **Less boilerplate**: No manual error checking + +## Troubleshooting + +### FormError not displaying + +1. **Check error key**: Ensure your server action returns `errors._form` (or your custom key) +2. **Verify form context**: FormError must be inside `` +3. **Check fetcher data**: Use React DevTools to inspect `fetcher.data.errors` + +### Custom styling not applying + +1. **CSS specificity**: Your custom classes might be overridden +2. **Component override**: Use the `components` prop for complex styling +3. **Conditional classes**: Use libraries like `clsx` for dynamic styling + +### Multiple errors showing + +1. **Error key conflicts**: Ensure different FormError components use different `name` props +2. **Server response format**: Check that your server returns the expected error structure + +## Examples Repository + +For complete working examples, see the Storybook stories: + +- `BasicFormError`: Simple server validation failure +- `MixedErrors`: Field + form-level errors together +- `CustomStyling`: Branded error components +- `PlacementVariations`: Different positioning options + +## Related Components + +- [`FormMessage`](./form-message.md): For field-level error messages +- [`TextField`](./text-field.md): Text input with built-in error handling +- [`Checkbox`](./checkbox.md): Checkbox with error support +- [`RadioGroup`](./radio-group.md): Radio buttons with error handling diff --git a/llms.txt b/llms.txt index 8e0b1f3f..bc12aa5a 100644 --- a/llms.txt +++ b/llms.txt @@ -1,6 +1,6 @@ # LambdaCurry Forms - Complete Implementation Guide for LLMs -This comprehensive guide covers everything needed to implement forms using the `@lambdacurry/forms` remix-hook-form components. This documentation is specifically designed for LLMs to understand all features, patterns, and best practices. +This comprehensive guide covers everything needed to implement forms using the `@lambdacurry/forms` remix-hook-form components, including the new FormError component for form-level error handling. This documentation is specifically designed for LLMs to understand all features, patterns, and best practices. ## Core Architecture Overview @@ -12,6 +12,75 @@ The library provides **form-aware wrapper components** in the `remix-hook-form` - Automatic error handling and validation display - Built-in accessibility features +## Form-Level Error Handling with FormError + +The `FormError` component provides standardized form-level error handling, complementing the existing field-level error system. + +### FormError Component Usage + +```typescript +import { FormError } from '@lambdacurry/forms'; + +// Basic usage - looks for errors._form by default + + +// Custom error key + + +// With custom styling and placement + + +// With custom component override + +``` + +### Server Action Pattern for Form-Level Errors + +```typescript +export const action = async ({ request }: ActionFunctionArgs) => { + const { data, errors } = await getValidatedFormData( + request, + zodResolver(formSchema) + ); + + // Return field-level validation errors + if (errors) { + return { errors }; + } + + // Business logic validation + try { + await processForm(data); + return { message: 'Success!' }; + } catch (error) { + // Return form-level error using _form key + return { + errors: { + _form: { message: 'Unable to process form. Please try again.' } + } + }; + } +}; +``` + +### Error Hierarchy Guidelines + +**Field-Level Errors (use FormMessage automatically in form components):** +- Validation errors: "Email is required", "Password too short" +- Format errors: "Invalid email format" +- Field-specific business rules: "Username already taken" + +**Form-Level Errors (use FormError component):** +- Server errors: "Server temporarily unavailable" +- Authentication failures: "Invalid credentials" +- Network issues: "Connection timeout" +- General business logic: "Account suspended" +- Rate limiting: "Too many attempts, try again later" + ## Basic Form Setup Pattern ### 1. Required Imports @@ -21,8 +90,8 @@ import { RemixFormProvider, useRemixForm, getValidatedFormData } from 'remix-hoo import { z } from 'zod'; import { useFetcher, type ActionFunctionArgs } from 'react-router'; -// Import form components -import { TextField, Checkbox } from '@lambdacurry/forms'; +// Import form components including FormError +import { TextField, Checkbox, FormError } from '@lambdacurry/forms'; import { Button } from '@lambdacurry/forms/ui/button'; ``` @@ -38,7 +107,70 @@ const formSchema = z.object({ type FormData = z.infer; ``` -### 3. Form Component Setup +### 3. Complete Login Form Example with FormError + +```typescript +const LoginForm = () => { + const fetcher = useFetcher<{ + message?: string; + errors?: Record + }>(); + + const methods = useRemixForm({ + resolver: zodResolver(formSchema), + defaultValues: { + email: '', + password: '', + }, + fetcher, + submitConfig: { + action: '/login', + method: 'post', + }, + }); + + const isSubmitting = fetcher.state === 'submitting'; + + return ( + + +

Sign In

+ + {/* Form-level error display */} + + + + + + + + + {fetcher.data?.message && ( +
+

{fetcher.data.message}

+
+ )} +
+
+ ); +}; +``` + +### 4. General Form Component Setup ```typescript const MyFormComponent = () => { const fetcher = useFetcher<{ message: string; errors?: Record }>(); @@ -74,7 +206,7 @@ const MyFormComponent = () => { }; ``` -### 4. Server Action Handler +### 5. Server Action Handler with FormError Support ```typescript export const action = async ({ request }: ActionFunctionArgs) => { const { data, errors } = await getValidatedFormData( @@ -86,10 +218,18 @@ export const action = async ({ request }: ActionFunctionArgs) => { return { errors }; } - // Process the validated data - console.log('Form data:', data); - - return { message: 'Form submitted successfully!' }; + // Business logic validation + try { + const user = await authenticateUser(data.email, data.password); + return { message: 'Login successful!', redirectTo: '/dashboard' }; + } catch (error) { + // Return form-level error using _form key + return { + errors: { + _form: { message: 'Invalid credentials. Please try again.' } + } + }; + } }; ``` @@ -1002,8 +1142,90 @@ This comprehensive example serves as a complete reference for implementing any f }; ``` +7. **FormError Placement Options** + ```typescript + // Top of form (most common) + + + // Between sections + + + // Bottom of form + + + // Multiple placements with different styling + + + ``` + +8. **Custom FormError Styling** + ```typescript + const CustomErrorMessage = (props: React.ComponentPropsWithoutRef) => ( +
+ + +
+ ); + + + ``` + +9. **Server Action Error Patterns** + ```typescript + // Field-level error + return { + errors: { + email: { message: 'Email already exists' } + } + }; + + // Form-level error + return { + errors: { + _form: { message: 'Server error occurred' } + } + }; + + // Multiple error types + return { + errors: { + email: { message: 'Email already exists' }, + _form: { message: 'Please fix the errors above' } + } + }; + ``` + +## FormError Best Practices + +### 1. Error Key Conventions +- Use `_form` for general form-level errors +- Use descriptive keys for specific categories (`payment`, `shipping`, `auth`) +- Be consistent across your application + +### 2. Error Message Guidelines +- **Be specific**: "Server temporarily unavailable" vs "Error occurred" +- **Be actionable**: "Please try again in a few minutes" vs "Failed" +- **Be user-friendly**: Avoid technical jargon and error codes + +### 3. Placement Strategy +- **Top placement**: For critical errors that should be seen immediately +- **Inline placement**: For contextual errors related to specific sections +- **Bottom placement**: For summary or less critical errors + +### 4. Component Integration +- FormError works seamlessly with all existing form components +- Maintains the same component override pattern as other form components +- Automatically integrates with form context and validation state + ### Important Reminders: - 🔥 **Always import from `@lambdacurry/forms`** for form-aware components - 🔥 **Use `createFormData()` for custom submissions** to ensure proper formatting - 🔥 **All components are accessible by default** - no additional ARIA setup needed - 🔥 **Form context is automatic** - no need to pass `control` props manually +- 🔥 **FormError provides form-level error handling** - use it for server errors, auth failures, and general business logic errors + +This comprehensive guide provides everything needed to implement forms with both field-level and form-level error handling using the LambdaCurry Forms library! diff --git a/packages/components/package.json b/packages/components/package.json index 12f634ed..59a76647 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@lambdacurry/forms", - "version": "0.17.3", + "version": "0.18.0", "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -32,7 +32,7 @@ "react": "^19.0.0", "react-router": "^7.0.0", "react-router-dom": "^7.0.0", - "remix-hook-form": "7.0.1" + "remix-hook-form": "7.1.0" }, "dependencies": { "@hookform/resolvers": "^3.9.1", @@ -64,7 +64,7 @@ "react-hook-form": "^7.53.1", "react-router": "^7.6.3", "react-router-dom": "^7.6.3", - "remix-hook-form": "7.0.1", + "remix-hook-form": "7.1.0", "sonner": "^1.7.1", "tailwind-merge": "^2.5.5", "tailwindcss-animate": "^1.0.7", @@ -83,7 +83,7 @@ "react": "^19.0.0", "tailwindcss": "^4.0.0", "typescript": "^5.7.2", - "vite": "^5.4.11", + "vite": "^6.2.2", "vite-plugin-dts": "^4.4.0", "vite-tsconfig-paths": "^5.1.4" } diff --git a/packages/components/src/remix-hook-form/form-error.tsx b/packages/components/src/remix-hook-form/form-error.tsx new file mode 100644 index 00000000..fbd6a7ff --- /dev/null +++ b/packages/components/src/remix-hook-form/form-error.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; +import { useRemixFormContext } from 'remix-hook-form'; +import { FormErrorField } from '../ui/form-error-field'; +import type { FormErrorFieldProps } from '../ui/form-error-field'; + +export type FormErrorProps = Omit & { + name?: string; +}; + +export function FormError({ name = '_form', ...props }: FormErrorProps) { + const { control } = useRemixFormContext(); + + return ; +} + +FormError.displayName = 'FormError'; diff --git a/packages/components/src/remix-hook-form/index.ts b/packages/components/src/remix-hook-form/index.ts index 1256860d..fc225664 100644 --- a/packages/components/src/remix-hook-form/index.ts +++ b/packages/components/src/remix-hook-form/index.ts @@ -1,5 +1,6 @@ export * from './checkbox'; export * from './form'; +export * from './form-error'; export * from './date-picker'; export * from './dropdown-menu-select'; export * from './text-field'; diff --git a/packages/components/src/ui/form-error-field.tsx b/packages/components/src/ui/form-error-field.tsx new file mode 100644 index 00000000..9f23bacd --- /dev/null +++ b/packages/components/src/ui/form-error-field.tsx @@ -0,0 +1,42 @@ +import * as React from 'react'; +import type { FieldPath, FieldValues, Control } from 'react-hook-form'; +import { FormField, FormItem, FormMessage } from './form'; +import type { FieldComponents } from './form'; + +export interface FormErrorFieldProps< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +> { + control?: Control; + name: TName; + className?: string; + components?: Partial; +} + +export const FormErrorField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>({ + control, + name, + className, + components, +}: FormErrorFieldProps) => { + return ( + ( + + {fieldState.error && ( + + {fieldState.error.message} + + )} + + )} + /> + ); +}; + +FormErrorField.displayName = 'FormErrorField'; diff --git a/packages/components/src/ui/index.ts b/packages/components/src/ui/index.ts index 4256f51c..e7a37d02 100644 --- a/packages/components/src/ui/index.ts +++ b/packages/components/src/ui/index.ts @@ -6,6 +6,7 @@ export * from './date-picker-field'; export * from './dropdown-menu'; export * from './dropdown-menu-select-field'; export * from './form'; +export * from './form-error-field'; export * from './label'; export * from './otp-input'; export * from './otp-input-field'; diff --git a/yarn.lock b/yarn.lock index 5339a995..3b098463 100644 --- a/yarn.lock +++ b/yarn.lock @@ -904,13 +904,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/aix-ppc64@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/aix-ppc64@npm:0.21.5" - conditions: os=aix & cpu=ppc64 - languageName: node - linkType: hard - "@esbuild/aix-ppc64@npm:0.25.8": version: 0.25.8 resolution: "@esbuild/aix-ppc64@npm:0.25.8" @@ -918,13 +911,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm64@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/android-arm64@npm:0.21.5" - conditions: os=android & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/android-arm64@npm:0.25.8": version: 0.25.8 resolution: "@esbuild/android-arm64@npm:0.25.8" @@ -932,13 +918,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/android-arm@npm:0.21.5" - conditions: os=android & cpu=arm - languageName: node - linkType: hard - "@esbuild/android-arm@npm:0.25.8": version: 0.25.8 resolution: "@esbuild/android-arm@npm:0.25.8" @@ -946,13 +925,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-x64@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/android-x64@npm:0.21.5" - conditions: os=android & cpu=x64 - languageName: node - linkType: hard - "@esbuild/android-x64@npm:0.25.8": version: 0.25.8 resolution: "@esbuild/android-x64@npm:0.25.8" @@ -960,13 +932,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-arm64@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/darwin-arm64@npm:0.21.5" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/darwin-arm64@npm:0.25.8": version: 0.25.8 resolution: "@esbuild/darwin-arm64@npm:0.25.8" @@ -974,13 +939,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-x64@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/darwin-x64@npm:0.21.5" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - "@esbuild/darwin-x64@npm:0.25.8": version: 0.25.8 resolution: "@esbuild/darwin-x64@npm:0.25.8" @@ -988,13 +946,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-arm64@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/freebsd-arm64@npm:0.21.5" - conditions: os=freebsd & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/freebsd-arm64@npm:0.25.8": version: 0.25.8 resolution: "@esbuild/freebsd-arm64@npm:0.25.8" @@ -1002,13 +953,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-x64@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/freebsd-x64@npm:0.21.5" - conditions: os=freebsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/freebsd-x64@npm:0.25.8": version: 0.25.8 resolution: "@esbuild/freebsd-x64@npm:0.25.8" @@ -1016,13 +960,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm64@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/linux-arm64@npm:0.21.5" - conditions: os=linux & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/linux-arm64@npm:0.25.8": version: 0.25.8 resolution: "@esbuild/linux-arm64@npm:0.25.8" @@ -1030,13 +967,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/linux-arm@npm:0.21.5" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - "@esbuild/linux-arm@npm:0.25.8": version: 0.25.8 resolution: "@esbuild/linux-arm@npm:0.25.8" @@ -1044,13 +974,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ia32@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/linux-ia32@npm:0.21.5" - conditions: os=linux & cpu=ia32 - languageName: node - linkType: hard - "@esbuild/linux-ia32@npm:0.25.8": version: 0.25.8 resolution: "@esbuild/linux-ia32@npm:0.25.8" @@ -1058,13 +981,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-loong64@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/linux-loong64@npm:0.21.5" - conditions: os=linux & cpu=loong64 - languageName: node - linkType: hard - "@esbuild/linux-loong64@npm:0.25.8": version: 0.25.8 resolution: "@esbuild/linux-loong64@npm:0.25.8" @@ -1072,13 +988,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-mips64el@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/linux-mips64el@npm:0.21.5" - conditions: os=linux & cpu=mips64el - languageName: node - linkType: hard - "@esbuild/linux-mips64el@npm:0.25.8": version: 0.25.8 resolution: "@esbuild/linux-mips64el@npm:0.25.8" @@ -1086,13 +995,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ppc64@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/linux-ppc64@npm:0.21.5" - conditions: os=linux & cpu=ppc64 - languageName: node - linkType: hard - "@esbuild/linux-ppc64@npm:0.25.8": version: 0.25.8 resolution: "@esbuild/linux-ppc64@npm:0.25.8" @@ -1100,13 +1002,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-riscv64@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/linux-riscv64@npm:0.21.5" - conditions: os=linux & cpu=riscv64 - languageName: node - linkType: hard - "@esbuild/linux-riscv64@npm:0.25.8": version: 0.25.8 resolution: "@esbuild/linux-riscv64@npm:0.25.8" @@ -1114,13 +1009,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-s390x@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/linux-s390x@npm:0.21.5" - conditions: os=linux & cpu=s390x - languageName: node - linkType: hard - "@esbuild/linux-s390x@npm:0.25.8": version: 0.25.8 resolution: "@esbuild/linux-s390x@npm:0.25.8" @@ -1128,13 +1016,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-x64@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/linux-x64@npm:0.21.5" - conditions: os=linux & cpu=x64 - languageName: node - linkType: hard - "@esbuild/linux-x64@npm:0.25.8": version: 0.25.8 resolution: "@esbuild/linux-x64@npm:0.25.8" @@ -1149,13 +1030,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/netbsd-x64@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/netbsd-x64@npm:0.21.5" - conditions: os=netbsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/netbsd-x64@npm:0.25.8": version: 0.25.8 resolution: "@esbuild/netbsd-x64@npm:0.25.8" @@ -1170,13 +1044,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/openbsd-x64@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/openbsd-x64@npm:0.21.5" - conditions: os=openbsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/openbsd-x64@npm:0.25.8": version: 0.25.8 resolution: "@esbuild/openbsd-x64@npm:0.25.8" @@ -1191,13 +1058,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/sunos-x64@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/sunos-x64@npm:0.21.5" - conditions: os=sunos & cpu=x64 - languageName: node - linkType: hard - "@esbuild/sunos-x64@npm:0.25.8": version: 0.25.8 resolution: "@esbuild/sunos-x64@npm:0.25.8" @@ -1205,13 +1065,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-arm64@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/win32-arm64@npm:0.21.5" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/win32-arm64@npm:0.25.8": version: 0.25.8 resolution: "@esbuild/win32-arm64@npm:0.25.8" @@ -1219,13 +1072,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-ia32@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/win32-ia32@npm:0.21.5" - conditions: os=win32 & cpu=ia32 - languageName: node - linkType: hard - "@esbuild/win32-ia32@npm:0.25.8": version: 0.25.8 resolution: "@esbuild/win32-ia32@npm:0.25.8" @@ -1233,13 +1079,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-x64@npm:0.21.5": - version: 0.21.5 - resolution: "@esbuild/win32-x64@npm:0.21.5" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - "@esbuild/win32-x64@npm:0.25.8": version: 0.25.8 resolution: "@esbuild/win32-x64@npm:0.25.8" @@ -1746,7 +1585,7 @@ __metadata: react: "npm:^19.0.0" react-hook-form: "npm:^7.51.0" react-router: "npm:^7.6.1" - remix-hook-form: "npm:^7.0.1" + remix-hook-form: "npm:^7.1.0" start-server-and-test: "npm:^2.0.11" storybook: "npm:^9.0.6" tailwindcss: "npm:^4.0.0" @@ -1800,13 +1639,13 @@ __metadata: react-hook-form: "npm:^7.53.1" react-router: "npm:^7.6.3" react-router-dom: "npm:^7.6.3" - remix-hook-form: "npm:7.0.1" + remix-hook-form: "npm:7.1.0" sonner: "npm:^1.7.1" tailwind-merge: "npm:^2.5.5" tailwindcss: "npm:^4.0.0" tailwindcss-animate: "npm:^1.0.7" typescript: "npm:^5.7.2" - vite: "npm:^5.4.11" + vite: "npm:^6.2.2" vite-plugin-dts: "npm:^4.4.0" vite-tsconfig-paths: "npm:^5.1.4" zod: "npm:^3.24.1" @@ -1814,7 +1653,7 @@ __metadata: react: ^19.0.0 react-router: ^7.0.0 react-router-dom: ^7.0.0 - remix-hook-form: 7.0.1 + remix-hook-form: 7.1.0 languageName: unknown linkType: soft @@ -2973,142 +2812,142 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-android-arm-eabi@npm:4.45.3": - version: 4.45.3 - resolution: "@rollup/rollup-android-arm-eabi@npm:4.45.3" +"@rollup/rollup-android-arm-eabi@npm:4.46.0": + version: 4.46.0 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.46.0" conditions: os=android & cpu=arm languageName: node linkType: hard -"@rollup/rollup-android-arm64@npm:4.45.3": - version: 4.45.3 - resolution: "@rollup/rollup-android-arm64@npm:4.45.3" +"@rollup/rollup-android-arm64@npm:4.46.0": + version: 4.46.0 + resolution: "@rollup/rollup-android-arm64@npm:4.46.0" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-arm64@npm:4.45.3": - version: 4.45.3 - resolution: "@rollup/rollup-darwin-arm64@npm:4.45.3" +"@rollup/rollup-darwin-arm64@npm:4.46.0": + version: 4.46.0 + resolution: "@rollup/rollup-darwin-arm64@npm:4.46.0" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-x64@npm:4.45.3": - version: 4.45.3 - resolution: "@rollup/rollup-darwin-x64@npm:4.45.3" +"@rollup/rollup-darwin-x64@npm:4.46.0": + version: 4.46.0 + resolution: "@rollup/rollup-darwin-x64@npm:4.46.0" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-freebsd-arm64@npm:4.45.3": - version: 4.45.3 - resolution: "@rollup/rollup-freebsd-arm64@npm:4.45.3" +"@rollup/rollup-freebsd-arm64@npm:4.46.0": + version: 4.46.0 + resolution: "@rollup/rollup-freebsd-arm64@npm:4.46.0" conditions: os=freebsd & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-freebsd-x64@npm:4.45.3": - version: 4.45.3 - resolution: "@rollup/rollup-freebsd-x64@npm:4.45.3" +"@rollup/rollup-freebsd-x64@npm:4.46.0": + version: 4.46.0 + resolution: "@rollup/rollup-freebsd-x64@npm:4.46.0" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-linux-arm-gnueabihf@npm:4.45.3": - version: 4.45.3 - resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.45.3" +"@rollup/rollup-linux-arm-gnueabihf@npm:4.46.0": + version: 4.46.0 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.46.0" conditions: os=linux & cpu=arm & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm-musleabihf@npm:4.45.3": - version: 4.45.3 - resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.45.3" +"@rollup/rollup-linux-arm-musleabihf@npm:4.46.0": + version: 4.46.0 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.46.0" conditions: os=linux & cpu=arm & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-arm64-gnu@npm:4.45.3": - version: 4.45.3 - resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.45.3" +"@rollup/rollup-linux-arm64-gnu@npm:4.46.0": + version: 4.46.0 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.46.0" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm64-musl@npm:4.45.3": - version: 4.45.3 - resolution: "@rollup/rollup-linux-arm64-musl@npm:4.45.3" +"@rollup/rollup-linux-arm64-musl@npm:4.46.0": + version: 4.46.0 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.46.0" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-loongarch64-gnu@npm:4.45.3": - version: 4.45.3 - resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.45.3" +"@rollup/rollup-linux-loongarch64-gnu@npm:4.46.0": + version: 4.46.0 + resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.46.0" conditions: os=linux & cpu=loong64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-ppc64-gnu@npm:4.45.3": - version: 4.45.3 - resolution: "@rollup/rollup-linux-ppc64-gnu@npm:4.45.3" +"@rollup/rollup-linux-ppc64-gnu@npm:4.46.0": + version: 4.46.0 + resolution: "@rollup/rollup-linux-ppc64-gnu@npm:4.46.0" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-riscv64-gnu@npm:4.45.3": - version: 4.45.3 - resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.45.3" +"@rollup/rollup-linux-riscv64-gnu@npm:4.46.0": + version: 4.46.0 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.46.0" conditions: os=linux & cpu=riscv64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-riscv64-musl@npm:4.45.3": - version: 4.45.3 - resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.45.3" +"@rollup/rollup-linux-riscv64-musl@npm:4.46.0": + version: 4.46.0 + resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.46.0" conditions: os=linux & cpu=riscv64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-s390x-gnu@npm:4.45.3": - version: 4.45.3 - resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.45.3" +"@rollup/rollup-linux-s390x-gnu@npm:4.46.0": + version: 4.46.0 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.46.0" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-gnu@npm:4.45.3": - version: 4.45.3 - resolution: "@rollup/rollup-linux-x64-gnu@npm:4.45.3" +"@rollup/rollup-linux-x64-gnu@npm:4.46.0": + version: 4.46.0 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.46.0" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-musl@npm:4.45.3": - version: 4.45.3 - resolution: "@rollup/rollup-linux-x64-musl@npm:4.45.3" +"@rollup/rollup-linux-x64-musl@npm:4.46.0": + version: 4.46.0 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.46.0" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-win32-arm64-msvc@npm:4.45.3": - version: 4.45.3 - resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.45.3" +"@rollup/rollup-win32-arm64-msvc@npm:4.46.0": + version: 4.46.0 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.46.0" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-win32-ia32-msvc@npm:4.45.3": - version: 4.45.3 - resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.45.3" +"@rollup/rollup-win32-ia32-msvc@npm:4.46.0": + version: 4.46.0 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.46.0" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@rollup/rollup-win32-x64-msvc@npm:4.45.3": - version: 4.45.3 - resolution: "@rollup/rollup-win32-x64-msvc@npm:4.45.3" +"@rollup/rollup-win32-x64-msvc@npm:4.46.0": + version: 4.46.0 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.46.0" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -4335,30 +4174,30 @@ __metadata: languageName: node linkType: hard -"@volar/language-core@npm:2.4.20, @volar/language-core@npm:~2.4.11": - version: 2.4.20 - resolution: "@volar/language-core@npm:2.4.20" +"@volar/language-core@npm:2.4.22, @volar/language-core@npm:~2.4.11": + version: 2.4.22 + resolution: "@volar/language-core@npm:2.4.22" dependencies: - "@volar/source-map": "npm:2.4.20" - checksum: 10c0/af2dfd7fa2b615e1a54d9db1f13a62d1c7e7c6444b42a6b864e33041640c8bd3da4aa7c9dff61f4e31ab5a9b330367ff398423bf19c88a06eafbd39de707c493 + "@volar/source-map": "npm:2.4.22" + checksum: 10c0/3b8f713e02c33919a04108796f6a1c177d6a6521d38b4355381e886364615e5601c7642ffd1378a3ebc24cd157990da38702ff47ece43342ce2707037b37e3b2 languageName: node linkType: hard -"@volar/source-map@npm:2.4.20": - version: 2.4.20 - resolution: "@volar/source-map@npm:2.4.20" - checksum: 10c0/ec657fcdd80e0f887847ecdff9af78804b1ca2e4d04adf06e1709058b57d4c0aaba905a582294d8fa0cce42ed6aac8c5174629439ec030e4c242f9040e013119 +"@volar/source-map@npm:2.4.22": + version: 2.4.22 + resolution: "@volar/source-map@npm:2.4.22" + checksum: 10c0/d145fb189adba8883299caeb770e76b4499b2087d74b779c049664591ca946bafa1b9516108331df5e22c106988939d2f0cfb57f6412dd7ea7b8327556efe069 languageName: node linkType: hard "@volar/typescript@npm:^2.4.11": - version: 2.4.20 - resolution: "@volar/typescript@npm:2.4.20" + version: 2.4.22 + resolution: "@volar/typescript@npm:2.4.22" dependencies: - "@volar/language-core": "npm:2.4.20" + "@volar/language-core": "npm:2.4.22" path-browserify: "npm:^1.0.1" vscode-uri: "npm:^3.0.8" - checksum: 10c0/70d15f42fa29f71cb18b3f19672169f271d7a7efe09a05e51b39945bd2aeadb291f0f6c7b5191a519b652a8fcae40820fefa9d44b4460f6f5ffac9e84a3385f2 + checksum: 10c0/f83ac0db6cfd420a249d9c24bf0761903b526fbfec1087f726bd18c94a8ef133d7a8a62d26b4781c6d3dccd70459c1a608d75b96ff4bcb5f91d9b7cea7a73897 languageName: node linkType: hard @@ -5959,86 +5798,6 @@ __metadata: languageName: node linkType: hard -"esbuild@npm:^0.21.3": - version: 0.21.5 - resolution: "esbuild@npm:0.21.5" - dependencies: - "@esbuild/aix-ppc64": "npm:0.21.5" - "@esbuild/android-arm": "npm:0.21.5" - "@esbuild/android-arm64": "npm:0.21.5" - "@esbuild/android-x64": "npm:0.21.5" - "@esbuild/darwin-arm64": "npm:0.21.5" - "@esbuild/darwin-x64": "npm:0.21.5" - "@esbuild/freebsd-arm64": "npm:0.21.5" - "@esbuild/freebsd-x64": "npm:0.21.5" - "@esbuild/linux-arm": "npm:0.21.5" - "@esbuild/linux-arm64": "npm:0.21.5" - "@esbuild/linux-ia32": "npm:0.21.5" - "@esbuild/linux-loong64": "npm:0.21.5" - "@esbuild/linux-mips64el": "npm:0.21.5" - "@esbuild/linux-ppc64": "npm:0.21.5" - "@esbuild/linux-riscv64": "npm:0.21.5" - "@esbuild/linux-s390x": "npm:0.21.5" - "@esbuild/linux-x64": "npm:0.21.5" - "@esbuild/netbsd-x64": "npm:0.21.5" - "@esbuild/openbsd-x64": "npm:0.21.5" - "@esbuild/sunos-x64": "npm:0.21.5" - "@esbuild/win32-arm64": "npm:0.21.5" - "@esbuild/win32-ia32": "npm:0.21.5" - "@esbuild/win32-x64": "npm:0.21.5" - dependenciesMeta: - "@esbuild/aix-ppc64": - optional: true - "@esbuild/android-arm": - optional: true - "@esbuild/android-arm64": - optional: true - "@esbuild/android-x64": - optional: true - "@esbuild/darwin-arm64": - optional: true - "@esbuild/darwin-x64": - optional: true - "@esbuild/freebsd-arm64": - optional: true - "@esbuild/freebsd-x64": - optional: true - "@esbuild/linux-arm": - optional: true - "@esbuild/linux-arm64": - optional: true - "@esbuild/linux-ia32": - optional: true - "@esbuild/linux-loong64": - optional: true - "@esbuild/linux-mips64el": - optional: true - "@esbuild/linux-ppc64": - optional: true - "@esbuild/linux-riscv64": - optional: true - "@esbuild/linux-s390x": - optional: true - "@esbuild/linux-x64": - optional: true - "@esbuild/netbsd-x64": - optional: true - "@esbuild/openbsd-x64": - optional: true - "@esbuild/sunos-x64": - optional: true - "@esbuild/win32-arm64": - optional: true - "@esbuild/win32-ia32": - optional: true - "@esbuild/win32-x64": - optional: true - bin: - esbuild: bin/esbuild - checksum: 10c0/fa08508adf683c3f399e8a014a6382a6b65542213431e26206c0720e536b31c09b50798747c2a105a4bbba1d9767b8d3615a74c2f7bf1ddf6d836cd11eb672de - languageName: node - linkType: hard - "escalade@npm:^3.1.1, escalade@npm:^3.2.0": version: 3.2.0 resolution: "escalade@npm:3.2.0" @@ -9308,7 +9067,7 @@ __metadata: languageName: node linkType: hard -"postcss@npm:^8.4.41, postcss@npm:^8.4.43, postcss@npm:^8.5.3, postcss@npm:^8.5.6": +"postcss@npm:^8.4.41, postcss@npm:^8.5.3, postcss@npm:^8.5.6": version: 8.5.6 resolution: "postcss@npm:8.5.6" dependencies: @@ -9714,19 +9473,7 @@ __metadata: languageName: node linkType: hard -"remix-hook-form@npm:7.0.1": - version: 7.0.1 - resolution: "remix-hook-form@npm:7.0.1" - peerDependencies: - react: ^18.2.0 || ^19.0.0 - react-dom: ^18.2.0 || ^19.0.0 - react-hook-form: ^7.55.0 - react-router: ">=7.5.0" - checksum: 10c0/5fe89c4a72aa65ec1c96d2420e7db37ae28a3711cb7968bd74a7a36e0fea7df8d0c4996beedfc76a24f2fe545e368b067f7e93ce9f15727b03149db92e29b304 - languageName: node - linkType: hard - -"remix-hook-form@npm:^7.0.1": +"remix-hook-form@npm:7.1.0, remix-hook-form@npm:^7.1.0": version: 7.1.0 resolution: "remix-hook-form@npm:7.1.0" peerDependencies: @@ -9850,30 +9597,30 @@ __metadata: languageName: node linkType: hard -"rollup@npm:^4.20.0, rollup@npm:^4.34.9, rollup@npm:^4.40.0": - version: 4.45.3 - resolution: "rollup@npm:4.45.3" - dependencies: - "@rollup/rollup-android-arm-eabi": "npm:4.45.3" - "@rollup/rollup-android-arm64": "npm:4.45.3" - "@rollup/rollup-darwin-arm64": "npm:4.45.3" - "@rollup/rollup-darwin-x64": "npm:4.45.3" - "@rollup/rollup-freebsd-arm64": "npm:4.45.3" - "@rollup/rollup-freebsd-x64": "npm:4.45.3" - "@rollup/rollup-linux-arm-gnueabihf": "npm:4.45.3" - "@rollup/rollup-linux-arm-musleabihf": "npm:4.45.3" - "@rollup/rollup-linux-arm64-gnu": "npm:4.45.3" - "@rollup/rollup-linux-arm64-musl": "npm:4.45.3" - "@rollup/rollup-linux-loongarch64-gnu": "npm:4.45.3" - "@rollup/rollup-linux-ppc64-gnu": "npm:4.45.3" - "@rollup/rollup-linux-riscv64-gnu": "npm:4.45.3" - "@rollup/rollup-linux-riscv64-musl": "npm:4.45.3" - "@rollup/rollup-linux-s390x-gnu": "npm:4.45.3" - "@rollup/rollup-linux-x64-gnu": "npm:4.45.3" - "@rollup/rollup-linux-x64-musl": "npm:4.45.3" - "@rollup/rollup-win32-arm64-msvc": "npm:4.45.3" - "@rollup/rollup-win32-ia32-msvc": "npm:4.45.3" - "@rollup/rollup-win32-x64-msvc": "npm:4.45.3" +"rollup@npm:^4.34.9, rollup@npm:^4.40.0": + version: 4.46.0 + resolution: "rollup@npm:4.46.0" + dependencies: + "@rollup/rollup-android-arm-eabi": "npm:4.46.0" + "@rollup/rollup-android-arm64": "npm:4.46.0" + "@rollup/rollup-darwin-arm64": "npm:4.46.0" + "@rollup/rollup-darwin-x64": "npm:4.46.0" + "@rollup/rollup-freebsd-arm64": "npm:4.46.0" + "@rollup/rollup-freebsd-x64": "npm:4.46.0" + "@rollup/rollup-linux-arm-gnueabihf": "npm:4.46.0" + "@rollup/rollup-linux-arm-musleabihf": "npm:4.46.0" + "@rollup/rollup-linux-arm64-gnu": "npm:4.46.0" + "@rollup/rollup-linux-arm64-musl": "npm:4.46.0" + "@rollup/rollup-linux-loongarch64-gnu": "npm:4.46.0" + "@rollup/rollup-linux-ppc64-gnu": "npm:4.46.0" + "@rollup/rollup-linux-riscv64-gnu": "npm:4.46.0" + "@rollup/rollup-linux-riscv64-musl": "npm:4.46.0" + "@rollup/rollup-linux-s390x-gnu": "npm:4.46.0" + "@rollup/rollup-linux-x64-gnu": "npm:4.46.0" + "@rollup/rollup-linux-x64-musl": "npm:4.46.0" + "@rollup/rollup-win32-arm64-msvc": "npm:4.46.0" + "@rollup/rollup-win32-ia32-msvc": "npm:4.46.0" + "@rollup/rollup-win32-x64-msvc": "npm:4.46.0" "@types/estree": "npm:1.0.8" fsevents: "npm:~2.3.2" dependenciesMeta: @@ -9921,7 +9668,7 @@ __metadata: optional: true bin: rollup: dist/bin/rollup - checksum: 10c0/6a68a81cbff9a05aec3524bae411400ad686217ab656462708b4091435dc17b0011b741de658f679e87da55c7b6c5dfbbc9c03f18ee737f39a2f94ca33debffc + checksum: 10c0/0e85ffd629de5ac8b714ce71ecf354cd3ef3357b430edb4103a3cae3fbff380a2680083dd6428d196584a6b287b4fa05d27563f85ac66400fb26d0dc5a6bb731 languageName: node linkType: hard @@ -11202,49 +10949,6 @@ __metadata: languageName: node linkType: hard -"vite@npm:^5.4.11": - version: 5.4.19 - resolution: "vite@npm:5.4.19" - dependencies: - esbuild: "npm:^0.21.3" - fsevents: "npm:~2.3.3" - postcss: "npm:^8.4.43" - rollup: "npm:^4.20.0" - peerDependencies: - "@types/node": ^18.0.0 || >=20.0.0 - less: "*" - lightningcss: ^1.21.0 - sass: "*" - sass-embedded: "*" - stylus: "*" - sugarss: "*" - terser: ^5.4.0 - dependenciesMeta: - fsevents: - optional: true - peerDependenciesMeta: - "@types/node": - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - bin: - vite: bin/vite.js - checksum: 10c0/c97601234dba482cea5290f2a2ea0fcd65e1fab3df06718ea48adc8ceb14bc3129508216c4989329c618f6a0470b42f439677a207aef62b0c76f445091c2d89e - languageName: node - linkType: hard - "vite@npm:^6.2.2": version: 6.3.5 resolution: "vite@npm:6.3.5"