### What problem does this PR solve? feat: Add next login page with shadcn/ui #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.14.0
| @@ -19,7 +19,10 @@ export default defineConfig({ | |||
| history: { | |||
| type: 'browser', | |||
| }, | |||
| plugins: ['@react-dev-inspector/umi4-plugin'], | |||
| plugins: [ | |||
| '@react-dev-inspector/umi4-plugin', | |||
| '@umijs/plugins/dist/tailwindcss', | |||
| ], | |||
| jsMinifier: 'terser', | |||
| lessLoader: { | |||
| modifyVars: { | |||
| @@ -38,9 +41,11 @@ export default defineConfig({ | |||
| // pathRewrite: { '^/v1': '/v1' }, | |||
| }, | |||
| ], | |||
| chainWebpack(memo, args) { | |||
| memo.module.rule('markdown').test(/\.md$/).type('asset/source'); | |||
| return memo; | |||
| }, | |||
| tailwindcss: {}, | |||
| }); | |||
| @@ -21,15 +21,27 @@ | |||
| "@ant-design/pro-components": "^2.6.46", | |||
| "@ant-design/pro-layout": "^7.17.16", | |||
| "@antv/g6": "^5.0.10", | |||
| "@hookform/resolvers": "^3.9.1", | |||
| "@js-preview/excel": "^1.7.8", | |||
| "@monaco-editor/react": "^4.6.0", | |||
| "@radix-ui/react-checkbox": "^1.1.2", | |||
| "@radix-ui/react-dropdown-menu": "^2.1.2", | |||
| "@radix-ui/react-icons": "^1.3.1", | |||
| "@radix-ui/react-label": "^2.1.0", | |||
| "@radix-ui/react-select": "^2.1.2", | |||
| "@radix-ui/react-separator": "^1.1.0", | |||
| "@radix-ui/react-slot": "^1.1.0", | |||
| "@radix-ui/react-switch": "^1.1.1", | |||
| "@radix-ui/react-toast": "^1.2.2", | |||
| "@tanstack/react-query": "^5.40.0", | |||
| "@tanstack/react-query-devtools": "^5.51.5", | |||
| "@uiw/react-markdown-preview": "^5.1.3", | |||
| "ahooks": "^3.7.10", | |||
| "antd": "^5.12.7", | |||
| "axios": "^1.6.3", | |||
| "class-variance-authority": "^0.7.0", | |||
| "classnames": "^2.5.1", | |||
| "clsx": "^2.1.1", | |||
| "dayjs": "^1.11.10", | |||
| "dompurify": "^3.1.6", | |||
| "eventsource-parser": "^1.1.2", | |||
| @@ -37,15 +49,18 @@ | |||
| "i18next": "^23.7.16", | |||
| "i18next-browser-languagedetector": "^8.0.0", | |||
| "immer": "^10.1.1", | |||
| "input-otp": "^1.4.1", | |||
| "js-base64": "^3.7.5", | |||
| "jsencrypt": "^3.3.2", | |||
| "lodash": "^4.17.21", | |||
| "lucide-react": "^0.454.0", | |||
| "mammoth": "^1.7.2", | |||
| "openai-speech-stream-player": "^1.0.8", | |||
| "rc-tween-one": "^3.0.6", | |||
| "react-copy-to-clipboard": "^5.1.0", | |||
| "react-error-boundary": "^4.0.13", | |||
| "react-force-graph": "^1.44.4", | |||
| "react-hook-form": "^7.53.1", | |||
| "react-i18next": "^14.0.0", | |||
| "react-markdown": "^9.0.1", | |||
| "react-pdf-highlighter": "^6.1.0", | |||
| @@ -56,10 +71,13 @@ | |||
| "recharts": "^2.12.4", | |||
| "rehype-raw": "^7.0.0", | |||
| "remark-gfm": "^4.0.0", | |||
| "tailwind-merge": "^2.5.4", | |||
| "tailwindcss-animate": "^1.0.7", | |||
| "umi": "^4.0.90", | |||
| "umi-request": "^1.4.0", | |||
| "unist-util-visit-parents": "^6.0.1", | |||
| "uuid": "^9.0.1", | |||
| "zod": "^3.23.8", | |||
| "zustand": "^4.5.2" | |||
| }, | |||
| "devDependencies": { | |||
| @@ -90,6 +108,7 @@ | |||
| "prettier-plugin-packagejson": "^2.4.9", | |||
| "react-dev-inspector": "^2.0.1", | |||
| "remark-loader": "^6.0.0", | |||
| "tailwindcss": "^3", | |||
| "ts-node": "^10.9.2", | |||
| "typescript": "^5.0.3", | |||
| "umi-plugin-icons": "^0.1.1" | |||
| @@ -13,6 +13,7 @@ import weekOfYear from 'dayjs/plugin/weekOfYear'; | |||
| import weekYear from 'dayjs/plugin/weekYear'; | |||
| import weekday from 'dayjs/plugin/weekday'; | |||
| import React, { ReactNode, useEffect, useState } from 'react'; | |||
| import { ThemeProvider } from './components/theme-provider'; | |||
| import storage from './utils/authorization-util'; | |||
| dayjs.extend(customParseFormat); | |||
| @@ -53,17 +54,19 @@ const RootProvider = ({ children }: React.PropsWithChildren) => { | |||
| return ( | |||
| <QueryClientProvider client={queryClient}> | |||
| <ConfigProvider | |||
| theme={{ | |||
| token: { | |||
| fontFamily: 'Inter', | |||
| }, | |||
| }} | |||
| locale={locale} | |||
| > | |||
| <App> {children}</App> | |||
| </ConfigProvider> | |||
| <ReactQueryDevtools buttonPosition={'top-left'} /> | |||
| <ThemeProvider defaultTheme="light" storageKey="ragflow-ui-theme"> | |||
| <ConfigProvider | |||
| theme={{ | |||
| token: { | |||
| fontFamily: 'Inter', | |||
| }, | |||
| }} | |||
| locale={locale} | |||
| > | |||
| <App> {children}</App> | |||
| </ConfigProvider> | |||
| <ReactQueryDevtools buttonPosition={'top-left'} /> | |||
| </ThemeProvider> | |||
| </QueryClientProvider> | |||
| ); | |||
| }; | |||
| @@ -0,0 +1,191 @@ | |||
| 'use client'; | |||
| // Inspired by react-hot-toast library | |||
| import * as React from 'react'; | |||
| import type { ToastActionElement, ToastProps } from '@/components/ui/toast'; | |||
| const TOAST_LIMIT = 1; | |||
| const TOAST_REMOVE_DELAY = 1000000; | |||
| type ToasterToast = ToastProps & { | |||
| id: string; | |||
| title?: React.ReactNode; | |||
| description?: React.ReactNode; | |||
| action?: ToastActionElement; | |||
| }; | |||
| const actionTypes = { | |||
| ADD_TOAST: 'ADD_TOAST', | |||
| UPDATE_TOAST: 'UPDATE_TOAST', | |||
| DISMISS_TOAST: 'DISMISS_TOAST', | |||
| REMOVE_TOAST: 'REMOVE_TOAST', | |||
| } as const; | |||
| let count = 0; | |||
| function genId() { | |||
| count = (count + 1) % Number.MAX_SAFE_INTEGER; | |||
| return count.toString(); | |||
| } | |||
| type ActionType = typeof actionTypes; | |||
| type Action = | |||
| | { | |||
| type: ActionType['ADD_TOAST']; | |||
| toast: ToasterToast; | |||
| } | |||
| | { | |||
| type: ActionType['UPDATE_TOAST']; | |||
| toast: Partial<ToasterToast>; | |||
| } | |||
| | { | |||
| type: ActionType['DISMISS_TOAST']; | |||
| toastId?: ToasterToast['id']; | |||
| } | |||
| | { | |||
| type: ActionType['REMOVE_TOAST']; | |||
| toastId?: ToasterToast['id']; | |||
| }; | |||
| interface State { | |||
| toasts: ToasterToast[]; | |||
| } | |||
| const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>(); | |||
| const addToRemoveQueue = (toastId: string) => { | |||
| if (toastTimeouts.has(toastId)) { | |||
| return; | |||
| } | |||
| const timeout = setTimeout(() => { | |||
| toastTimeouts.delete(toastId); | |||
| dispatch({ | |||
| type: 'REMOVE_TOAST', | |||
| toastId: toastId, | |||
| }); | |||
| }, TOAST_REMOVE_DELAY); | |||
| toastTimeouts.set(toastId, timeout); | |||
| }; | |||
| export const reducer = (state: State, action: Action): State => { | |||
| switch (action.type) { | |||
| case 'ADD_TOAST': | |||
| return { | |||
| ...state, | |||
| toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), | |||
| }; | |||
| case 'UPDATE_TOAST': | |||
| return { | |||
| ...state, | |||
| toasts: state.toasts.map((t) => | |||
| t.id === action.toast.id ? { ...t, ...action.toast } : t, | |||
| ), | |||
| }; | |||
| case 'DISMISS_TOAST': { | |||
| const { toastId } = action; | |||
| // ! Side effects ! - This could be extracted into a dismissToast() action, | |||
| // but I'll keep it here for simplicity | |||
| if (toastId) { | |||
| addToRemoveQueue(toastId); | |||
| } else { | |||
| state.toasts.forEach((toast) => { | |||
| addToRemoveQueue(toast.id); | |||
| }); | |||
| } | |||
| return { | |||
| ...state, | |||
| toasts: state.toasts.map((t) => | |||
| t.id === toastId || toastId === undefined | |||
| ? { | |||
| ...t, | |||
| open: false, | |||
| } | |||
| : t, | |||
| ), | |||
| }; | |||
| } | |||
| case 'REMOVE_TOAST': | |||
| if (action.toastId === undefined) { | |||
| return { | |||
| ...state, | |||
| toasts: [], | |||
| }; | |||
| } | |||
| return { | |||
| ...state, | |||
| toasts: state.toasts.filter((t) => t.id !== action.toastId), | |||
| }; | |||
| } | |||
| }; | |||
| const listeners: Array<(state: State) => void> = []; | |||
| let memoryState: State = { toasts: [] }; | |||
| function dispatch(action: Action) { | |||
| memoryState = reducer(memoryState, action); | |||
| listeners.forEach((listener) => { | |||
| listener(memoryState); | |||
| }); | |||
| } | |||
| type Toast = Omit<ToasterToast, 'id'>; | |||
| function toast({ ...props }: Toast) { | |||
| const id = genId(); | |||
| const update = (props: ToasterToast) => | |||
| dispatch({ | |||
| type: 'UPDATE_TOAST', | |||
| toast: { ...props, id }, | |||
| }); | |||
| const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id }); | |||
| dispatch({ | |||
| type: 'ADD_TOAST', | |||
| toast: { | |||
| ...props, | |||
| id, | |||
| open: true, | |||
| onOpenChange: (open) => { | |||
| if (!open) dismiss(); | |||
| }, | |||
| }, | |||
| }); | |||
| return { | |||
| id: id, | |||
| dismiss, | |||
| update, | |||
| }; | |||
| } | |||
| function useToast() { | |||
| const [state, setState] = React.useState<State>(memoryState); | |||
| React.useEffect(() => { | |||
| listeners.push(setState); | |||
| return () => { | |||
| const index = listeners.indexOf(setState); | |||
| if (index > -1) { | |||
| listeners.splice(index, 1); | |||
| } | |||
| }; | |||
| }, [state]); | |||
| return { | |||
| ...state, | |||
| toast, | |||
| dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }), | |||
| }; | |||
| } | |||
| export { toast, useToast }; | |||
| @@ -0,0 +1,73 @@ | |||
| import { createContext, useContext, useEffect, useState } from 'react'; | |||
| type Theme = 'dark' | 'light' | 'system'; | |||
| type ThemeProviderProps = { | |||
| children: React.ReactNode; | |||
| defaultTheme?: Theme; | |||
| storageKey?: string; | |||
| }; | |||
| type ThemeProviderState = { | |||
| theme: Theme; | |||
| setTheme: (theme: Theme) => void; | |||
| }; | |||
| const initialState: ThemeProviderState = { | |||
| theme: 'system', | |||
| setTheme: () => null, | |||
| }; | |||
| const ThemeProviderContext = createContext<ThemeProviderState>(initialState); | |||
| export function ThemeProvider({ | |||
| children, | |||
| defaultTheme = 'system', | |||
| storageKey = 'vite-ui-theme', | |||
| ...props | |||
| }: ThemeProviderProps) { | |||
| const [theme, setTheme] = useState<Theme>( | |||
| () => (localStorage.getItem(storageKey) as Theme) || defaultTheme, | |||
| ); | |||
| useEffect(() => { | |||
| const root = window.document.documentElement; | |||
| root.classList.remove('light', 'dark'); | |||
| if (theme === 'system') { | |||
| const systemTheme = window.matchMedia('(prefers-color-scheme: dark)') | |||
| .matches | |||
| ? 'dark' | |||
| : 'light'; | |||
| root.classList.add(systemTheme); | |||
| return; | |||
| } | |||
| root.classList.add(theme); | |||
| }, [theme]); | |||
| const value = { | |||
| theme, | |||
| setTheme: (theme: Theme) => { | |||
| localStorage.setItem(storageKey, theme); | |||
| setTheme(theme); | |||
| }, | |||
| }; | |||
| return ( | |||
| <ThemeProviderContext.Provider {...props} value={value}> | |||
| {children} | |||
| </ThemeProviderContext.Provider> | |||
| ); | |||
| } | |||
| export const useTheme = () => { | |||
| const context = useContext(ThemeProviderContext); | |||
| if (context === undefined) | |||
| throw new Error('useTheme must be used within a ThemeProvider'); | |||
| return context; | |||
| }; | |||
| @@ -0,0 +1,56 @@ | |||
| import { Slot } from '@radix-ui/react-slot'; | |||
| import { cva, type VariantProps } from 'class-variance-authority'; | |||
| import * as React from 'react'; | |||
| import { cn } from '@/lib/utils'; | |||
| const buttonVariants = cva( | |||
| 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', | |||
| { | |||
| variants: { | |||
| variant: { | |||
| default: 'bg-primary text-primary-foreground hover:bg-primary/90', | |||
| destructive: | |||
| 'bg-destructive text-destructive-foreground hover:bg-destructive/90', | |||
| outline: | |||
| 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', | |||
| secondary: | |||
| 'bg-secondary text-secondary-foreground hover:bg-secondary/80', | |||
| ghost: 'hover:bg-accent hover:text-accent-foreground', | |||
| link: 'text-primary underline-offset-4 hover:underline', | |||
| }, | |||
| size: { | |||
| default: 'h-10 px-4 py-2', | |||
| sm: 'h-9 rounded-md px-3', | |||
| lg: 'h-11 rounded-md px-8', | |||
| icon: 'h-10 w-10', | |||
| }, | |||
| }, | |||
| defaultVariants: { | |||
| variant: 'default', | |||
| size: 'default', | |||
| }, | |||
| }, | |||
| ); | |||
| export interface ButtonProps | |||
| extends React.ButtonHTMLAttributes<HTMLButtonElement>, | |||
| VariantProps<typeof buttonVariants> { | |||
| asChild?: boolean; | |||
| } | |||
| const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( | |||
| ({ className, variant, size, asChild = false, ...props }, ref) => { | |||
| const Comp = asChild ? Slot : 'button'; | |||
| return ( | |||
| <Comp | |||
| className={cn(buttonVariants({ variant, size, className }))} | |||
| ref={ref} | |||
| {...props} | |||
| /> | |||
| ); | |||
| }, | |||
| ); | |||
| Button.displayName = 'Button'; | |||
| export { Button, buttonVariants }; | |||
| @@ -0,0 +1,86 @@ | |||
| import * as React from 'react'; | |||
| import { cn } from '@/lib/utils'; | |||
| const Card = React.forwardRef< | |||
| HTMLDivElement, | |||
| React.HTMLAttributes<HTMLDivElement> | |||
| >(({ className, ...props }, ref) => ( | |||
| <div | |||
| ref={ref} | |||
| className={cn( | |||
| 'rounded-lg border bg-card text-card-foreground shadow-sm', | |||
| className, | |||
| )} | |||
| {...props} | |||
| /> | |||
| )); | |||
| Card.displayName = 'Card'; | |||
| const CardHeader = React.forwardRef< | |||
| HTMLDivElement, | |||
| React.HTMLAttributes<HTMLDivElement> | |||
| >(({ className, ...props }, ref) => ( | |||
| <div | |||
| ref={ref} | |||
| className={cn('flex flex-col space-y-1.5 p-6', className)} | |||
| {...props} | |||
| /> | |||
| )); | |||
| CardHeader.displayName = 'CardHeader'; | |||
| const CardTitle = React.forwardRef< | |||
| HTMLDivElement, | |||
| React.HTMLAttributes<HTMLDivElement> | |||
| >(({ className, ...props }, ref) => ( | |||
| <div | |||
| ref={ref} | |||
| className={cn( | |||
| 'text-2xl font-semibold leading-none tracking-tight', | |||
| className, | |||
| )} | |||
| {...props} | |||
| /> | |||
| )); | |||
| CardTitle.displayName = 'CardTitle'; | |||
| const CardDescription = React.forwardRef< | |||
| HTMLDivElement, | |||
| React.HTMLAttributes<HTMLDivElement> | |||
| >(({ className, ...props }, ref) => ( | |||
| <div | |||
| ref={ref} | |||
| className={cn('text-sm text-muted-foreground', className)} | |||
| {...props} | |||
| /> | |||
| )); | |||
| CardDescription.displayName = 'CardDescription'; | |||
| const CardContent = React.forwardRef< | |||
| HTMLDivElement, | |||
| React.HTMLAttributes<HTMLDivElement> | |||
| >(({ className, ...props }, ref) => ( | |||
| <div ref={ref} className={cn('p-6 pt-0', className)} {...props} /> | |||
| )); | |||
| CardContent.displayName = 'CardContent'; | |||
| const CardFooter = React.forwardRef< | |||
| HTMLDivElement, | |||
| React.HTMLAttributes<HTMLDivElement> | |||
| >(({ className, ...props }, ref) => ( | |||
| <div | |||
| ref={ref} | |||
| className={cn('flex items-center p-6 pt-0', className)} | |||
| {...props} | |||
| /> | |||
| )); | |||
| CardFooter.displayName = 'CardFooter'; | |||
| export { | |||
| Card, | |||
| CardContent, | |||
| CardDescription, | |||
| CardFooter, | |||
| CardHeader, | |||
| CardTitle, | |||
| }; | |||
| @@ -0,0 +1,30 @@ | |||
| 'use client'; | |||
| import * as CheckboxPrimitive from '@radix-ui/react-checkbox'; | |||
| import { Check } from 'lucide-react'; | |||
| import * as React from 'react'; | |||
| import { cn } from '@/lib/utils'; | |||
| const Checkbox = React.forwardRef< | |||
| React.ElementRef<typeof CheckboxPrimitive.Root>, | |||
| React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> | |||
| >(({ className, ...props }, ref) => ( | |||
| <CheckboxPrimitive.Root | |||
| ref={ref} | |||
| className={cn( | |||
| 'peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground', | |||
| className, | |||
| )} | |||
| {...props} | |||
| > | |||
| <CheckboxPrimitive.Indicator | |||
| className={cn('flex items-center justify-center text-current')} | |||
| > | |||
| <Check className="h-4 w-4" /> | |||
| </CheckboxPrimitive.Indicator> | |||
| </CheckboxPrimitive.Root> | |||
| )); | |||
| Checkbox.displayName = CheckboxPrimitive.Root.displayName; | |||
| export { Checkbox }; | |||
| @@ -0,0 +1,200 @@ | |||
| 'use client'; | |||
| import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; | |||
| import { Check, ChevronRight, Circle } from 'lucide-react'; | |||
| import * as React from 'react'; | |||
| import { cn } from '@/lib/utils'; | |||
| const DropdownMenu = DropdownMenuPrimitive.Root; | |||
| const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; | |||
| const DropdownMenuGroup = DropdownMenuPrimitive.Group; | |||
| const DropdownMenuPortal = DropdownMenuPrimitive.Portal; | |||
| const DropdownMenuSub = DropdownMenuPrimitive.Sub; | |||
| const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; | |||
| const DropdownMenuSubTrigger = React.forwardRef< | |||
| React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>, | |||
| React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & { | |||
| inset?: boolean; | |||
| } | |||
| >(({ className, inset, children, ...props }, ref) => ( | |||
| <DropdownMenuPrimitive.SubTrigger | |||
| ref={ref} | |||
| className={cn( | |||
| 'flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', | |||
| inset && 'pl-8', | |||
| className, | |||
| )} | |||
| {...props} | |||
| > | |||
| {children} | |||
| <ChevronRight className="ml-auto" /> | |||
| </DropdownMenuPrimitive.SubTrigger> | |||
| )); | |||
| DropdownMenuSubTrigger.displayName = | |||
| DropdownMenuPrimitive.SubTrigger.displayName; | |||
| const DropdownMenuSubContent = React.forwardRef< | |||
| React.ElementRef<typeof DropdownMenuPrimitive.SubContent>, | |||
| React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent> | |||
| >(({ className, ...props }, ref) => ( | |||
| <DropdownMenuPrimitive.SubContent | |||
| ref={ref} | |||
| className={cn( | |||
| 'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', | |||
| className, | |||
| )} | |||
| {...props} | |||
| /> | |||
| )); | |||
| DropdownMenuSubContent.displayName = | |||
| DropdownMenuPrimitive.SubContent.displayName; | |||
| const DropdownMenuContent = React.forwardRef< | |||
| React.ElementRef<typeof DropdownMenuPrimitive.Content>, | |||
| React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content> | |||
| >(({ className, sideOffset = 4, ...props }, ref) => ( | |||
| <DropdownMenuPrimitive.Portal> | |||
| <DropdownMenuPrimitive.Content | |||
| ref={ref} | |||
| sideOffset={sideOffset} | |||
| className={cn( | |||
| 'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', | |||
| className, | |||
| )} | |||
| {...props} | |||
| /> | |||
| </DropdownMenuPrimitive.Portal> | |||
| )); | |||
| DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; | |||
| const DropdownMenuItem = React.forwardRef< | |||
| React.ElementRef<typeof DropdownMenuPrimitive.Item>, | |||
| React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & { | |||
| inset?: boolean; | |||
| } | |||
| >(({ className, inset, ...props }, ref) => ( | |||
| <DropdownMenuPrimitive.Item | |||
| ref={ref} | |||
| className={cn( | |||
| 'relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', | |||
| inset && 'pl-8', | |||
| className, | |||
| )} | |||
| {...props} | |||
| /> | |||
| )); | |||
| DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; | |||
| const DropdownMenuCheckboxItem = React.forwardRef< | |||
| React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>, | |||
| React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem> | |||
| >(({ className, children, checked, ...props }, ref) => ( | |||
| <DropdownMenuPrimitive.CheckboxItem | |||
| ref={ref} | |||
| className={cn( | |||
| 'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50', | |||
| className, | |||
| )} | |||
| checked={checked} | |||
| {...props} | |||
| > | |||
| <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> | |||
| <DropdownMenuPrimitive.ItemIndicator> | |||
| <Check className="h-4 w-4" /> | |||
| </DropdownMenuPrimitive.ItemIndicator> | |||
| </span> | |||
| {children} | |||
| </DropdownMenuPrimitive.CheckboxItem> | |||
| )); | |||
| DropdownMenuCheckboxItem.displayName = | |||
| DropdownMenuPrimitive.CheckboxItem.displayName; | |||
| const DropdownMenuRadioItem = React.forwardRef< | |||
| React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>, | |||
| React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem> | |||
| >(({ className, children, ...props }, ref) => ( | |||
| <DropdownMenuPrimitive.RadioItem | |||
| ref={ref} | |||
| className={cn( | |||
| 'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50', | |||
| className, | |||
| )} | |||
| {...props} | |||
| > | |||
| <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> | |||
| <DropdownMenuPrimitive.ItemIndicator> | |||
| <Circle className="h-2 w-2 fill-current" /> | |||
| </DropdownMenuPrimitive.ItemIndicator> | |||
| </span> | |||
| {children} | |||
| </DropdownMenuPrimitive.RadioItem> | |||
| )); | |||
| DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; | |||
| const DropdownMenuLabel = React.forwardRef< | |||
| React.ElementRef<typeof DropdownMenuPrimitive.Label>, | |||
| React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & { | |||
| inset?: boolean; | |||
| } | |||
| >(({ className, inset, ...props }, ref) => ( | |||
| <DropdownMenuPrimitive.Label | |||
| ref={ref} | |||
| className={cn( | |||
| 'px-2 py-1.5 text-sm font-semibold', | |||
| inset && 'pl-8', | |||
| className, | |||
| )} | |||
| {...props} | |||
| /> | |||
| )); | |||
| DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; | |||
| const DropdownMenuSeparator = React.forwardRef< | |||
| React.ElementRef<typeof DropdownMenuPrimitive.Separator>, | |||
| React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator> | |||
| >(({ className, ...props }, ref) => ( | |||
| <DropdownMenuPrimitive.Separator | |||
| ref={ref} | |||
| className={cn('-mx-1 my-1 h-px bg-muted', className)} | |||
| {...props} | |||
| /> | |||
| )); | |||
| DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; | |||
| const DropdownMenuShortcut = ({ | |||
| className, | |||
| ...props | |||
| }: React.HTMLAttributes<HTMLSpanElement>) => { | |||
| return ( | |||
| <span | |||
| className={cn('ml-auto text-xs tracking-widest opacity-60', className)} | |||
| {...props} | |||
| /> | |||
| ); | |||
| }; | |||
| DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'; | |||
| export { | |||
| DropdownMenu, | |||
| DropdownMenuCheckboxItem, | |||
| DropdownMenuContent, | |||
| DropdownMenuGroup, | |||
| DropdownMenuItem, | |||
| DropdownMenuLabel, | |||
| DropdownMenuPortal, | |||
| DropdownMenuRadioGroup, | |||
| DropdownMenuRadioItem, | |||
| DropdownMenuSeparator, | |||
| DropdownMenuShortcut, | |||
| DropdownMenuSub, | |||
| DropdownMenuSubContent, | |||
| DropdownMenuSubTrigger, | |||
| DropdownMenuTrigger, | |||
| }; | |||
| @@ -0,0 +1,179 @@ | |||
| 'use client'; | |||
| import * as LabelPrimitive from '@radix-ui/react-label'; | |||
| import { Slot } from '@radix-ui/react-slot'; | |||
| import * as React from 'react'; | |||
| import { | |||
| Controller, | |||
| ControllerProps, | |||
| FieldPath, | |||
| FieldValues, | |||
| FormProvider, | |||
| useFormContext, | |||
| } from 'react-hook-form'; | |||
| import { Label } from '@/components/ui/label'; | |||
| import { cn } from '@/lib/utils'; | |||
| const Form = FormProvider; | |||
| type FormItemContextValue = { | |||
| id: string; | |||
| }; | |||
| const FormItemContext = React.createContext<FormItemContextValue>( | |||
| {} as FormItemContextValue, | |||
| ); | |||
| type FormFieldContextValue< | |||
| TFieldValues extends FieldValues = FieldValues, | |||
| TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, | |||
| > = { | |||
| name: TName; | |||
| }; | |||
| const FormFieldContext = React.createContext<FormFieldContextValue>( | |||
| {} as FormFieldContextValue, | |||
| ); | |||
| const FormField = < | |||
| TFieldValues extends FieldValues = FieldValues, | |||
| TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, | |||
| >({ | |||
| ...props | |||
| }: ControllerProps<TFieldValues, TName>) => { | |||
| return ( | |||
| <FormFieldContext.Provider value={{ name: props.name }}> | |||
| <Controller {...props} /> | |||
| </FormFieldContext.Provider> | |||
| ); | |||
| }; | |||
| const useFormField = () => { | |||
| const fieldContext = React.useContext(FormFieldContext); | |||
| const itemContext = React.useContext(FormItemContext); | |||
| const { getFieldState, formState } = useFormContext(); | |||
| const fieldState = getFieldState(fieldContext.name, formState); | |||
| if (!fieldContext) { | |||
| throw new Error('useFormField should be used within <FormField>'); | |||
| } | |||
| const { id } = itemContext; | |||
| return { | |||
| id, | |||
| name: fieldContext.name, | |||
| formItemId: `${id}-form-item`, | |||
| formDescriptionId: `${id}-form-item-description`, | |||
| formMessageId: `${id}-form-item-message`, | |||
| ...fieldState, | |||
| }; | |||
| }; | |||
| const FormItem = React.forwardRef< | |||
| HTMLDivElement, | |||
| React.HTMLAttributes<HTMLDivElement> | |||
| >(({ className, ...props }, ref) => { | |||
| const id = React.useId(); | |||
| return ( | |||
| <FormItemContext.Provider value={{ id }}> | |||
| <div ref={ref} className={cn('space-y-2', className)} {...props} /> | |||
| </FormItemContext.Provider> | |||
| ); | |||
| }); | |||
| FormItem.displayName = 'FormItem'; | |||
| const FormLabel = React.forwardRef< | |||
| React.ElementRef<typeof LabelPrimitive.Root>, | |||
| React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> | |||
| >(({ className, ...props }, ref) => { | |||
| const { error, formItemId } = useFormField(); | |||
| return ( | |||
| <Label | |||
| ref={ref} | |||
| className={cn(error && 'text-destructive', className)} | |||
| htmlFor={formItemId} | |||
| {...props} | |||
| /> | |||
| ); | |||
| }); | |||
| FormLabel.displayName = 'FormLabel'; | |||
| const FormControl = React.forwardRef< | |||
| React.ElementRef<typeof Slot>, | |||
| React.ComponentPropsWithoutRef<typeof Slot> | |||
| >(({ ...props }, ref) => { | |||
| const { error, formItemId, formDescriptionId, formMessageId } = | |||
| useFormField(); | |||
| return ( | |||
| <Slot | |||
| ref={ref} | |||
| id={formItemId} | |||
| aria-describedby={ | |||
| !error | |||
| ? `${formDescriptionId}` | |||
| : `${formDescriptionId} ${formMessageId}` | |||
| } | |||
| aria-invalid={!!error} | |||
| {...props} | |||
| /> | |||
| ); | |||
| }); | |||
| FormControl.displayName = 'FormControl'; | |||
| const FormDescription = React.forwardRef< | |||
| HTMLParagraphElement, | |||
| React.HTMLAttributes<HTMLParagraphElement> | |||
| >(({ className, ...props }, ref) => { | |||
| const { formDescriptionId } = useFormField(); | |||
| return ( | |||
| <p | |||
| ref={ref} | |||
| id={formDescriptionId} | |||
| className={cn('text-sm text-muted-foreground', className)} | |||
| {...props} | |||
| /> | |||
| ); | |||
| }); | |||
| FormDescription.displayName = 'FormDescription'; | |||
| const FormMessage = React.forwardRef< | |||
| HTMLParagraphElement, | |||
| React.HTMLAttributes<HTMLParagraphElement> | |||
| >(({ className, children, ...props }, ref) => { | |||
| const { error, formMessageId } = useFormField(); | |||
| const body = error ? String(error?.message) : children; | |||
| if (!body) { | |||
| return null; | |||
| } | |||
| return ( | |||
| <p | |||
| ref={ref} | |||
| id={formMessageId} | |||
| className={cn('text-sm font-medium text-destructive', className)} | |||
| {...props} | |||
| > | |||
| {body} | |||
| </p> | |||
| ); | |||
| }); | |||
| FormMessage.displayName = 'FormMessage'; | |||
| export { | |||
| Form, | |||
| FormControl, | |||
| FormDescription, | |||
| FormField, | |||
| FormItem, | |||
| FormLabel, | |||
| FormMessage, | |||
| useFormField, | |||
| }; | |||
| @@ -0,0 +1,71 @@ | |||
| 'use client'; | |||
| import { OTPInput, OTPInputContext } from 'input-otp'; | |||
| import { Dot } from 'lucide-react'; | |||
| import * as React from 'react'; | |||
| import { cn } from '@/lib/utils'; | |||
| const InputOTP = React.forwardRef< | |||
| React.ElementRef<typeof OTPInput>, | |||
| React.ComponentPropsWithoutRef<typeof OTPInput> | |||
| >(({ className, containerClassName, ...props }, ref) => ( | |||
| <OTPInput | |||
| ref={ref} | |||
| containerClassName={cn( | |||
| 'flex items-center gap-2 has-[:disabled]:opacity-50', | |||
| containerClassName, | |||
| )} | |||
| className={cn('disabled:cursor-not-allowed', className)} | |||
| {...props} | |||
| /> | |||
| )); | |||
| InputOTP.displayName = 'InputOTP'; | |||
| const InputOTPGroup = React.forwardRef< | |||
| React.ElementRef<'div'>, | |||
| React.ComponentPropsWithoutRef<'div'> | |||
| >(({ className, ...props }, ref) => ( | |||
| <div ref={ref} className={cn('flex items-center', className)} {...props} /> | |||
| )); | |||
| InputOTPGroup.displayName = 'InputOTPGroup'; | |||
| const InputOTPSlot = React.forwardRef< | |||
| React.ElementRef<'div'>, | |||
| React.ComponentPropsWithoutRef<'div'> & { index: number } | |||
| >(({ index, className, ...props }, ref) => { | |||
| const inputOTPContext = React.useContext(OTPInputContext); | |||
| const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]; | |||
| return ( | |||
| <div | |||
| ref={ref} | |||
| className={cn( | |||
| 'relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md', | |||
| isActive && 'z-10 ring-2 ring-ring ring-offset-background', | |||
| className, | |||
| )} | |||
| {...props} | |||
| > | |||
| {char} | |||
| {hasFakeCaret && ( | |||
| <div className="pointer-events-none absolute inset-0 flex items-center justify-center"> | |||
| <div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" /> | |||
| </div> | |||
| )} | |||
| </div> | |||
| ); | |||
| }); | |||
| InputOTPSlot.displayName = 'InputOTPSlot'; | |||
| const InputOTPSeparator = React.forwardRef< | |||
| React.ElementRef<'div'>, | |||
| React.ComponentPropsWithoutRef<'div'> | |||
| >(({ ...props }, ref) => ( | |||
| <div ref={ref} role="separator" {...props}> | |||
| <Dot /> | |||
| </div> | |||
| )); | |||
| InputOTPSeparator.displayName = 'InputOTPSeparator'; | |||
| export { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot }; | |||
| @@ -0,0 +1,25 @@ | |||
| import * as React from 'react'; | |||
| import { cn } from '@/lib/utils'; | |||
| export interface InputProps | |||
| extends React.InputHTMLAttributes<HTMLInputElement> {} | |||
| const Input = React.forwardRef<HTMLInputElement, InputProps>( | |||
| ({ className, type, ...props }, ref) => { | |||
| return ( | |||
| <input | |||
| type={type} | |||
| className={cn( | |||
| 'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50', | |||
| className, | |||
| )} | |||
| ref={ref} | |||
| {...props} | |||
| /> | |||
| ); | |||
| }, | |||
| ); | |||
| Input.displayName = 'Input'; | |||
| export { Input }; | |||
| @@ -0,0 +1,26 @@ | |||
| 'use client'; | |||
| import * as LabelPrimitive from '@radix-ui/react-label'; | |||
| import { cva, type VariantProps } from 'class-variance-authority'; | |||
| import * as React from 'react'; | |||
| import { cn } from '@/lib/utils'; | |||
| const labelVariants = cva( | |||
| 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70', | |||
| ); | |||
| const Label = React.forwardRef< | |||
| React.ElementRef<typeof LabelPrimitive.Root>, | |||
| React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & | |||
| VariantProps<typeof labelVariants> | |||
| >(({ className, ...props }, ref) => ( | |||
| <LabelPrimitive.Root | |||
| ref={ref} | |||
| className={cn(labelVariants(), className)} | |||
| {...props} | |||
| /> | |||
| )); | |||
| Label.displayName = LabelPrimitive.Root.displayName; | |||
| export { Label }; | |||
| @@ -0,0 +1,160 @@ | |||
| 'use client'; | |||
| import * as SelectPrimitive from '@radix-ui/react-select'; | |||
| import { Check, ChevronDown, ChevronUp } from 'lucide-react'; | |||
| import * as React from 'react'; | |||
| import { cn } from '@/lib/utils'; | |||
| const Select = SelectPrimitive.Root; | |||
| const SelectGroup = SelectPrimitive.Group; | |||
| const SelectValue = SelectPrimitive.Value; | |||
| const SelectTrigger = React.forwardRef< | |||
| React.ElementRef<typeof SelectPrimitive.Trigger>, | |||
| React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> | |||
| >(({ className, children, ...props }, ref) => ( | |||
| <SelectPrimitive.Trigger | |||
| ref={ref} | |||
| className={cn( | |||
| 'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1', | |||
| className, | |||
| )} | |||
| {...props} | |||
| > | |||
| {children} | |||
| <SelectPrimitive.Icon asChild> | |||
| <ChevronDown className="h-4 w-4 opacity-50" /> | |||
| </SelectPrimitive.Icon> | |||
| </SelectPrimitive.Trigger> | |||
| )); | |||
| SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; | |||
| const SelectScrollUpButton = React.forwardRef< | |||
| React.ElementRef<typeof SelectPrimitive.ScrollUpButton>, | |||
| React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton> | |||
| >(({ className, ...props }, ref) => ( | |||
| <SelectPrimitive.ScrollUpButton | |||
| ref={ref} | |||
| className={cn( | |||
| 'flex cursor-default items-center justify-center py-1', | |||
| className, | |||
| )} | |||
| {...props} | |||
| > | |||
| <ChevronUp className="h-4 w-4" /> | |||
| </SelectPrimitive.ScrollUpButton> | |||
| )); | |||
| SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; | |||
| const SelectScrollDownButton = React.forwardRef< | |||
| React.ElementRef<typeof SelectPrimitive.ScrollDownButton>, | |||
| React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton> | |||
| >(({ className, ...props }, ref) => ( | |||
| <SelectPrimitive.ScrollDownButton | |||
| ref={ref} | |||
| className={cn( | |||
| 'flex cursor-default items-center justify-center py-1', | |||
| className, | |||
| )} | |||
| {...props} | |||
| > | |||
| <ChevronDown className="h-4 w-4" /> | |||
| </SelectPrimitive.ScrollDownButton> | |||
| )); | |||
| SelectScrollDownButton.displayName = | |||
| SelectPrimitive.ScrollDownButton.displayName; | |||
| const SelectContent = React.forwardRef< | |||
| React.ElementRef<typeof SelectPrimitive.Content>, | |||
| React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content> | |||
| >(({ className, children, position = 'popper', ...props }, ref) => ( | |||
| <SelectPrimitive.Portal> | |||
| <SelectPrimitive.Content | |||
| ref={ref} | |||
| className={cn( | |||
| 'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', | |||
| position === 'popper' && | |||
| 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1', | |||
| className, | |||
| )} | |||
| position={position} | |||
| {...props} | |||
| > | |||
| <SelectScrollUpButton /> | |||
| <SelectPrimitive.Viewport | |||
| className={cn( | |||
| 'p-1', | |||
| position === 'popper' && | |||
| 'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]', | |||
| )} | |||
| > | |||
| {children} | |||
| </SelectPrimitive.Viewport> | |||
| <SelectScrollDownButton /> | |||
| </SelectPrimitive.Content> | |||
| </SelectPrimitive.Portal> | |||
| )); | |||
| SelectContent.displayName = SelectPrimitive.Content.displayName; | |||
| const SelectLabel = React.forwardRef< | |||
| React.ElementRef<typeof SelectPrimitive.Label>, | |||
| React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label> | |||
| >(({ className, ...props }, ref) => ( | |||
| <SelectPrimitive.Label | |||
| ref={ref} | |||
| className={cn('py-1.5 pl-8 pr-2 text-sm font-semibold', className)} | |||
| {...props} | |||
| /> | |||
| )); | |||
| SelectLabel.displayName = SelectPrimitive.Label.displayName; | |||
| const SelectItem = React.forwardRef< | |||
| React.ElementRef<typeof SelectPrimitive.Item>, | |||
| React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> | |||
| >(({ className, children, ...props }, ref) => ( | |||
| <SelectPrimitive.Item | |||
| ref={ref} | |||
| className={cn( | |||
| 'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50', | |||
| className, | |||
| )} | |||
| {...props} | |||
| > | |||
| <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> | |||
| <SelectPrimitive.ItemIndicator> | |||
| <Check className="h-4 w-4" /> | |||
| </SelectPrimitive.ItemIndicator> | |||
| </span> | |||
| <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> | |||
| </SelectPrimitive.Item> | |||
| )); | |||
| SelectItem.displayName = SelectPrimitive.Item.displayName; | |||
| const SelectSeparator = React.forwardRef< | |||
| React.ElementRef<typeof SelectPrimitive.Separator>, | |||
| React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator> | |||
| >(({ className, ...props }, ref) => ( | |||
| <SelectPrimitive.Separator | |||
| ref={ref} | |||
| className={cn('-mx-1 my-1 h-px bg-muted', className)} | |||
| {...props} | |||
| /> | |||
| )); | |||
| SelectSeparator.displayName = SelectPrimitive.Separator.displayName; | |||
| export { | |||
| Select, | |||
| SelectContent, | |||
| SelectGroup, | |||
| SelectItem, | |||
| SelectLabel, | |||
| SelectScrollDownButton, | |||
| SelectScrollUpButton, | |||
| SelectSeparator, | |||
| SelectTrigger, | |||
| SelectValue, | |||
| }; | |||
| @@ -0,0 +1,31 @@ | |||
| 'use client'; | |||
| import * as SeparatorPrimitive from '@radix-ui/react-separator'; | |||
| import * as React from 'react'; | |||
| import { cn } from '@/lib/utils'; | |||
| const Separator = React.forwardRef< | |||
| React.ElementRef<typeof SeparatorPrimitive.Root>, | |||
| React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root> | |||
| >( | |||
| ( | |||
| { className, orientation = 'horizontal', decorative = true, ...props }, | |||
| ref, | |||
| ) => ( | |||
| <SeparatorPrimitive.Root | |||
| ref={ref} | |||
| decorative={decorative} | |||
| orientation={orientation} | |||
| className={cn( | |||
| 'shrink-0 bg-border', | |||
| orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]', | |||
| className, | |||
| )} | |||
| {...props} | |||
| /> | |||
| ), | |||
| ); | |||
| Separator.displayName = SeparatorPrimitive.Root.displayName; | |||
| export { Separator }; | |||
| @@ -0,0 +1,29 @@ | |||
| 'use client'; | |||
| import * as SwitchPrimitives from '@radix-ui/react-switch'; | |||
| import * as React from 'react'; | |||
| import { cn } from '@/lib/utils'; | |||
| const Switch = React.forwardRef< | |||
| React.ElementRef<typeof SwitchPrimitives.Root>, | |||
| React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root> | |||
| >(({ className, ...props }, ref) => ( | |||
| <SwitchPrimitives.Root | |||
| className={cn( | |||
| 'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input', | |||
| className, | |||
| )} | |||
| {...props} | |||
| ref={ref} | |||
| > | |||
| <SwitchPrimitives.Thumb | |||
| className={cn( | |||
| 'pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0', | |||
| )} | |||
| /> | |||
| </SwitchPrimitives.Root> | |||
| )); | |||
| Switch.displayName = SwitchPrimitives.Root.displayName; | |||
| export { Switch }; | |||
| @@ -0,0 +1,129 @@ | |||
| 'use client'; | |||
| import * as ToastPrimitives from '@radix-ui/react-toast'; | |||
| import { cva, type VariantProps } from 'class-variance-authority'; | |||
| import { X } from 'lucide-react'; | |||
| import * as React from 'react'; | |||
| import { cn } from '@/lib/utils'; | |||
| const ToastProvider = ToastPrimitives.Provider; | |||
| const ToastViewport = React.forwardRef< | |||
| React.ElementRef<typeof ToastPrimitives.Viewport>, | |||
| React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport> | |||
| >(({ className, ...props }, ref) => ( | |||
| <ToastPrimitives.Viewport | |||
| ref={ref} | |||
| className={cn( | |||
| 'fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]', | |||
| className, | |||
| )} | |||
| {...props} | |||
| /> | |||
| )); | |||
| ToastViewport.displayName = ToastPrimitives.Viewport.displayName; | |||
| const toastVariants = cva( | |||
| 'group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full', | |||
| { | |||
| variants: { | |||
| variant: { | |||
| default: 'border bg-background text-foreground', | |||
| destructive: | |||
| 'destructive group border-destructive bg-destructive text-destructive-foreground', | |||
| }, | |||
| }, | |||
| defaultVariants: { | |||
| variant: 'default', | |||
| }, | |||
| }, | |||
| ); | |||
| const Toast = React.forwardRef< | |||
| React.ElementRef<typeof ToastPrimitives.Root>, | |||
| React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> & | |||
| VariantProps<typeof toastVariants> | |||
| >(({ className, variant, ...props }, ref) => { | |||
| return ( | |||
| <ToastPrimitives.Root | |||
| ref={ref} | |||
| className={cn(toastVariants({ variant }), className)} | |||
| {...props} | |||
| /> | |||
| ); | |||
| }); | |||
| Toast.displayName = ToastPrimitives.Root.displayName; | |||
| const ToastAction = React.forwardRef< | |||
| React.ElementRef<typeof ToastPrimitives.Action>, | |||
| React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action> | |||
| >(({ className, ...props }, ref) => ( | |||
| <ToastPrimitives.Action | |||
| ref={ref} | |||
| className={cn( | |||
| 'inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive', | |||
| className, | |||
| )} | |||
| {...props} | |||
| /> | |||
| )); | |||
| ToastAction.displayName = ToastPrimitives.Action.displayName; | |||
| const ToastClose = React.forwardRef< | |||
| React.ElementRef<typeof ToastPrimitives.Close>, | |||
| React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close> | |||
| >(({ className, ...props }, ref) => ( | |||
| <ToastPrimitives.Close | |||
| ref={ref} | |||
| className={cn( | |||
| 'absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600', | |||
| className, | |||
| )} | |||
| toast-close="" | |||
| {...props} | |||
| > | |||
| <X className="h-4 w-4" /> | |||
| </ToastPrimitives.Close> | |||
| )); | |||
| ToastClose.displayName = ToastPrimitives.Close.displayName; | |||
| const ToastTitle = React.forwardRef< | |||
| React.ElementRef<typeof ToastPrimitives.Title>, | |||
| React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title> | |||
| >(({ className, ...props }, ref) => ( | |||
| <ToastPrimitives.Title | |||
| ref={ref} | |||
| className={cn('text-sm font-semibold', className)} | |||
| {...props} | |||
| /> | |||
| )); | |||
| ToastTitle.displayName = ToastPrimitives.Title.displayName; | |||
| const ToastDescription = React.forwardRef< | |||
| React.ElementRef<typeof ToastPrimitives.Description>, | |||
| React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description> | |||
| >(({ className, ...props }, ref) => ( | |||
| <ToastPrimitives.Description | |||
| ref={ref} | |||
| className={cn('text-sm opacity-90', className)} | |||
| {...props} | |||
| /> | |||
| )); | |||
| ToastDescription.displayName = ToastPrimitives.Description.displayName; | |||
| type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>; | |||
| type ToastActionElement = React.ReactElement<typeof ToastAction>; | |||
| export { | |||
| Toast, | |||
| ToastAction, | |||
| ToastClose, | |||
| ToastDescription, | |||
| ToastProvider, | |||
| ToastTitle, | |||
| ToastViewport, | |||
| type ToastActionElement, | |||
| type ToastProps, | |||
| }; | |||
| @@ -0,0 +1,35 @@ | |||
| 'use client'; | |||
| import { useToast } from '@/components/hooks/use-toast'; | |||
| import { | |||
| Toast, | |||
| ToastClose, | |||
| ToastDescription, | |||
| ToastProvider, | |||
| ToastTitle, | |||
| ToastViewport, | |||
| } from '@/components/ui/toast'; | |||
| export function Toaster() { | |||
| const { toasts } = useToast(); | |||
| return ( | |||
| <ToastProvider> | |||
| {toasts.map(function ({ id, title, description, action, ...props }: any) { | |||
| return ( | |||
| <Toast key={id} {...props}> | |||
| <div className="grid gap-1"> | |||
| {title && <ToastTitle>{title}</ToastTitle>} | |||
| {description && ( | |||
| <ToastDescription>{description}</ToastDescription> | |||
| )} | |||
| </div> | |||
| {action} | |||
| <ToastClose /> | |||
| </Toast> | |||
| ); | |||
| })} | |||
| <ToastViewport /> | |||
| </ToastProvider> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,6 @@ | |||
| import { clsx, type ClassValue } from 'clsx'; | |||
| import { twMerge } from 'tailwind-merge'; | |||
| export function cn(...inputs: ClassValue[]) { | |||
| return twMerge(clsx(inputs)); | |||
| } | |||
| @@ -0,0 +1,49 @@ | |||
| import { useTheme } from '@/components/theme-provider'; | |||
| import { Button } from '@/components/ui/button'; | |||
| import { | |||
| DropdownMenu, | |||
| DropdownMenuContent, | |||
| DropdownMenuItem, | |||
| DropdownMenuTrigger, | |||
| } from '@/components/ui/dropdown-menu'; | |||
| import { Moon, Sun } from 'lucide-react'; | |||
| export function ModeToggle() { | |||
| const { setTheme } = useTheme(); | |||
| return ( | |||
| <DropdownMenu> | |||
| <DropdownMenuTrigger asChild> | |||
| <Button variant="outline" size="icon"> | |||
| <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" /> | |||
| <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" /> | |||
| <span className="sr-only">Toggle theme</span> | |||
| </Button> | |||
| </DropdownMenuTrigger> | |||
| <DropdownMenuContent align="end"> | |||
| <DropdownMenuItem onClick={() => setTheme('light')}> | |||
| Light | |||
| </DropdownMenuItem> | |||
| <DropdownMenuItem onClick={() => setTheme('dark')}> | |||
| Dark | |||
| </DropdownMenuItem> | |||
| <DropdownMenuItem onClick={() => setTheme('system')}> | |||
| System | |||
| </DropdownMenuItem> | |||
| </DropdownMenuContent> | |||
| </DropdownMenu> | |||
| ); | |||
| } | |||
| const Demo = () => { | |||
| return ( | |||
| <div> | |||
| <div> | |||
| <ModeToggle></ModeToggle> | |||
| </div> | |||
| <Button>Destructive</Button> | |||
| </div> | |||
| ); | |||
| }; | |||
| export default Demo; | |||
| @@ -0,0 +1,246 @@ | |||
| 'use client'; | |||
| import { toast } from '@/components/hooks/use-toast'; | |||
| import { Button } from '@/components/ui/button'; | |||
| import { Checkbox } from '@/components/ui/checkbox'; | |||
| import { | |||
| Form, | |||
| FormControl, | |||
| FormField, | |||
| FormItem, | |||
| FormLabel, | |||
| FormMessage, | |||
| } from '@/components/ui/form'; | |||
| import { Input } from '@/components/ui/input'; | |||
| import { | |||
| InputOTP, | |||
| InputOTPGroup, | |||
| InputOTPSlot, | |||
| } from '@/components/ui/input-otp'; | |||
| import { useTranslate } from '@/hooks/common-hooks'; | |||
| import { zodResolver } from '@hookform/resolvers/zod'; | |||
| import { useForm } from 'react-hook-form'; | |||
| import { z } from 'zod'; | |||
| export function SignUpForm() { | |||
| const { t } = useTranslate('login'); | |||
| const FormSchema = z.object({ | |||
| email: z.string().email({ | |||
| message: t('emailPlaceholder'), | |||
| }), | |||
| nickname: z.string({ required_error: t('nicknamePlaceholder') }), | |||
| password: z.string({ required_error: t('passwordPlaceholder') }), | |||
| }); | |||
| const form = useForm<z.infer<typeof FormSchema>>({ | |||
| resolver: zodResolver(FormSchema), | |||
| defaultValues: { | |||
| email: '', | |||
| }, | |||
| }); | |||
| function onSubmit(data: z.infer<typeof FormSchema>) { | |||
| console.log('🚀 ~ onSubmit ~ data:', data); | |||
| toast({ | |||
| title: 'You submitted the following values:', | |||
| description: ( | |||
| <pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4"> | |||
| <code className="text-white">{JSON.stringify(data, null, 2)}</code> | |||
| </pre> | |||
| ), | |||
| }); | |||
| } | |||
| return ( | |||
| <Form {...form}> | |||
| <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6"> | |||
| <FormField | |||
| control={form.control} | |||
| name="email" | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormLabel>{t('emailLabel')}</FormLabel> | |||
| <FormControl> | |||
| <Input placeholder={t('emailPlaceholder')} {...field} /> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| <FormField | |||
| control={form.control} | |||
| name="nickname" | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormLabel>{t('nicknameLabel')}</FormLabel> | |||
| <FormControl> | |||
| <Input placeholder={t('nicknamePlaceholder')} {...field} /> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| <FormField | |||
| control={form.control} | |||
| name="password" | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormLabel>{t('passwordLabel')}</FormLabel> | |||
| <FormControl> | |||
| <Input | |||
| type={'password'} | |||
| placeholder={t('passwordPlaceholder')} | |||
| {...field} | |||
| /> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| <Button type="submit" className="w-full"> | |||
| {t('signUp')} | |||
| </Button> | |||
| </form> | |||
| </Form> | |||
| ); | |||
| } | |||
| export function SignInForm() { | |||
| const { t } = useTranslate('login'); | |||
| const FormSchema = z.object({ | |||
| email: z.string().email({ | |||
| message: t('emailPlaceholder'), | |||
| }), | |||
| password: z.string({ required_error: t('passwordPlaceholder') }), | |||
| }); | |||
| const form = useForm<z.infer<typeof FormSchema>>({ | |||
| resolver: zodResolver(FormSchema), | |||
| defaultValues: { | |||
| email: '', | |||
| }, | |||
| }); | |||
| function onSubmit(data: z.infer<typeof FormSchema>) { | |||
| console.log('🚀 ~ onSubmit ~ data:', data); | |||
| toast({ | |||
| title: 'You submitted the following values:', | |||
| description: ( | |||
| <pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4"> | |||
| <code className="text-white">{JSON.stringify(data, null, 2)}</code> | |||
| </pre> | |||
| ), | |||
| }); | |||
| } | |||
| return ( | |||
| <Form {...form}> | |||
| <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6"> | |||
| <FormField | |||
| control={form.control} | |||
| name="email" | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormLabel>{t('emailLabel')}</FormLabel> | |||
| <FormControl> | |||
| <Input placeholder={t('emailPlaceholder')} {...field} /> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| <FormField | |||
| control={form.control} | |||
| name="password" | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormLabel>{t('passwordLabel')}</FormLabel> | |||
| <FormControl> | |||
| <Input | |||
| type={'password'} | |||
| placeholder={t('passwordPlaceholder')} | |||
| {...field} | |||
| /> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| <div className="flex items-center space-x-2"> | |||
| <Checkbox id="terms" /> | |||
| <label | |||
| htmlFor="terms" | |||
| className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" | |||
| > | |||
| {t('rememberMe')} | |||
| </label> | |||
| </div> | |||
| <Button type="submit" className="w-full"> | |||
| {t('login')} | |||
| </Button> | |||
| </form> | |||
| </Form> | |||
| ); | |||
| } | |||
| export function VerifyEmailForm() { | |||
| const FormSchema = z.object({ | |||
| pin: z.string().min(6, { | |||
| message: 'Your one-time password must be 6 characters.', | |||
| }), | |||
| }); | |||
| const form = useForm<z.infer<typeof FormSchema>>({ | |||
| resolver: zodResolver(FormSchema), | |||
| defaultValues: { | |||
| pin: '', | |||
| }, | |||
| }); | |||
| function onSubmit(data: z.infer<typeof FormSchema>) { | |||
| console.log('🚀 ~ onSubmit ~ data:', data); | |||
| toast({ | |||
| title: 'You submitted the following values:', | |||
| description: ( | |||
| <pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4"> | |||
| <code className="text-white">{JSON.stringify(data, null, 2)}</code> | |||
| </pre> | |||
| ), | |||
| }); | |||
| } | |||
| return ( | |||
| <Form {...form}> | |||
| <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6"> | |||
| <FormField | |||
| control={form.control} | |||
| name="pin" | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormLabel>One-Time Password</FormLabel> | |||
| <FormControl> | |||
| <InputOTP maxLength={6} {...field}> | |||
| <InputOTPGroup> | |||
| <InputOTPSlot index={0} /> | |||
| <InputOTPSlot index={1} /> | |||
| <InputOTPSlot index={2} /> | |||
| <InputOTPSlot index={3} /> | |||
| <InputOTPSlot index={4} /> | |||
| <InputOTPSlot index={5} /> | |||
| </InputOTPGroup> | |||
| </InputOTP> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| <Button type="submit" className="w-full"> | |||
| Verify | |||
| </Button> | |||
| </form> | |||
| </Form> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,88 @@ | |||
| import { Button } from '@/components/ui/button'; | |||
| import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; | |||
| import { Separator } from '@/components/ui/separator'; | |||
| import { useTranslate } from '@/hooks/common-hooks'; | |||
| import { DiscordLogoIcon, GitHubLogoIcon } from '@radix-ui/react-icons'; | |||
| import { SignInForm, SignUpForm, VerifyEmailForm } from './form'; | |||
| function LoginFooter() { | |||
| return ( | |||
| <section className="pt-[30px]"> | |||
| <Separator /> | |||
| <p className="text-center pt-[20px]">or continue with</p> | |||
| <div className="flex gap-4 justify-center pt-[20px]"> | |||
| <GitHubLogoIcon className="w-8 h-8"></GitHubLogoIcon> | |||
| <DiscordLogoIcon className="w-8 h-8"></DiscordLogoIcon> | |||
| </div> | |||
| </section> | |||
| ); | |||
| } | |||
| export function SignUpCard() { | |||
| const { t } = useTranslate('login'); | |||
| return ( | |||
| <Card className="w-[400px]"> | |||
| <CardHeader> | |||
| <CardTitle>{t('signUp')}</CardTitle> | |||
| </CardHeader> | |||
| <CardContent> | |||
| <SignUpForm></SignUpForm> | |||
| <LoginFooter></LoginFooter> | |||
| </CardContent> | |||
| </Card> | |||
| ); | |||
| } | |||
| export function SignInCard() { | |||
| const { t } = useTranslate('login'); | |||
| return ( | |||
| <Card className="w-[400px]"> | |||
| <CardHeader> | |||
| <CardTitle>{t('login')}</CardTitle> | |||
| </CardHeader> | |||
| <CardContent> | |||
| <SignInForm></SignInForm> | |||
| </CardContent> | |||
| </Card> | |||
| ); | |||
| } | |||
| export function VerifyEmailCard() { | |||
| // const { t } = useTranslate('login'); | |||
| return ( | |||
| <Card className="w-[400px]"> | |||
| <CardHeader> | |||
| <CardTitle>Verify email</CardTitle> | |||
| </CardHeader> | |||
| <CardContent> | |||
| <section className="flex gap-y-6 flex-col"> | |||
| <div className="flex items-center space-x-4"> | |||
| <div className="flex-1 space-y-1"> | |||
| <p className="text-sm font-medium leading-none"> | |||
| We’ve sent a 6-digit code to | |||
| </p> | |||
| <p className="text-sm text-blue-500">yifanwu92@gmail.com.</p> | |||
| </div> | |||
| <Button>Resend</Button> | |||
| </div> | |||
| <VerifyEmailForm></VerifyEmailForm> | |||
| </section> | |||
| </CardContent> | |||
| </Card> | |||
| ); | |||
| } | |||
| const Login = () => { | |||
| return ( | |||
| <> | |||
| <SignUpCard></SignUpCard> | |||
| <SignInCard></SignInCard> | |||
| <VerifyEmailCard></VerifyEmailCard> | |||
| </> | |||
| ); | |||
| }; | |||
| export default Login; | |||
| @@ -4,6 +4,11 @@ const routes = [ | |||
| component: '@/pages/login', | |||
| layout: false, | |||
| }, | |||
| { | |||
| path: '/login-next', | |||
| component: '@/pages/login-next', | |||
| layout: false, | |||
| }, | |||
| { | |||
| path: '/chat/share', | |||
| component: '@/pages/chat/share', | |||
| @@ -116,6 +121,11 @@ const routes = [ | |||
| component: '@/pages/404', | |||
| layout: false, | |||
| }, | |||
| { | |||
| path: '/demo', | |||
| component: '@/pages/demo', | |||
| layout: false, | |||
| }, | |||
| ]; | |||
| export default routes; | |||
| @@ -0,0 +1,86 @@ | |||
| const { fontFamily } = require('tailwindcss/defaultTheme'); | |||
| /** @type {import('tailwindcss').Config} */ | |||
| module.exports = { | |||
| darkMode: ['class'], | |||
| content: [ | |||
| './src/pages/**/*.tsx', | |||
| './src/components/**/*.tsx', | |||
| './src/layouts/**/*.tsx', | |||
| ], | |||
| theme: { | |||
| container: { | |||
| center: true, | |||
| padding: '2rem', | |||
| screens: { | |||
| '2xl': '1400px', | |||
| }, | |||
| }, | |||
| extend: { | |||
| colors: { | |||
| border: 'hsl(var(--border))', | |||
| input: 'hsl(var(--input))', | |||
| ring: 'hsl(var(--ring))', | |||
| background: 'hsl(var(--background))', | |||
| foreground: 'hsl(var(--foreground))', | |||
| primary: { | |||
| DEFAULT: 'hsl(var(--primary))', | |||
| foreground: 'hsl(var(--primary-foreground))', | |||
| }, | |||
| secondary: { | |||
| DEFAULT: 'hsl(var(--secondary))', | |||
| foreground: 'hsl(var(--secondary-foreground))', | |||
| }, | |||
| destructive: { | |||
| DEFAULT: 'hsl(var(--destructive))', | |||
| foreground: 'hsl(var(--destructive-foreground))', | |||
| }, | |||
| muted: { | |||
| DEFAULT: 'hsl(var(--muted))', | |||
| foreground: 'hsl(var(--muted-foreground))', | |||
| }, | |||
| accent: { | |||
| DEFAULT: 'hsl(var(--accent))', | |||
| foreground: 'hsl(var(--accent-foreground))', | |||
| }, | |||
| popover: { | |||
| DEFAULT: 'hsl(var(--popover))', | |||
| foreground: 'hsl(var(--popover-foreground))', | |||
| }, | |||
| card: { | |||
| DEFAULT: 'hsl(var(--card))', | |||
| foreground: 'hsl(var(--card-foreground))', | |||
| }, | |||
| }, | |||
| borderRadius: { | |||
| lg: `var(--radius)`, | |||
| md: `calc(var(--radius) - 2px)`, | |||
| sm: 'calc(var(--radius) - 4px)', | |||
| }, | |||
| fontFamily: { | |||
| sans: ['var(--font-sans)', ...fontFamily.sans], | |||
| }, | |||
| keyframes: { | |||
| 'accordion-down': { | |||
| from: { height: '0' }, | |||
| to: { height: 'var(--radix-accordion-content-height)' }, | |||
| }, | |||
| 'accordion-up': { | |||
| from: { height: 'var(--radix-accordion-content-height)' }, | |||
| to: { height: '0' }, | |||
| }, | |||
| 'caret-blink': { | |||
| '0%,70%,100%': { opacity: '1' }, | |||
| '20%,50%': { opacity: '0' }, | |||
| }, | |||
| }, | |||
| animation: { | |||
| 'accordion-down': 'accordion-down 0.2s ease-out', | |||
| 'accordion-up': 'accordion-up 0.2s ease-out', | |||
| 'caret-blink': 'caret-blink 1.25s ease-out infinite', | |||
| }, | |||
| }, | |||
| }, | |||
| plugins: [require('tailwindcss-animate')], | |||
| }; | |||
| @@ -0,0 +1,83 @@ | |||
| @tailwind base; | |||
| @tailwind components; | |||
| @tailwind utilities; | |||
| @layer base { | |||
| :root { | |||
| --background: 0 0% 100%; | |||
| --foreground: 222.2 47.4% 11.2%; | |||
| --muted: 210 40% 96.1%; | |||
| --muted-foreground: 215.4 16.3% 46.9%; | |||
| --popover: 0 0% 100%; | |||
| --popover-foreground: 222.2 47.4% 11.2%; | |||
| --border: 214.3 31.8% 91.4%; | |||
| --input: 214.3 31.8% 91.4%; | |||
| --card: 0 0% 100%; | |||
| --card-foreground: 222.2 47.4% 11.2%; | |||
| --primary: 222.2 47.4% 11.2%; | |||
| --primary-foreground: 210 40% 98%; | |||
| --secondary: 210 40% 96.1%; | |||
| --secondary-foreground: 222.2 47.4% 11.2%; | |||
| --accent: 210 40% 96.1%; | |||
| --accent-foreground: 222.2 47.4% 11.2%; | |||
| --destructive: 0 100% 50%; | |||
| --destructive-foreground: 210 40% 98%; | |||
| --ring: 215 20.2% 65.1%; | |||
| --radius: 0.5rem; | |||
| } | |||
| .dark { | |||
| --background: 224 71% 4%; | |||
| --foreground: 213 31% 91%; | |||
| --muted: 223 47% 11%; | |||
| --muted-foreground: 215.4 16.3% 56.9%; | |||
| --accent: 216 34% 17%; | |||
| --accent-foreground: 210 40% 98%; | |||
| --popover: 224 71% 4%; | |||
| --popover-foreground: 215 20.2% 65.1%; | |||
| --border: 216 34% 17%; | |||
| --input: 216 34% 17%; | |||
| --card: 224 71% 4%; | |||
| --card-foreground: 213 31% 91%; | |||
| --primary: 210 40% 98%; | |||
| --primary-foreground: 222.2 47.4% 1.2%; | |||
| --secondary: 222.2 47.4% 11.2%; | |||
| --secondary-foreground: 210 40% 98%; | |||
| --destructive: 0 63% 31%; | |||
| --destructive-foreground: 210 40% 98%; | |||
| --ring: 216 34% 17%; | |||
| --radius: 0.5rem; | |||
| } | |||
| } | |||
| @layer base { | |||
| * { | |||
| @apply border-border; | |||
| } | |||
| body { | |||
| @apply bg-background text-foreground; | |||
| font-feature-settings: | |||
| 'rlig' 1, | |||
| 'calt' 1; | |||
| } | |||
| } | |||