diff --git a/examples/ts-react-search/.cta.json b/examples/ts-react-search/.cta.json new file mode 100644 index 00000000..23fab52f --- /dev/null +++ b/examples/ts-react-search/.cta.json @@ -0,0 +1,12 @@ +{ + "projectName": "ts-react-search", + "mode": "file-router", + "typescript": true, + "tailwind": true, + "packageManager": "pnpm", + "addOnOptions": {}, + "git": true, + "version": 1, + "framework": "react-cra", + "chosenAddOns": ["nitro", "start"] +} diff --git a/examples/ts-react-search/.gitignore b/examples/ts-react-search/.gitignore new file mode 100644 index 00000000..6221ecbd --- /dev/null +++ b/examples/ts-react-search/.gitignore @@ -0,0 +1,13 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local +count.txt +.env +.nitro +.tanstack +.wrangler +.output +.vinxi +todos.json diff --git a/examples/ts-react-search/.vscode/settings.json b/examples/ts-react-search/.vscode/settings.json new file mode 100644 index 00000000..00b5278e --- /dev/null +++ b/examples/ts-react-search/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "files.watcherExclude": { + "**/routeTree.gen.ts": true + }, + "search.exclude": { + "**/routeTree.gen.ts": true + }, + "files.readonlyInclude": { + "**/routeTree.gen.ts": true + } +} diff --git a/examples/ts-react-search/README.md b/examples/ts-react-search/README.md new file mode 100644 index 00000000..bf0db48d --- /dev/null +++ b/examples/ts-react-search/README.md @@ -0,0 +1,287 @@ +Welcome to your new TanStack app! + +# Getting Started + +To run this application: + +```bash +pnpm install +pnpm start +``` + +# Building For Production + +To build this application for production: + +```bash +pnpm build +``` + +## Testing + +This project uses [Vitest](https://vitest.dev/) for testing. You can run the tests with: + +```bash +pnpm test +``` + +## Styling + +This project uses [Tailwind CSS](https://tailwindcss.com/) for styling. + +## Routing + +This project uses [TanStack Router](https://tanstack.com/router). The initial setup is a file based router. Which means that the routes are managed as files in `src/routes`. + +### Adding A Route + +To add a new route to your application just add another a new file in the `./src/routes` directory. + +TanStack will automatically generate the content of the route file for you. + +Now that you have two routes you can use a `Link` component to navigate between them. + +### Adding Links + +To use SPA (Single Page Application) navigation you will need to import the `Link` component from `@tanstack/react-router`. + +```tsx +import { Link } from '@tanstack/react-router' +``` + +Then anywhere in your JSX you can use it like so: + +```tsx +About +``` + +This will create a link that will navigate to the `/about` route. + +More information on the `Link` component can be found in the [Link documentation](https://tanstack.com/router/v1/docs/framework/react/api/router/linkComponent). + +### Using A Layout + +In the File Based Routing setup the layout is located in `src/routes/__root.tsx`. Anything you add to the root route will appear in all the routes. The route content will appear in the JSX where you use the `` component. + +Here is an example layout that includes a header: + +```tsx +import { Outlet, createRootRoute } from '@tanstack/react-router' +import { TanStackRouterDevtools } from '@tanstack/react-router-devtools' + +import { Link } from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: () => ( + <> +
+ +
+ + + + ), +}) +``` + +The `` component is not required so you can remove it if you don't want it in your layout. + +More information on layouts can be found in the [Layouts documentation](https://tanstack.com/router/latest/docs/framework/react/guide/routing-concepts#layouts). + +## Data Fetching + +There are multiple ways to fetch data in your application. You can use TanStack Query to fetch data from a server. But you can also use the `loader` functionality built into TanStack Router to load the data for a route before it's rendered. + +For example: + +```tsx +const peopleRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/people', + loader: async () => { + const response = await fetch('https://swapi.dev/api/people') + return response.json() as Promise<{ + results: { + name: string + }[] + }> + }, + component: () => { + const data = peopleRoute.useLoaderData() + return ( +
    + {data.results.map((person) => ( +
  • {person.name}
  • + ))} +
