diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 01c6946a..7bc9ce25 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,17 +34,6 @@ jobs: - name: Install Playwright Chromium run: npx playwright install chromium - - name: Build Storybook - run: yarn build-storybook - - - name: Start Storybook server - run: yarn workspace @lambdacurry/forms-docs storybook --ci --port 6006 & - env: - NODE_OPTIONS: --max-old-space-size=4096 - - - name: Wait for Storybook to be ready - run: npx wait-on http://localhost:6006 - - name: Run tests run: yarn test diff --git a/.gitignore b/.gitignore index 584f29c9..5f3020a1 100644 --- a/.gitignore +++ b/.gitignore @@ -179,4 +179,7 @@ dist .turbo *storybook.log -storybook-static \ No newline at end of file +storybook-static + +# React Router v7 +.react-router/ \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index d07ff312..f8b9823a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,6 +7,7 @@ "hookform", "isbot", "lucide", + "Nuqs", "shadcn", "sonner" ], @@ -20,5 +21,6 @@ "editor.codeActionsOnSave": { "source.fixAll.biome": "explicit", "source.organizeImports.biome": "explicit" - } + }, + "tailwindCSS.classAttributes": ["class", "className", "ngClass", "class:list", "wrapperClassName"] } diff --git a/.yarnrc.yml b/.yarnrc.yml index 8b757b29..3186f3f0 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -1 +1 @@ -nodeLinker: node-modules \ No newline at end of file +nodeLinker: node-modules diff --git a/ai/CustomInputsProject.md b/ai/CustomInputsProject.md index 585e684a..f37262b0 100644 --- a/ai/CustomInputsProject.md +++ b/ai/CustomInputsProject.md @@ -484,7 +484,8 @@ import { expect, userEvent } from '@storybook/test'; import * as React from 'react'; import { RemixFormProvider, getValidatedFormData, useRemixForm } from 'remix-hook-form'; import { z } from 'zod'; -import { withRemixStubDecorator } from '../lib/storybook/remix-stub'; + +import { withReactRouterStubDecorator } from '../lib/storybook/react-router-stub'; // Custom checkbox component const CustomCheckbox = React.forwardRef< @@ -597,6 +598,18 @@ const handleFormSubmission = async (request: Request) => { // Story definition export const CustomComponents: Story = { render: () => , + decorators: [ + withReactRouterStubDecorator({ + routes: [ + { + path: '/', + action: async ({ request }: ActionFunctionArgs) => { + return handleFormSubmission(request); + }, + }, + ], + }), + ], parameters: { docs: { description: { diff --git a/apps/docs/.storybook/main.ts b/apps/docs/.storybook/main.ts index 77bdd2e7..fa85d0eb 100644 --- a/apps/docs/.storybook/main.ts +++ b/apps/docs/.storybook/main.ts @@ -1,7 +1,5 @@ import { dirname, join } from 'node:path'; import type { StorybookConfig } from '@storybook/react-vite'; -import { mergeConfig } from 'vite'; -import viteConfig from '../vite.config'; /** * This function is used to resolve the absolute path of a package. @@ -21,7 +19,9 @@ const config: StorybookConfig = { name: getAbsolutePath('@storybook/react-vite'), options: {}, }, - viteFinal: (config) => { + viteFinal: async (config) => { + const { mergeConfig } = await import('vite'); + const viteConfig = await import('../vite.config.mjs'); return mergeConfig(config, viteConfig); }, }; diff --git a/apps/docs/package.json b/apps/docs/package.json index 6012b2f5..2376e1b1 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -6,35 +6,39 @@ "build": "storybook build", "build-storybook": "storybook build", "storybook": "storybook dev -p 6006", - "test": "test-storybook" + "serve": "http-server ./storybook-static -p 6006 -s", + "test": "start-server-and-test serve http://127.0.0.1:6006 'test-storybook --url http://127.0.0.1:6006'", + "test:local": "test-storybook" }, "dependencies": { "@lambdacurry/forms": "*", - "@storybook/addon-essentials": "^8.4.7", - "@storybook/addon-interactions": "^8.4.7", - "@storybook/addon-links": "^8.4.7", - "@storybook/blocks": "^8.4.7", - "@storybook/react": "^8.4.7", - "@storybook/react-vite": "^8.4.7", - "@storybook/test": "^8.4.7", - "storybook": "^8.4.7" + "@storybook/addon-essentials": "^8.6.7", + "@storybook/addon-interactions": "^8.6.7", + "@storybook/addon-links": "^8.6.7", + "@storybook/blocks": "^8.6.7", + "@storybook/react": "^8.6.7", + "@storybook/react-vite": "^8.6.7", + "@storybook/test": "^8.6.7", + "storybook": "^8.6.7" }, "devDependencies": { - "@remix-run/dev": "^2.15.1", - "@remix-run/testing": "^2.15.1", - "@storybook/test-runner": "^0.20.1", + "@react-router/dev": "^7.0.0", + "@storybook/test-runner": "^0.22.0", "@storybook/testing-library": "^0.2.2", - "@types/react": "^18.0.0", - "@types/react-dom": "^18.0.0", + "@tailwindcss/vite": "^4.0.0", + "@types/react": "^19.0.0", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", "autoprefixer": "^10.4.20", - "postcss": "^8.4.49", - "react": "^18.0.0", - "react-dom": "^18.0.0", - "tailwindcss": "^3.4.17", + "http-server": "^14.1.1", + "react": "^19.0.0", + "react-router": "^7.0.0", + "react-router-dom": "^7.0.0", + "start-server-and-test": "^2.0.11", + "tailwindcss": "^4.0.0", "typescript": "^5.7.2", - "vite": "^5.4.11", - "vite-tsconfig-paths": "^5.1.4" + "vite": "^6.2.2", + "vite-tsconfig-paths": "^5.1.4", + "wait-on": "^8.0.3" } } diff --git a/apps/docs/postcss.config.js b/apps/docs/postcss.config.js deleted file mode 100644 index 12a703d9..00000000 --- a/apps/docs/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -}; diff --git a/apps/docs/src/examples/middleware-example.tsx b/apps/docs/src/examples/middleware-example.tsx new file mode 100644 index 00000000..10d968bc --- /dev/null +++ b/apps/docs/src/examples/middleware-example.tsx @@ -0,0 +1,79 @@ +// Example of using the new middleware feature in remix-hook-form v7.0.0 +import { Form } from 'react-router'; +import { RemixFormProvider, useRemixForm } from 'remix-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import * as zod from 'zod'; +import { TextField } from '@lambdacurry/forms/remix-hook-form'; +import { getValidatedFormData } from 'remix-hook-form/middleware'; +import type { ActionFunctionArgs } from 'react-router'; + +// Define schema and types +const schema = zod.object({ + name: zod.string().min(1, "Name is required"), + email: zod.string().email("Invalid email format").min(1, "Email is required"), +}); + +type FormData = zod.infer; +const resolver = zodResolver(schema); + +// Action function using the new middleware +export const action = async ({ context }: ActionFunctionArgs) => { + // Use the middleware to extract and validate form data + const { errors, data, receivedValues } = await getValidatedFormData( + context, + resolver + ); + + if (errors) { + return { errors, defaultValues: receivedValues }; + } + + // Process the validated data + console.log('Processing data:', data); + + return { success: true, data }; +}; + +// Component +export default function MiddlewareExample() { + const { + handleSubmit, + formState: { errors }, + register, + } = useRemixForm({ + mode: "onSubmit", + resolver, + }); + + return ( +
+

Remix Hook Form v7 Middleware Example

+ + +
+
+ + + + + +
+
+
+
+ ); +} diff --git a/apps/docs/src/examples/root-example.tsx b/apps/docs/src/examples/root-example.tsx new file mode 100644 index 00000000..586a1a50 --- /dev/null +++ b/apps/docs/src/examples/root-example.tsx @@ -0,0 +1,23 @@ +// Example of setting up the middleware in root.tsx +import { unstable_extractFormDataMiddleware } from 'remix-hook-form/middleware'; +import { Outlet } from 'react-router-dom'; + +// Export the middleware for React Router 7 +export const unstable_middleware = [unstable_extractFormDataMiddleware()]; + +export default function Root() { + return ( + + + + + Remix Hook Form v7 Example + + +
+ +
+ + + ); +} diff --git a/apps/docs/src/lib/storybook/react-router-stub.tsx b/apps/docs/src/lib/storybook/react-router-stub.tsx new file mode 100644 index 00000000..901b3f02 --- /dev/null +++ b/apps/docs/src/lib/storybook/react-router-stub.tsx @@ -0,0 +1,85 @@ +import type { Decorator } from '@storybook/react'; +import type { ComponentType } from 'react'; +import { + type ActionFunction, + type IndexRouteObject, + type LinksFunction, + type LoaderFunction, + type MetaFunction, + type NonIndexRouteObject, + RouterProvider, + createMemoryRouter, +} from 'react-router-dom'; + +export type StubRouteObject = StubIndexRouteObject | StubNonIndexRouteObject; + +interface StubNonIndexRouteObject + extends Omit { + loader?: LoaderFunction; + action?: ActionFunction; + children?: StubRouteObject[]; + meta?: MetaFunction; + links?: LinksFunction; + // biome-ignore lint/suspicious/noExplicitAny: allow any here + Component?: ComponentType; +} + +interface StubIndexRouteObject + extends Omit { + loader?: LoaderFunction; + action?: ActionFunction; + children?: StubRouteObject[]; + meta?: MetaFunction; + links?: LinksFunction; + // biome-ignore lint/suspicious/noExplicitAny: allow any here + Component?: ComponentType; +} + +interface RemixStubOptions { + routes: StubRouteObject[]; + initialPath?: string; +} + +export const withReactRouterStubDecorator = (options: RemixStubOptions): Decorator => { + const { routes, initialPath = '/' } = options; + // This outer function runs once when Storybook loads the story meta + + return (Story, context) => { + // This inner function runs when the story component actually renders + const mappedRoutes = routes.map((route) => ({ + ...route, + Component: route.Component ?? (() => ), + })); + + // Get the base path (without existing query params from options) + const basePath = initialPath.split('?')[0]; + + // Get the current search string from the actual browser window, if available + // If not available, use a default search string with parameters needed for the data table + const currentWindowSearch = typeof window !== 'undefined' + ? window.location.search + : '?page=0&pageSize=10'; + + // Combine them for the initial entry + const actualInitialPath = `${basePath}${currentWindowSearch}`; + + // Create a memory router, initializing it with the path derived from the window's search params + // biome-ignore lint/suspicious/noExplicitAny: + const router = createMemoryRouter(mappedRoutes as any, { + initialEntries: [actualInitialPath], // Use the path combined with window.location.search + }); + + return ; + }; +}; + +/** + * A decorator that provides URL state management for stories + * Use this when you need URL query parameters in your stories + */ +export const withURLState = (initialPath = '/'): Decorator => { + return withReactRouterStubDecorator({ + routes: [{ path: '/' }], + initialPath, + }); +}; diff --git a/apps/docs/src/lib/storybook/remix-stub.tsx b/apps/docs/src/lib/storybook/remix-stub.tsx deleted file mode 100644 index 93133319..00000000 --- a/apps/docs/src/lib/storybook/remix-stub.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import type { ActionFunction, LinksFunction, LoaderFunction, MetaFunction } from '@remix-run/node'; -import { createRemixStub } from '@remix-run/testing'; -import type { Decorator } from '@storybook/react'; -import type { ComponentType } from 'react'; -import type { IndexRouteObject, NonIndexRouteObject } from 'react-router-dom'; - -export type StubRouteObject = StubIndexRouteObject | StubNonIndexRouteObject; - -interface StubNonIndexRouteObject - extends Omit { - loader?: LoaderFunction; - action?: ActionFunction; - children?: StubRouteObject[]; - meta?: MetaFunction; - links?: LinksFunction; - // biome-ignore lint/suspicious/noExplicitAny: allow any here - Component?: ComponentType; -} - -interface StubIndexRouteObject - extends Omit { - loader?: LoaderFunction; - action?: ActionFunction; - children?: StubRouteObject[]; - meta?: MetaFunction; - links?: LinksFunction; - // biome-ignore lint/suspicious/noExplicitAny: allow any here - Component?: ComponentType; -} - -interface RemixStubOptions { - root?: { - // biome-ignore lint/suspicious/noExplicitAny: allow any here - Component?: ComponentType; - loader?: LoaderFunction; - action?: ActionFunction; - meta?: MetaFunction; - links?: LinksFunction; - }; - routes?: StubRouteObject[]; -} - -export const withRemixStubDecorator = (options: RemixStubOptions = {}): Decorator => { - return (Story) => { - const { root, routes = [] } = options; - - // Map routes to include Story component as fallback if no Component provided - const mappedRoutes = routes.map((route) => ({ - ...route, - Component: route.Component ? route.Component : () => , - })); - const rootRoute: StubRouteObject = { - id: 'root', - path: '/', - ...root, - Component: root?.Component ? root.Component : () => , - children: - mappedRoutes.length > 0 - ? mappedRoutes.map((route) => ({ - action: () => null, - ...route, - })) - : undefined, - }; - - const RemixStub = createRemixStub([rootRoute]); - - // You can also provide hydrationData if needed - return ; - }; -}; diff --git a/apps/docs/src/main.css b/apps/docs/src/main.css index 96eed7ed..0272199c 100644 --- a/apps/docs/src/main.css +++ b/apps/docs/src/main.css @@ -1,81 +1,108 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; +@import 'tailwindcss'; +@source "../../../packages/components"; :root { - --background: 0 0% 100%; - --foreground: 222.2 47.4% 11.2%; + --background: hsl(0 0% 100%); + --foreground: hsl(222.2 47.4% 11.2%); - --muted: 210 40% 96.1%; - --muted-foreground: 215.4 16.3% 46.9%; + --muted: hsl(210 40% 96.1%); + --muted-foreground: hsl(215.4 16.3% 46.9%); - --popover: 0 0% 100%; - --popover-foreground: 222.2 47.4% 11.2%; + --popover: hsl(0 0% 100%); + --popover-foreground: hsl(222.2 47.4% 11.2%); - --border: 214.3 31.8% 91.4%; - --input: 214.3 31.8% 91.4%; + --border: hsl(214.3 31.8% 91.4%); + --input: hsl(214.3 31.8% 91.4%); - --card: 0 0% 100%; - --card-foreground: 222.2 47.4% 11.2%; + --card: hsl(0 0% 100%); + --card-foreground: hsl(222.2 47.4% 11.2%); - --primary: 222.2 47.4% 11.2%; - --primary-foreground: 210 40% 98%; + --primary: hsl(222.2 47.4% 11.2%); + --primary-foreground: hsl(210 40% 98%); - --secondary: 210 40% 96.1%; - --secondary-foreground: 222.2 47.4% 11.2%; + --secondary: hsl(210 40% 96.1%); + --secondary-foreground: hsl(222.2 47.4% 11.2%); - --accent: 210 40% 96.1%; - --accent-foreground: 222.2 47.4% 11.2%; + --accent: hsl(210 40% 96.1%); + --accent-foreground: hsl(222.2 47.4% 11.2%); - --destructive: 0 100% 50%; - --destructive-foreground: 210 40% 98%; + --destructive: hsl(0 100% 50%); + --destructive-foreground: hsl(210 40% 98%); - --ring: 215 20.2% 65.1%; + --ring: hsl(215 20.2% 65.1%); --radius: 0.5rem; } -@layer base { - .dark { - --background: 224 71% 4%; - --foreground: 213 31% 91%; +.dark { + --background: hsl(224 71% 4%); + --foreground: hsl(213 31% 91%); - --muted: 223 47% 11%; - --muted-foreground: 215.4 16.3% 56.9%; + --muted: hsl(223 47% 11%); + --muted-foreground: hsl(215.4 16.3% 56.9%); - --accent: 216 34% 17%; - --accent-foreground: 210 40% 98%; + --accent: hsl(216 34% 17%); + --accent-foreground: hsl(210 40% 98%); - --popover: 224 71% 4%; - --popover-foreground: 215 20.2% 65.1%; + --popover: hsl(224 71% 4%); + --popover-foreground: hsl(215 20.2% 65.1%); - --border: 216 34% 17%; - --input: 216 34% 17%; + --border: hsl(216 34% 17%); + --input: hsl(216 34% 17%); - --card: 224 71% 4%; - --card-foreground: 213 31% 91%; + --card: hsl(224 71% 4%); + --card-foreground: hsl(213 31% 91%); - --primary: 210 40% 98%; - --primary-foreground: 222.2 47.4% 1.2%; + --primary: hsl(210 40% 98%); + --primary-foreground: hsl(222.2 47.4% 1.2%); - --secondary: 222.2 47.4% 11.2%; - --secondary-foreground: 210 40% 98%; + --secondary: hsl(222.2 47.4% 11.2%); + --secondary-foreground: hsl(210 40% 98%); - --destructive: 0 63% 31%; - --destructive-foreground: 210 40% 98%; + --destructive: hsl(0 63% 31%); + --destructive-foreground: hsl(210 40% 98%); - --ring: 216 34% 17%; + --ring: hsl(216 34% 17%); - --radius: 0.5rem; - } + --radius: 0.5rem; +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + + --color-border: var(--border); + --color-input: var(--input); + + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + + --color-ring: var(--ring); } @layer base { * { - @apply border-border; + @apply border-[var(--color-border)]; } body { - @apply bg-background text-foreground; - font-feature-settings: 'rlig' 1, 'calt' 1; + @apply bg-[var(--color-background)] text-[var(--color-foreground)]; } } diff --git a/apps/docs/src/remix-hook-form/0.1 Remix Stub.mdx b/apps/docs/src/remix-hook-form/0.1 React Router Stub.mdx similarity index 50% rename from apps/docs/src/remix-hook-form/0.1 Remix Stub.mdx rename to apps/docs/src/remix-hook-form/0.1 React Router Stub.mdx index 7fdc26ca..bd2b4d3e 100644 --- a/apps/docs/src/remix-hook-form/0.1 Remix Stub.mdx +++ b/apps/docs/src/remix-hook-form/0.1 React Router Stub.mdx @@ -1,18 +1,20 @@ -# Remix Stub Decorator for Storybook +# React Router Stub Decorator for Storybook -The Remix Stub Decorator provides a mock Remix environment for testing and previewing components in Storybook that depend on Remix's routing and data loading features. +The React Router Stub Decorator provides a mock React Router environment for testing and previewing components in Storybook that depend on React Router's routing and data loading features. ## Basic Usage The simplest way to use the decorator is to wrap your story with the default configuration: ```typescript -import { withRemixStubDecorator } from './remix-stub'; +import { withReactRouterStubDecorator } from './react-router-stub'; export default { title: 'Components/MyComponent', component: MyComponent, - decorators: [withRemixStubDecorator()] + decorators: [withReactRouterStubDecorator({ + routes: [{ path: '/' }] + })] }; export const Default = { @@ -20,54 +22,21 @@ export const Default = { }; ``` -## Root Configuration - -The decorator supports root-level configuration for providers, shared data, and actions: - -```typescript -export const WithRootConfig = { - decorators: [ - withRemixStubDecorator({ - root: { - // Root layout with providers - Component: ({ children }) => ( - - {children} - - ), - // Root-level action handler - action: async ({ request }) => { - // Handle form submissions - return { success: true }; - }, - // Root-level loader - loader: () => ({ - user: { name: 'John Doe' } - }), - // Root-level meta tags - meta: () => [{ - title: 'My App' - }] - } - }) - ] -}; -``` - ## Route Configuration -Configure additional routes alongside root configuration: +Configure routes for your components: ```typescript export const WithRoutes = { decorators: [ - withRemixStubDecorator({ - root: { - Component: ({ children }) => ( - {children} - ) - }, + withReactRouterStubDecorator({ routes: [ + { + path: '/', + Component: ({ children }) => ( + {children} + ) + }, { path: '/form', action: async ({ request }) => { @@ -85,46 +54,44 @@ export const WithRoutes = { ```typescript interface RemixStubOptions { - root?: { - Component?: ComponentType; - loader?: LoaderFunction; - action?: ActionFunction; - meta?: MetaFunction; - links?: LinksFunction; - }; - routes?: StubRouteObject[]; + routes: StubRouteObject[]; } -interface StubRouteObject { - path?: string; +export type StubRouteObject = StubIndexRouteObject | StubNonIndexRouteObject; + +interface StubNonIndexRouteObject + extends Omit { loader?: LoaderFunction; action?: ActionFunction; + children?: StubRouteObject[]; + meta?: MetaFunction; + links?: LinksFunction; Component?: ComponentType; +} + +interface StubIndexRouteObject + extends Omit { + loader?: LoaderFunction; + action?: ActionFunction; children?: StubRouteObject[]; meta?: MetaFunction; links?: LinksFunction; + Component?: ComponentType; } ``` ## Best Practices -1. **Root Configuration** - - Use root for global providers and layouts - - Handle common actions at the root level - - Share global data through root loader - - Set default meta and link tags - -2. **Route Organization** +1. **Route Configuration** - Define specific routes for form submissions - Keep route structure similar to production - Provide default actions for routes that need them -3. **Component Structure** - - Place shared UI elements in root Component +2. **Component Structure** - Use routes for page-specific components - Handle form submissions in route actions -4. **Form Handling** +3. **Form Handling** - Set appropriate action paths in forms - Handle validation in route actions - Return proper response structures @@ -136,13 +103,14 @@ interface StubRouteObject { ```typescript export const FormExample = { decorators: [ - withRemixStubDecorator({ - root: { - Component: ({ children }) => ( -
{children}
- ) - }, + withReactRouterStubDecorator({ routes: [ + { + path: '/', + Component: ({ children }) => ( +
{children}
+ ) + }, { path: '/submit', action: async ({ request }) => { @@ -161,16 +129,17 @@ export const FormExample = { ```typescript export const ProtectedExample = { decorators: [ - withRemixStubDecorator({ - root: { - loader: () => { - return { user: { isAuthenticated: true } }; - }, - Component: ({ children }) => ( - {children} - ) - }, + withReactRouterStubDecorator({ routes: [ + { + path: '/', + loader: () => { + return { user: { isAuthenticated: true } }; + }, + Component: ({ children }) => ( + {children} + ) + }, { path: '/dashboard', loader: () => { @@ -197,6 +166,6 @@ export const ProtectedExample = { - Check meta tag updates 3. **Component Integration** - - Test component interactions with Remix hooks + - Test component interactions with React Router hooks - Verify form state management - Check data loading states \ No newline at end of file diff --git a/apps/docs/src/remix-hook-form/checkbox-custom.stories.tsx b/apps/docs/src/remix-hook-form/checkbox-custom.stories.tsx index 558420cc..08da48f5 100644 --- a/apps/docs/src/remix-hook-form/checkbox-custom.stories.tsx +++ b/apps/docs/src/remix-hook-form/checkbox-custom.stories.tsx @@ -3,14 +3,14 @@ import { Checkbox } from '@lambdacurry/forms/remix-hook-form/checkbox'; import type { FormLabel, FormMessage } from '@lambdacurry/forms/remix-hook-form/form'; import { Button } from '@lambdacurry/forms/ui/button'; import * as CheckboxPrimitive from '@radix-ui/react-checkbox'; -import type { ActionFunctionArgs } from '@remix-run/node'; -import { useFetcher } from '@remix-run/react'; import type { Meta, StoryObj } from '@storybook/react'; import { expect, userEvent, within } from '@storybook/test'; -import * as React from 'react'; +import type * as React from 'react'; +import type { ActionFunctionArgs } from 'react-router'; +import { useFetcher } from 'react-router'; import { RemixFormProvider, getValidatedFormData, useRemixForm } from 'remix-hook-form'; import { z } from 'zod'; -import { withRemixStubDecorator } from '../lib/storybook/remix-stub'; +import { withReactRouterStubDecorator } from '../lib/storybook/react-router-stub'; const formSchema = z.object({ terms: z.boolean().refine((val) => val === true, 'You must accept the terms and conditions'), @@ -21,62 +21,44 @@ const formSchema = z.object({ type FormData = z.infer; // Custom checkbox component -const PurpleCheckbox = React.forwardRef< - HTMLButtonElement, - React.ComponentPropsWithoutRef ->((props, ref) => ( +const PurpleCheckbox = (props: React.ComponentPropsWithoutRef) => ( {props.children} -)); +); PurpleCheckbox.displayName = 'PurpleCheckbox'; // Custom indicator -const PurpleIndicator = React.forwardRef< - HTMLDivElement, - React.ComponentPropsWithoutRef ->((props, ref) => ( - +const PurpleIndicator = (props: React.ComponentPropsWithoutRef) => ( + -)); +); PurpleIndicator.displayName = 'PurpleIndicator'; // Custom form label component -const CustomLabel = React.forwardRef>( - ({ className, htmlFor, ...props }, ref) => ( -