+ ) + }, +}) +``` + +Loaders simplify your data fetching logic dramatically. Check out more information in the [Loader documentation](https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#loader-parameters). + +### React-Query + +React-Query is an excellent addition or alternative to route loading and integrating it into you application is a breeze. + +First add your dependencies: + +```bash +pnpm add @tanstack/react-query @tanstack/react-query-devtools +``` + +Next we'll need to create a query client and provider. We recommend putting those in `main.tsx`. + +```tsx +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' + +// ... + +const queryClient = new QueryClient() + +// ... + +if (!rootElement.innerHTML) { + const root = ReactDOM.createRoot(rootElement) + + root.render( + + + , + ) +} +``` + +You can also add TanStack Query Devtools to the root route (optional). + +```tsx +import { ReactQueryDevtools } from '@tanstack/react-query-devtools' + +const rootRoute = createRootRoute({ + component: () => ( + <> + + + + + ), +}) +``` + +Now you can use `useQuery` to fetch your data. + +```tsx +import { useQuery } from '@tanstack/react-query' + +import './App.css' + +function App() { + const { data } = useQuery({ + queryKey: ['people'], + queryFn: () => + fetch('https://swapi.dev/api/people') + .then((res) => res.json()) + .then((data) => data.results as { name: string }[]), + initialData: [], + }) + + return ( +
+
    + {data.map((person) => ( +
  • {person.name}
  • + ))} +
+
+ ) +} + +export default App +``` + +You can find out everything you need to know on how to use React-Query in the [React-Query documentation](https://tanstack.com/query/latest/docs/framework/react/overview). + +## State Management + +Another common requirement for React applications is state management. There are many options for state management in React. TanStack Store provides a great starting point for your project. + +First you need to add TanStack Store as a dependency: + +```bash +pnpm add @tanstack/store +``` + +Now let's create a simple counter in the `src/App.tsx` file as a demonstration. + +```tsx +import { useStore } from '@tanstack/react-store' +import { Store } from '@tanstack/store' +import './App.css' + +const countStore = new Store(0) + +function App() { + const count = useStore(countStore) + return ( +
+ +
+ ) +} + +export default App +``` + +One of the many nice features of TanStack Store is the ability to derive state from other state. That derived state will update when the base state updates. + +Let's check this out by doubling the count using derived state. + +```tsx +import { useStore } from '@tanstack/react-store' +import { Store, Derived } from '@tanstack/store' +import './App.css' + +const countStore = new Store(0) + +const doubledStore = new Derived({ + fn: () => countStore.state * 2, + deps: [countStore], +}) +doubledStore.mount() + +function App() { + const count = useStore(countStore) + const doubledCount = useStore(doubledStore) + + return ( +
+ +
Doubled - {doubledCount}
+
+ ) +} + +export default App +``` + +We use the `Derived` class to create a new store that is derived from another store. The `Derived` class has a `mount` method that will start the derived store updating. + +Once we've created the derived store we can use it in the `App` component just like we would any other store using the `useStore` hook. + +You can find out everything you need to know on how to use TanStack Store in the [TanStack Store documentation](https://tanstack.com/store/latest). + +# Demo files + +Files prefixed with `demo` can be safely deleted. They are there to provide a starting point for you to play around with the features you've installed. + +# Learn More + +You can learn more about all of the offerings from TanStack in the [TanStack documentation](https://tanstack.com). diff --git a/examples/ts-react-search/package.json b/examples/ts-react-search/package.json new file mode 100644 index 00000000..84b325c2 --- /dev/null +++ b/examples/ts-react-search/package.json @@ -0,0 +1,55 @@ +{ + "name": "ts-react-search", + "private": true, + "type": "module", + "scripts": { + "dev": "vite dev --port 3000", + "build": "vite build", + "serve": "vite preview", + "test": "exit 0" + }, + "dependencies": { + "@radix-ui/react-slot": "^1.2.4", + "@tailwindcss/vite": "^4.1.17", + "@tanstack/ai": "workspace:*", + "@tanstack/ai-openai": "workspace:*", + "@tanstack/ai-react": "workspace:*", + "@tanstack/query-db-collection": "^1.0.6", + "@tanstack/react-db": "^0.1.55", + "@tanstack/react-devtools": "^0.8.2", + "@tanstack/react-query": "^5.90.12", + "@tanstack/react-router": "^1.139.7", + "@tanstack/react-router-devtools": "^1.139.7", + "@tanstack/react-router-ssr-query": "^1.139.7", + "@tanstack/react-start": "^1.139.8", + "@tanstack/router-plugin": "^1.139.7", + "@tanstack/zod-adapter": "^1.140.1", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.555.0", + "nitro": "latest", + "radix-ui": "^1.4.3", + "react": "^19.2.0", + "react-day-picker": "^9.12.0", + "react-dom": "^19.2.0", + "tailwind-merge": "^3.4.0", + "tailwindcss": "^4.1.17", + "tw-animate-css": "^1.4.0", + "vite-tsconfig-paths": "^5.1.4", + "zod": "^4.1.13" + }, + "devDependencies": { + "@tanstack/devtools-vite": "^0.3.11", + "@testing-library/dom": "^10.4.1", + "@testing-library/react": "^16.3.0", + "@types/node": "^24.10.1", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "jsdom": "^27.2.0", + "typescript": "5.9.3", + "vite": "^7.2.4", + "vitest": "^4.0.14", + "web-vitals": "^5.1.0" + } +} diff --git a/examples/ts-react-search/public/favicon.ico b/examples/ts-react-search/public/favicon.ico new file mode 100644 index 00000000..e69de29b diff --git a/examples/ts-react-search/public/logo192.png b/examples/ts-react-search/public/logo192.png new file mode 100644 index 00000000..e69de29b diff --git a/examples/ts-react-search/public/logo512.png b/examples/ts-react-search/public/logo512.png new file mode 100644 index 00000000..e69de29b diff --git a/examples/ts-react-search/public/manifest.json b/examples/ts-react-search/public/manifest.json new file mode 100644 index 00000000..078ef501 --- /dev/null +++ b/examples/ts-react-search/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "TanStack App", + "name": "Create TanStack App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/examples/ts-react-search/public/robots.txt b/examples/ts-react-search/public/robots.txt new file mode 100644 index 00000000..e9e57dc4 --- /dev/null +++ b/examples/ts-react-search/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/examples/ts-react-search/public/tanstack-circle-logo.png b/examples/ts-react-search/public/tanstack-circle-logo.png new file mode 100644 index 00000000..e69de29b diff --git a/examples/ts-react-search/public/tanstack-word-logo-white.svg b/examples/ts-react-search/public/tanstack-word-logo-white.svg new file mode 100644 index 00000000..b6ec5086 --- /dev/null +++ b/examples/ts-react-search/public/tanstack-word-logo-white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/ts-react-search/src/components/FilterSelect.tsx b/examples/ts-react-search/src/components/FilterSelect.tsx new file mode 100644 index 00000000..dd256daf --- /dev/null +++ b/examples/ts-react-search/src/components/FilterSelect.tsx @@ -0,0 +1,46 @@ +import { Label } from '@/components/ui/label' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { ALL_OPTION } from '@/constants' + +type FilterSelectProps = { + id: string + label: string + value: string + onChange: (value: string) => void + options: Array<[string, string]> +} + +function FilterSelect({ + id, + label, + value, + onChange, + options, +}: FilterSelectProps) { + return ( +
+ + +
+ ) +} + +export default FilterSelect diff --git a/examples/ts-react-search/src/components/HeroSection/Brand.tsx b/examples/ts-react-search/src/components/HeroSection/Brand.tsx new file mode 100644 index 00000000..9ec17543 --- /dev/null +++ b/examples/ts-react-search/src/components/HeroSection/Brand.tsx @@ -0,0 +1,15 @@ +function Brand() { + return ( +

+ TanStack + + AI + + DEMO + + +

+ ) +} + +export default Brand diff --git a/examples/ts-react-search/src/components/HeroSection/HeroSection.tsx b/examples/ts-react-search/src/components/HeroSection/HeroSection.tsx new file mode 100644 index 00000000..1f30b22c --- /dev/null +++ b/examples/ts-react-search/src/components/HeroSection/HeroSection.tsx @@ -0,0 +1,17 @@ +import Brand from './Brand' +import Search from './Search' +import ProjectDescription from './ProjectDescription' +import Navigation from './Navigation' + +function HeroSection() { + return ( +
+ + + + +
+ ) +} + +export default HeroSection diff --git a/examples/ts-react-search/src/components/HeroSection/Navigation.tsx b/examples/ts-react-search/src/components/HeroSection/Navigation.tsx new file mode 100644 index 00000000..1de1e505 --- /dev/null +++ b/examples/ts-react-search/src/components/HeroSection/Navigation.tsx @@ -0,0 +1,32 @@ +import { Link } from '@tanstack/react-router' + +const NAVIGATION = [ + { name: 'Home', to: '/' }, + { name: 'Orders', to: '/orders' }, + { name: 'Disputes', to: '/disputes' }, + { name: 'Settlements', to: '/settlements' }, +] + +function Navigation() { + return ( + + ) +} + +export default Navigation diff --git a/examples/ts-react-search/src/components/HeroSection/ProjectDescription.tsx b/examples/ts-react-search/src/components/HeroSection/ProjectDescription.tsx new file mode 100644 index 00000000..02ac5b4e --- /dev/null +++ b/examples/ts-react-search/src/components/HeroSection/ProjectDescription.tsx @@ -0,0 +1,15 @@ +function ProjectDescription() { + return ( +
+

+ Natural Language Search +

+

+ Search your data using everyday language. Find orders, disputes, and + settlements. +

+
+ ) +} + +export default ProjectDescription diff --git a/examples/ts-react-search/src/components/HeroSection/Search/QuickPrompts.tsx b/examples/ts-react-search/src/components/HeroSection/Search/QuickPrompts.tsx new file mode 100644 index 00000000..7de0bbb4 --- /dev/null +++ b/examples/ts-react-search/src/components/HeroSection/Search/QuickPrompts.tsx @@ -0,0 +1,47 @@ +'use client' + +const PROMPTS = [ + { + locale: 'EN', + prompt: 'Show captured orders created after February 1, 2025', + }, + { locale: 'JA', prompt: '失われた紛争を教えて' }, + { + locale: 'SV', + prompt: + 'Visa mig USD-avstämningar som slutfördes mellan 01-01-2025 och 25-05-2025', + }, +] + +type QuickPromptsProps = { + onClick: (value: string) => void +} + +function QuickPrompts({ onClick }: QuickPromptsProps) { + function makePromptClickHandler(value: string) { + return () => onClick(value) + } + + return ( +
+

+ Quick prompts: +

+
    + {PROMPTS.map(({ prompt, locale }) => ( +
  • + +
  • + ))} +
+
+ ) +} + +export default QuickPrompts diff --git a/examples/ts-react-search/src/components/HeroSection/Search/Search.tsx b/examples/ts-react-search/src/components/HeroSection/Search/Search.tsx new file mode 100644 index 00000000..52b48333 --- /dev/null +++ b/examples/ts-react-search/src/components/HeroSection/Search/Search.tsx @@ -0,0 +1,55 @@ +'use client' + +import { useState } from 'react' +import { fetchServerSentEvents, useChat } from '@tanstack/ai-react' +import { useNavigate } from '@tanstack/react-router' +import QuickPrompts from './QuickPrompts' +import SearchForm from './SearchForm' +import type { FormEvent } from 'react' + +function Search() { + const navigate = useNavigate() + const [value, setValue] = useState('') + + const { sendMessage, error, isLoading } = useChat({ + connection: fetchServerSentEvents('/api/search'), + onFinish(message) { + if (message.role === 'assistant' && message.parts[0].type === 'text') { + const result = message.parts[0].content + const { name, parameters } = JSON.parse(result) || {} + + if (name && parameters) { + navigate({ to: `/${name}`, search: parameters }) + } + } + }, + }) + + function handleSubmit(event: FormEvent) { + event.preventDefault() + + if (value.trim() && !isLoading) { + sendMessage(value) + setValue('') + } + } + + return ( +
+ + {error && ( +

+ Error: {error.message} +

+ )} + +
+ ) +} + +export default Search diff --git a/examples/ts-react-search/src/components/HeroSection/Search/SearchForm.tsx b/examples/ts-react-search/src/components/HeroSection/Search/SearchForm.tsx new file mode 100644 index 00000000..bbb5fa9a --- /dev/null +++ b/examples/ts-react-search/src/components/HeroSection/Search/SearchForm.tsx @@ -0,0 +1,85 @@ +'use client' + +import { + ArrowRightIcon, + LoaderCircleIcon, + MicIcon, + SearchIcon, +} from 'lucide-react' +import { useEffect } from 'react' +import type { FormEvent } from 'react' +import { useSpeechRecognition } from '@/hooks/useSpeechRecognition' + +const PLACEHOLDER = 'e.g. Show all the expired orders created this year' + +type SearchFormProps = { + value: string + onChange: (value: string) => void + onSubmit: (event: FormEvent) => void + isLoading: boolean +} + +function SearchForm({ onChange, onSubmit, value, isLoading }: SearchFormProps) { + const { listening, transcript, startListening, stopListening } = + useSpeechRecognition() + + useEffect(() => { + if (transcript) { + onChange(transcript) + } + }, [transcript]) + + const hasValue = Boolean(value.trim().length) + + function handleQueryChange(event: FormEvent) { + onChange(event.currentTarget.value) + } + + function handleVoiceOverClick() { + if (listening) { + stopListening() + } else { + startListening() + } + } + + return ( +
+
+ +
+ +
+ + +
+
+ ) +} + +export default SearchForm diff --git a/examples/ts-react-search/src/components/HeroSection/Search/index.ts b/examples/ts-react-search/src/components/HeroSection/Search/index.ts new file mode 100644 index 00000000..78c5e486 --- /dev/null +++ b/examples/ts-react-search/src/components/HeroSection/Search/index.ts @@ -0,0 +1 @@ +export { default } from './Search' diff --git a/examples/ts-react-search/src/components/HeroSection/index.ts b/examples/ts-react-search/src/components/HeroSection/index.ts new file mode 100644 index 00000000..22de459e --- /dev/null +++ b/examples/ts-react-search/src/components/HeroSection/index.ts @@ -0,0 +1 @@ +export { default } from './HeroSection' diff --git a/examples/ts-react-search/src/components/Spinner.tsx b/examples/ts-react-search/src/components/Spinner.tsx new file mode 100644 index 00000000..80684bec --- /dev/null +++ b/examples/ts-react-search/src/components/Spinner.tsx @@ -0,0 +1,7 @@ +import { LoaderCircleIcon } from 'lucide-react' + +function Spinner() { + return +} + +export default Spinner diff --git a/examples/ts-react-search/src/components/TableSummary.tsx b/examples/ts-react-search/src/components/TableSummary.tsx new file mode 100644 index 00000000..ea35c700 --- /dev/null +++ b/examples/ts-react-search/src/components/TableSummary.tsx @@ -0,0 +1,15 @@ +type TableSummaryProps = { + totalCount: number + resultCount: number +} + +function TableSummary({ totalCount, resultCount }: TableSummaryProps) { + const summary = + resultCount === totalCount + ? `Displaying ${resultCount} records` + : `Displaying ${resultCount} of ${totalCount} records` + + return

{summary}

+} + +export default TableSummary diff --git a/examples/ts-react-search/src/components/ui/README.md b/examples/ts-react-search/src/components/ui/README.md new file mode 100644 index 00000000..2b56cde5 --- /dev/null +++ b/examples/ts-react-search/src/components/ui/README.md @@ -0,0 +1,7 @@ +# UI components + +This folder contains **shadcn/ui** and related components installed with: + +```bash +npx shadcn@latest add +``` diff --git a/examples/ts-react-search/src/components/ui/button.tsx b/examples/ts-react-search/src/components/ui/button.tsx new file mode 100644 index 00000000..010a338b --- /dev/null +++ b/examples/ts-react-search/src/components/ui/button.tsx @@ -0,0 +1,61 @@ +import * as React from 'react' +import { Slot } from '@radix-ui/react-slot' +import { cva } from 'class-variance-authority' +import type { VariantProps } from 'class-variance-authority' + +import cn from '@/utils/cn' + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: 'bg-primary text-primary-foreground hover:bg-primary/90', + destructive: + 'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', + outline: + 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50', + secondary: + 'bg-secondary text-secondary-foreground hover:bg-secondary/80', + ghost: + 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50', + link: 'text-primary underline-offset-4 hover:underline', + }, + size: { + default: 'h-11 px-4 py-2 has-[>svg]:px-3', + sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5', + lg: 'h-10 rounded-md px-6 has-[>svg]:px-4', + icon: 'size-9', + 'icon-sm': 'size-8', + 'icon-lg': 'size-10', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + }, +) + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<'button'> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot : 'button' + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/examples/ts-react-search/src/components/ui/calendar.tsx b/examples/ts-react-search/src/components/ui/calendar.tsx new file mode 100644 index 00000000..001e250d --- /dev/null +++ b/examples/ts-react-search/src/components/ui/calendar.tsx @@ -0,0 +1,92 @@ +'use client' + +import * as React from 'react' +import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react' +import { DayPicker } from 'react-day-picker' + +import cn from '@/utils/cn' +import { buttonVariants } from '@/components/ui/button' + +function Calendar({ + className, + classNames, + showOutsideDays = true, + components: userComponents, + ...props +}: React.ComponentProps) { + const defaultClassNames = { + months: 'relative flex flex-col sm:flex-row gap-4', + month: 'w-full', + month_caption: + 'relative mx-10 mb-1 flex h-9 items-center justify-center z-20', + caption_label: 'text-sm font-medium', + nav: 'absolute top-0 flex w-full justify-between z-10', + button_previous: cn( + buttonVariants({ variant: 'ghost' }), + 'size-9 p-0 text-muted-foreground/80 hover:text-foreground', + ), + button_next: cn( + buttonVariants({ variant: 'ghost' }), + 'size-9 p-0 text-muted-foreground/80 hover:text-foreground', + ), + weekday: 'size-9 p-0 text-xs font-medium text-muted-foreground/80', + day_button: + 'relative flex size-9 items-center justify-center whitespace-nowrap rounded-md p-0 text-foreground group-[[data-selected]:not(.range-middle)]:[transition-property:color,background-color,border-radius,box-shadow] group-[[data-selected]:not(.range-middle)]:duration-150 group-data-disabled:pointer-events-none focus-visible:z-10 hover:not-in-data-selected:bg-accent group-data-selected:bg-primary hover:not-in-data-selected:text-foreground group-data-selected:text-primary-foreground group-data-disabled:text-foreground/30 group-data-disabled:line-through group-data-outside:text-foreground/30 group-data-selected:group-data-outside:text-primary-foreground outline-none focus-visible:ring-ring/50 focus-visible:ring-[3px] group-[.range-start:not(.range-end)]:rounded-e-none group-[.range-end:not(.range-start)]:rounded-s-none group-[.range-middle]:rounded-none group-[.range-middle]:group-data-selected:bg-accent group-[.range-middle]:group-data-selected:text-foreground', + day: 'group size-9 px-0 py-px text-sm', + range_start: 'range-start', + range_end: 'range-end', + range_middle: 'range-middle', + today: + '*:after:pointer-events-none *:after:absolute *:after:bottom-1 *:after:start-1/2 *:after:z-10 *:after:size-[3px] *:after:-translate-x-1/2 *:after:rounded-full *:after:bg-primary [&[data-selected]:not(.range-middle)>*]:after:bg-background [&[data-disabled]>*]:after:bg-foreground/30 *:after:transition-colors', + outside: + 'text-muted-foreground data-selected:bg-accent/50 data-selected:text-muted-foreground', + hidden: 'invisible', + week_number: 'size-9 p-0 text-xs font-medium text-muted-foreground/80', + } + + const mergedClassNames: typeof defaultClassNames = Object.keys( + defaultClassNames, + ).reduce( + (acc, key) => ({ + ...acc, + [key]: classNames?.[key as keyof typeof classNames] + ? cn( + defaultClassNames[key as keyof typeof defaultClassNames], + classNames[key as keyof typeof classNames], + ) + : defaultClassNames[key as keyof typeof defaultClassNames], + }), + {} as typeof defaultClassNames, + ) + + const defaultComponents = { + Chevron: (props: { + className?: string + size?: number + disabled?: boolean + orientation?: 'left' | 'right' | 'up' | 'down' + }) => { + if (props.orientation === 'left') { + return