diff --git a/apps/web/package.json b/apps/web/package.json
index d6e2629..afb8a5d 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -12,6 +12,9 @@
"test": "vitest run"
},
"dependencies": {
+ "@bprogress/react": "catalog:",
+ "@dicebear/collection": "catalog:",
+ "@dicebear/core": "catalog:",
"@orpc/client": "catalog:",
"@orpc/contract": "catalog:",
"@orpc/json-schema": "catalog:",
@@ -19,6 +22,7 @@
"@orpc/server": "catalog:",
"@orpc/tanstack-query": "catalog:",
"@orpc/zod": "catalog:",
+ "@tabler/icons-react": "catalog:",
"@tailwindcss/typography": "catalog:",
"@tailwindcss/vite": "catalog:",
"@tanstack/react-devtools": "catalog:",
@@ -36,10 +40,16 @@
"@trid/shared": "workspace:*",
"@trid/ui": "workspace:*",
"better-auth": "catalog:",
+ "date-fns": "catalog:",
"lucide-react": "catalog:",
- "nitro": "npm:nitro-nightly@latest",
+ "motion": "catalog:",
+ "next-themes": "catalog:",
+ "nitro": "catalog:",
"react": "catalog:",
"react-dom": "catalog:",
+ "react-icons": "catalog:",
+ "react-intersection-observer": "catalog:",
+ "sonner": "catalog:",
"tailwindcss": "catalog:",
"zod": "catalog:"
},
diff --git a/apps/web/src/components/avatar-user.tsx b/apps/web/src/components/avatar-user.tsx
new file mode 100644
index 0000000..2fa611b
--- /dev/null
+++ b/apps/web/src/components/avatar-user.tsx
@@ -0,0 +1,35 @@
+import { Avatar, AvatarFallback, AvatarImage } from '@trid/ui/components/avatar'
+import { cn } from '@trid/ui/lib/utils'
+
+import { GeneratedAvatar } from '#/components/generated-avatar'
+
+export const AvatarUser = ({
+ image,
+ name,
+ className,
+}: {
+ image?: string | null
+ name: string
+ className?: string
+}) => {
+ if (!image) {
+ return (
+
+ )
+ }
+
+ return (
+
+
+
+ {name.charAt(0)}
+
+
+ )
+}
diff --git a/apps/web/src/components/error-components.tsx b/apps/web/src/components/error-components.tsx
new file mode 100644
index 0000000..7e4d0b2
--- /dev/null
+++ b/apps/web/src/components/error-components.tsx
@@ -0,0 +1,118 @@
+import { useQueryErrorResetBoundary } from '@tanstack/react-query'
+import { Link, useRouter } from '@tanstack/react-router'
+
+import { useEffect } from 'react'
+
+import {
+ AlertTriangleIcon,
+ ArrowUpRightFromSquareIcon,
+ HomeIcon,
+ RefreshCcwIcon,
+} from 'lucide-react'
+
+import { Button } from '@trid/ui/components/button'
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@trid/ui/components/dialog'
+import {
+ Empty,
+ EmptyContent,
+ EmptyDescription,
+ EmptyHeader,
+ EmptyMedia,
+ EmptyTitle,
+} from '@trid/ui/components/empty'
+
+export const DefaultErrorComponent = ({ error }: { error: Error }) => {
+ const isDev = process.env.NODE_ENV !== 'production'
+
+ const router = useRouter()
+ const queryClientErrorBoundary = useQueryErrorResetBoundary()
+
+ const handleRetry = () => {
+ void router.invalidate()
+ }
+
+ useEffect(() => {
+ queryClientErrorBoundary.reset()
+ }, [queryClientErrorBoundary])
+
+ return (
+
+
+
+
+
+
+ Oops! Something went wrong
+
+ It looks like something unexpected happened.
+
+ Please try again later.
+
+
+
+
+
+
+
+
+ {isDev && (
+
+ )}
+
+
+ )
+}
diff --git a/apps/web/src/components/generated-avatar.tsx b/apps/web/src/components/generated-avatar.tsx
new file mode 100644
index 0000000..cdd83f7
--- /dev/null
+++ b/apps/web/src/components/generated-avatar.tsx
@@ -0,0 +1,40 @@
+import { useMemo } from 'react'
+
+import {
+ botttsNeutral,
+ initials,
+ notionistsNeutral,
+} from '@dicebear/collection'
+import { createAvatar } from '@dicebear/core'
+
+import { Avatar, AvatarFallback, AvatarImage } from '@trid/ui/components/avatar'
+import { cn } from '@trid/ui/lib/utils'
+
+interface Props {
+ seed: string
+ className?: string
+ style: 'botttsNeutral' | 'initials' | 'notionistsNeutral'
+}
+
+export const GeneratedAvatar = ({ seed, style, className }: Props) => {
+ const avatar = useMemo(() => {
+ const avatarVariants = {
+ botttsNeutral: () => createAvatar(botttsNeutral, { seed }),
+ initials: () => createAvatar(initials, { seed }),
+ notionistsNeutral: () => createAvatar(notionistsNeutral, { seed }),
+ }
+
+ return avatarVariants[style]()
+ }, [seed, style])
+
+ const avatarUri = useMemo(() => avatar.toDataUri(), [avatar])
+
+ return (
+
+
+
+ {Array.from(seed)[0] ?? '?'}
+
+
+ )
+}
diff --git a/apps/web/src/components/loader.tsx b/apps/web/src/components/loader.tsx
new file mode 100644
index 0000000..d049975
--- /dev/null
+++ b/apps/web/src/components/loader.tsx
@@ -0,0 +1,28 @@
+import { motion } from 'motion/react'
+
+import { cn } from '@trid/ui/lib/utils'
+
+function InlineLoader({ className }: { className?: string }) {
+ return (
+
+ {[0, 0.15, 0.3].map((delay, i) => (
+
+ ))}
+
+ )
+}
+
+export const Loader = {
+ Inline: InlineLoader,
+}
diff --git a/apps/web/src/components/marv-icon.tsx b/apps/web/src/components/marv-icon.tsx
new file mode 100644
index 0000000..92b852a
--- /dev/null
+++ b/apps/web/src/components/marv-icon.tsx
@@ -0,0 +1,26 @@
+import type { SVGProps } from 'react'
+
+type MarvIconProps = SVGProps
+
+export const MarvIcon = ({ ...props }: MarvIconProps) => {
+ return (
+
+ )
+}
diff --git a/apps/web/src/components/navbar.tsx b/apps/web/src/components/navbar.tsx
new file mode 100644
index 0000000..552cb43
--- /dev/null
+++ b/apps/web/src/components/navbar.tsx
@@ -0,0 +1,32 @@
+import { PanelLeftCloseIcon, PanelLeftIcon } from 'lucide-react'
+
+import { Button } from '@trid/ui/components/button'
+import { useSidebar } from '@trid/ui/components/sidebar'
+
+export const Navbar = () => {
+ const { state, isMobile, toggleSidebar } = useSidebar()
+
+ return (
+
+
+
+
+
+ )
+}
diff --git a/apps/web/src/components/not-found-components.tsx b/apps/web/src/components/not-found-components.tsx
new file mode 100644
index 0000000..693fef9
--- /dev/null
+++ b/apps/web/src/components/not-found-components.tsx
@@ -0,0 +1,40 @@
+import { Link } from '@tanstack/react-router'
+
+import { HomeIcon } from 'lucide-react'
+
+import { Button } from '@trid/ui/components/button'
+import {
+ Empty,
+ EmptyContent,
+ EmptyDescription,
+ EmptyHeader,
+ EmptyTitle,
+} from '@trid/ui/components/empty'
+
+export const DefaultNotFoundComponent = () => {
+ return (
+
+
+
+
+ 404
+
+
+ The page you're looking for might have been
+ moved or doesn't exist.
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/web/src/components/pending-components.tsx b/apps/web/src/components/pending-components.tsx
new file mode 100644
index 0000000..ab64ef8
--- /dev/null
+++ b/apps/web/src/components/pending-components.tsx
@@ -0,0 +1,9 @@
+import { Loader } from '#/components/loader'
+
+export const DefaultPendingComponent = () => {
+ return (
+
+
+
+ )
+}
diff --git a/apps/web/src/components/sidebar-content.tsx b/apps/web/src/components/sidebar-content.tsx
new file mode 100644
index 0000000..d769ad0
--- /dev/null
+++ b/apps/web/src/components/sidebar-content.tsx
@@ -0,0 +1,64 @@
+import {
+ Link,
+ linkOptions,
+ useMatch,
+ rootRouteId,
+} from '@tanstack/react-router'
+
+import { HomeIcon } from 'lucide-react'
+
+import {
+ SidebarContent,
+ SidebarGroup,
+ SidebarGroupContent,
+ SidebarMenu,
+ SidebarMenuButton,
+ SidebarMenuItem,
+} from '@trid/ui/components/sidebar'
+import { cn } from '@trid/ui/lib/utils'
+
+type MainSidebarContentProps = React.ComponentProps
+
+const navLinks = linkOptions([
+ {
+ to: '/',
+ icon: HomeIcon,
+ label: 'Home',
+ },
+])
+
+export const MainSidebarContent = ({
+ className,
+ ...props
+}: MainSidebarContentProps) => {
+ const match = useMatch({ from: rootRouteId })
+
+ return (
+
+
+
+
+ {navLinks.map((link) => (
+
+
+
+
+ {link.label}
+
+
+
+ ))}
+
+
+
+
+ )
+}
diff --git a/apps/web/src/components/sidebar-footer.tsx b/apps/web/src/components/sidebar-footer.tsx
new file mode 100644
index 0000000..db62426
--- /dev/null
+++ b/apps/web/src/components/sidebar-footer.tsx
@@ -0,0 +1,283 @@
+import {
+ getRouteApi,
+ Link,
+ linkOptions,
+ useMatchRoute,
+ useRouter,
+} from '@tanstack/react-router'
+
+import type { ComponentProps, FC } from 'react'
+
+import { AvatarUser } from './avatar-user'
+import { IconSettings } from '@tabler/icons-react'
+import {
+ ChevronsUpDownIcon,
+ LogInIcon,
+ LogOutIcon,
+ UserIcon,
+} from 'lucide-react'
+import { toast } from 'sonner'
+
+import type { auth } from '@trid/auth'
+
+import { Button } from '@trid/ui/components/button'
+import {
+ Drawer,
+ DrawerClose,
+ DrawerContent,
+ DrawerDescription,
+ DrawerFooter,
+ DrawerHeader,
+ DrawerTitle,
+ DrawerTrigger,
+} from '@trid/ui/components/drawer'
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuGroup,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from '@trid/ui/components/dropdown-menu'
+import { Separator } from '@trid/ui/components/separator'
+import {
+ SidebarFooter,
+ SidebarGroup,
+ SidebarGroupContent,
+ SidebarMenu,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ useSidebar,
+} from '@trid/ui/components/sidebar'
+import { cn } from '@trid/ui/lib/utils'
+
+import { authClient } from '#/libs/auth-client'
+
+type MainSidebarFooterProps = ComponentProps
+
+const routeApi = getRouteApi('/_app')
+
+export const MainSidebarFooter: FC = ({
+ className,
+ ...props
+}) => {
+ const { auth } = routeApi.useRouteContext()
+ const router = useRouter()
+
+ const { isMobile } = useSidebar()
+
+ const handleSignOut = async () => {
+ await authClient.signOut({
+ fetchOptions: {
+ onSuccess: () => {
+ void router.invalidate()
+ },
+ onError: (ctx) => {
+ toast.error('Sign out failed', {
+ description: ctx.error.message,
+ })
+ },
+ },
+ })
+ }
+ return (
+
+
+
+
+ {auth ? (
+
+ ) : (
+
+
+
+ )}
+
+
+
+
+ )
+}
+
+const getUserDropdownLinks = () => {
+ return linkOptions([
+ {
+ to: '.',
+ icon: UserIcon,
+ label: 'Profile',
+ },
+ {
+ to: '.',
+ icon: IconSettings,
+ label: 'Settings',
+ },
+ ])
+}
+
+interface SidebarFooterUserButtonProps {
+ user: typeof auth.$Infer.Session.user
+ onSignOut: () => void
+ isMobile: boolean
+}
+
+const SidebarFooterUserButton: FC = ({
+ user,
+ onSignOut,
+ isMobile,
+}) => {
+ const userDropdownLinks = getUserDropdownLinks()
+
+ const matchRoute = useMatchRoute()
+ if (isMobile) {
+ return (
+
+
+
+
+
+ {user.name}
+ @{user.username}
+
+
+
+
+
+
+
+
+
+
+ {user.name}
+
+
+ @{user.username}
+
+
+
+
+
+
+ {userDropdownLinks.map((link) => {
+ const isActive = matchRoute({ to: link.to })
+
+ return (
+
+
+
+ )
+ })}
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+
+ return (
+
+
+
+
+
+ {user.name}
+
+ @{user.username}
+
+
+
+
+
+
+
+
+
+
+ {user.name}
+
+ @{user.username}
+
+
+
+
+
+
+ {userDropdownLinks.map((item) => {
+ const isActive = matchRoute({ to: item.to })
+
+ return (
+
+
+
+ )
+ })}
+
+
+
+
+ Sign out
+
+
+
+ )
+}
diff --git a/apps/web/src/components/sidebar-header.tsx b/apps/web/src/components/sidebar-header.tsx
new file mode 100644
index 0000000..13913d1
--- /dev/null
+++ b/apps/web/src/components/sidebar-header.tsx
@@ -0,0 +1,38 @@
+import { Link } from '@tanstack/react-router'
+
+import type { ComponentProps } from 'react'
+
+import { env } from '@trid/env/client'
+
+import {
+ SidebarHeader,
+ SidebarMenu,
+ SidebarMenuButton,
+ SidebarMenuItem,
+} from '@trid/ui/components/sidebar'
+
+import { MarvIcon } from '#/components/marv-icon'
+
+type MainSidebarHeaderProps = ComponentProps
+
+export const MainSidebarHeader = ({ ...props }: MainSidebarHeaderProps) => {
+ return (
+
+
+
+
+
+
+
+ {env.VITE_APP_NAME}
+
+
+
+
+
+
+ )
+}
diff --git a/apps/web/src/components/sidebar-wrapper.tsx b/apps/web/src/components/sidebar-wrapper.tsx
new file mode 100644
index 0000000..36fa308
--- /dev/null
+++ b/apps/web/src/components/sidebar-wrapper.tsx
@@ -0,0 +1,33 @@
+import type { ComponentProps, FC, ReactNode } from 'react'
+
+import {
+ SidebarInset,
+ SidebarProvider,
+ Sidebar,
+} from '@trid/ui/components/sidebar'
+
+import { MainSidebarContent } from '#/components/sidebar-content'
+import { MainSidebarFooter } from '#/components/sidebar-footer'
+import { MainSidebarHeader } from '#/components/sidebar-header'
+
+type SidebarMainProps = ComponentProps
+
+export const SidebarWrapper = ({ children }: { children: ReactNode }) => {
+ return (
+
+
+
+ {children}
+
+ )
+}
+
+const SidebarMain: FC = ({ ...props }) => {
+ return (
+
+
+
+
+
+ )
+}
diff --git a/apps/web/src/components/sign-in-social-buttons.tsx b/apps/web/src/components/sign-in-social-buttons.tsx
new file mode 100644
index 0000000..c135e7c
--- /dev/null
+++ b/apps/web/src/components/sign-in-social-buttons.tsx
@@ -0,0 +1,65 @@
+import { useSearch } from '@tanstack/react-router'
+
+import { FaGithub } from 'react-icons/fa6'
+import { FcGoogle } from 'react-icons/fc'
+import type { IconType } from 'react-icons/lib'
+import { toast } from 'sonner'
+
+import { Button } from '@trid/ui/components/button'
+
+import { authClient } from '#/libs/auth-client'
+
+type SocialButton = {
+ provider: 'github' | 'google'
+ icon: IconType
+ label: string
+}
+
+const socialButtons: Array = [
+ {
+ provider: 'github',
+ icon: FaGithub,
+ label: 'GitHub',
+ },
+ {
+ provider: 'google',
+ icon: FcGoogle,
+ label: 'Google',
+ },
+] as const
+
+export const SignInSocialButtons = () => {
+ const { callbackURL } = useSearch({ from: '/_auth', strict: true })
+
+ const handleSignIn = async (provider: SocialButton['provider']) => {
+ await authClient.signIn.social({
+ provider,
+ callbackURL,
+ fetchOptions: {
+ onError: (ctx) => {
+ toast.error('Failed to sign in.', {
+ description: ctx.error.message,
+ })
+ },
+ },
+ })
+ }
+
+ return (
+
+ {socialButtons.map((button) => (
+
+ ))}
+
+ )
+}
diff --git a/apps/web/src/components/tanstack-router-progress-provider.tsx b/apps/web/src/components/tanstack-router-progress-provider.tsx
new file mode 100644
index 0000000..40a2408
--- /dev/null
+++ b/apps/web/src/components/tanstack-router-progress-provider.tsx
@@ -0,0 +1,41 @@
+import { ProgressProvider } from '@bprogress/react'
+import type { RouterProgressProviderProps } from '@bprogress/react'
+
+import { TanStackRouterProgress } from '#/components/tanstack-router-progress'
+
+export type TanStackRouterProgressProviderProps = RouterProgressProviderProps
+
+export const TanStackRouterProgressProvider = ({
+ children,
+ color,
+ delay,
+ disableSameURL,
+ disableStyle,
+ height,
+ nonce,
+ options,
+ spinnerPosition,
+ startPosition,
+ stopDelay,
+ style,
+}: TanStackRouterProgressProviderProps) => {
+ return (
+
+
+ {children}
+
+ )
+}
diff --git a/apps/web/src/components/tanstack-router-progress.tsx b/apps/web/src/components/tanstack-router-progress.tsx
new file mode 100644
index 0000000..0b25f16
--- /dev/null
+++ b/apps/web/src/components/tanstack-router-progress.tsx
@@ -0,0 +1,48 @@
+import type { ListenerFn, RouterEvents } from '@tanstack/react-router'
+import { useRouter } from '@tanstack/react-router'
+
+import { useEffect } from 'react'
+
+import type { RouterProgressProps } from '@bprogress/react'
+import { useProgress, withMemo } from '@bprogress/react'
+
+export type TanStackRouterProgressProps = RouterProgressProps
+
+const TanStackRouterProgressComponent = ({
+ delay = 0,
+ disableSameURL = true,
+ startPosition = 0,
+ stopDelay = 0,
+}: RouterProgressProps) => {
+ const { start, stop } = useProgress()
+ const router = useRouter()
+
+ useEffect(() => {
+ const handleRouteStart: ListenerFn = (
+ event
+ ) => {
+ // If the URL is the same, we don't want to start the progress bar
+ if (!event.hrefChanged && disableSameURL) {
+ return
+ }
+
+ start(startPosition, delay)
+ }
+
+ const handleRouteDone = () => stop(stopDelay)
+
+ const unsubscribeStart = router.subscribe('onBeforeLoad', handleRouteStart)
+ const unsubscribeStop = router.subscribe('onResolved', handleRouteDone)
+
+ return () => {
+ unsubscribeStart()
+ unsubscribeStop()
+ }
+ }, [router, start, stop, disableSameURL, startPosition, delay, stopDelay])
+
+ return null
+}
+
+export const TanStackRouterProgress = withMemo(TanStackRouterProgressComponent)
+
+TanStackRouterProgress.displayName = 'TanStackRouterProgress'
diff --git a/apps/web/src/components/theme-provider.tsx b/apps/web/src/components/theme-provider.tsx
new file mode 100644
index 0000000..6ee8150
--- /dev/null
+++ b/apps/web/src/components/theme-provider.tsx
@@ -0,0 +1,118 @@
+import { ScriptOnce } from '@tanstack/react-router'
+
+import {
+ createContext,
+ use,
+ useCallback,
+ useEffect,
+ useMemo,
+ useState,
+} from 'react'
+
+type Theme = 'dark' | 'light' | 'system'
+const MEDIA = '(prefers-color-scheme: dark)'
+
+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(initialState)
+
+// references:
+// https://ui.shadcn.com/docs/dark-mode/vite
+// https://github.com/pacocoursey/next-themes/blob/main/next-themes/src/index.tsx
+export function ThemeProvider({
+ children,
+ defaultTheme = 'system',
+ storageKey = 'theme',
+ ...props
+}: ThemeProviderProps) {
+ const [theme, setTheme] = useState(
+ () =>
+ (typeof window !== 'undefined'
+ ? (localStorage.getItem(storageKey) as Theme)
+ : null) ?? defaultTheme
+ )
+
+ const handleMediaQuery = useCallback(
+ (e: MediaQueryListEvent | MediaQueryList) => {
+ if (theme !== 'system') return
+ const root = window.document.documentElement
+ const targetTheme = e.matches ? 'dark' : 'light'
+ if (!root.classList.contains(targetTheme)) {
+ root.classList.remove('light', 'dark')
+ root.classList.add(targetTheme)
+ }
+ },
+ [theme]
+ )
+
+ // Listen for system preference changes
+ useEffect(() => {
+ const media = window.matchMedia(MEDIA)
+
+ media.addEventListener('change', handleMediaQuery)
+ handleMediaQuery(media)
+
+ return () => media.removeEventListener('change', handleMediaQuery)
+ }, [handleMediaQuery])
+
+ useEffect(() => {
+ const root = window.document.documentElement
+
+ let targetTheme: string
+
+ if (theme === 'system') {
+ localStorage.removeItem(storageKey)
+ targetTheme = window.matchMedia(MEDIA).matches ? 'dark' : 'light'
+ } else {
+ localStorage.setItem(storageKey, theme)
+ targetTheme = theme
+ }
+
+ // Only update if the target theme is not already applied
+ if (!root.classList.contains(targetTheme)) {
+ root.classList.remove('light', 'dark')
+ root.classList.add(targetTheme)
+ }
+ }, [theme, storageKey])
+
+ const value = useMemo(
+ () => ({
+ theme,
+ setTheme,
+ }),
+ [theme]
+ )
+
+ return (
+
+
+ {/* Apply theme early to avoid FOUC */}
+ {`document.documentElement.classList.toggle(
+ 'dark',
+ localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)
+ )`}
+
+ {children}
+
+ )
+}
+
+export const useTheme = () => {
+ const context = use(ThemeProviderContext)
+
+ return context
+}
diff --git a/apps/web/src/features/threads/components/thread-card.tsx b/apps/web/src/features/threads/components/thread-card.tsx
new file mode 100644
index 0000000..841d513
--- /dev/null
+++ b/apps/web/src/features/threads/components/thread-card.tsx
@@ -0,0 +1,184 @@
+import { Link } from '@tanstack/react-router'
+
+import {
+ IconArrowBigDown,
+ IconArrowBigDownFilled,
+ IconArrowBigUp,
+ IconArrowBigUpFilled,
+ IconBookmark,
+ IconDotsVertical,
+ IconMessageCirclePlus,
+} from '@tabler/icons-react'
+import { formatDistanceToNowStrict } from 'date-fns'
+
+import type { RouterOutputs } from '@trid/api/routers'
+
+import type { auth as authServer } from '@trid/auth'
+
+import { Button } from '@trid/ui/components/button'
+import {
+ Card,
+ CardContent,
+ CardFooter,
+ CardHeader,
+} from '@trid/ui/components/card'
+import { Separator } from '@trid/ui/components/separator'
+
+import type { VoteDirection } from '@trid/shared'
+
+import { AvatarUser } from '#/components/avatar-user'
+import type { useToggleThreadVoteMutation } from '#/features/votes/hooks/use-votes'
+
+interface ThreadCardProps {
+ thread: RouterOutputs['threads']['list']['items'][number]
+ toggleVote: ReturnType['mutate']
+ auth: typeof authServer.$Infer.Session | null
+}
+
+export const ThreadCard = ({ thread, toggleVote, auth }: ThreadCardProps) => {
+ const relativeTime = (date: Date) => formatDistanceToNowStrict(date)
+
+ const handleVote = (direction: VoteDirection, slug: string) =>
+ toggleVote({ direction, slug })
+
+ return (
+
+
+
+
+
+
+
+
+ {thread.author.name}
+
+
+
+
+
+ {relativeTime(thread.createdAt)}
+
+
+
+
+ @{thread.author.username}
+
+
+
+
+ {auth ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {thread.title}
+
+
+
+
+
+
+
+ {auth ? (
+
+ ) : (
+
+ )}
+
+ {thread.votesScoresCount}
+
+ {auth ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ {auth ? (
+
+ ) : (
+
+ )}
+
+
+
+ )
+}
diff --git a/apps/web/src/features/threads/components/thread-list-card.tsx b/apps/web/src/features/threads/components/thread-list-card.tsx
new file mode 100644
index 0000000..f8484e2
--- /dev/null
+++ b/apps/web/src/features/threads/components/thread-list-card.tsx
@@ -0,0 +1,281 @@
+import { useNavigate } from '@tanstack/react-router'
+
+import { useEffect } from 'react'
+
+import {
+ IconClock,
+ IconCompass,
+ IconFlame,
+ IconMessageCircle,
+ IconRefresh,
+} from '@tabler/icons-react'
+import type { Icon } from '@tabler/icons-react'
+import { useInView } from 'react-intersection-observer'
+
+import type { auth as authServer } from '@trid/auth'
+
+import { Button } from '@trid/ui/components/button'
+import { ButtonGroup } from '@trid/ui/components/button-group'
+import {
+ Empty,
+ EmptyDescription,
+ EmptyHeader,
+ EmptyMedia,
+ EmptyTitle,
+} from '@trid/ui/components/empty'
+import { Skeleton } from '@trid/ui/components/skeleton'
+import { Spinner } from '@trid/ui/components/spinner'
+
+import type { ThreadFeed } from '@trid/shared'
+
+import { ThreadCard } from '#/features/threads/components/thread-card'
+import { useListThreadsInfiniteQuery } from '#/features/threads/hooks/use-threads'
+import { useToggleThreadVoteMutation } from '#/features/votes/hooks/use-votes'
+
+const threadListSort: Array<{ label: string; icon: Icon; value: ThreadFeed }> =
+ [
+ {
+ label: 'Discover',
+ value: 'discover',
+ icon: IconCompass,
+ },
+ {
+ label: 'Popular',
+ value: 'popular',
+ icon: IconFlame,
+ },
+ {
+ label: 'Latest',
+ value: 'latest',
+ icon: IconClock,
+ },
+ ] as const
+
+export const ThreadsListCard = ({
+ feed,
+ auth,
+}: {
+ feed: ThreadFeed
+ auth: typeof authServer.$Infer.Session | null
+}) => {
+ const navigate = useNavigate()
+ const { mutate } = useToggleThreadVoteMutation()
+ const {
+ data,
+ error,
+ fetchNextPage,
+ hasNextPage,
+ isFetchNextPageError,
+ isFetchingNextPage,
+ } = useListThreadsInfiniteQuery({ feed })
+ const { ref: loadMoreRef, inView } = useInView({
+ rootMargin: '320px 0px',
+ threshold: 0,
+ })
+
+ const threads = data.pages
+ .flatMap((page) => page.items)
+ .filter((threadItem) => !threadItem.deletedAt)
+ const hasReachedEnd =
+ !hasNextPage && !isFetchingNextPage && !isFetchNextPageError
+
+ const handleFeedChange = (value: ThreadFeed) => {
+ void navigate({
+ to: '.',
+ search: (prev) => ({ ...prev, feed: value }),
+ replace: true,
+ viewTransition: true,
+ })
+ }
+
+ const handleRetry = () => {
+ void fetchNextPage()
+ }
+
+ useEffect(() => {
+ if (inView && hasNextPage && !isFetchingNextPage && !isFetchNextPageError) {
+ void fetchNextPage()
+ }
+ }, [
+ fetchNextPage,
+ hasNextPage,
+ inView,
+ isFetchNextPageError,
+ isFetchingNextPage,
+ ])
+
+ return (
+
+
+ {threadListSort.map((sortItem) => (
+
+ ))}
+
+
+ {threads.length === 0 ? (
+
+ ) : (
+
+ {threads.map((threadItem) => (
+
+ ))}
+
+ {isFetchingNextPage &&
}
+
+ {isFetchNextPageError && (
+
+ )}
+
+
+
+ {hasReachedEnd && (
+
+ You're caught up.
+
+ )}
+
+ )}
+
+ )
+}
+
+export const ThreadListSkeleton = ({ count = 3 }: { count?: number }) => {
+ return (
+
+ {Array.from({ length: count }).map((_, index) => (
+
+ ))}
+
+ )
+}
+
+const ThreadListEmptyState = ({ feed }: { feed: ThreadFeed }) => {
+ const copyByFeed: Record =
+ {
+ discover: {
+ title: 'No threads in discovery yet',
+ description:
+ 'Fresh discussions will appear here once people start posting.',
+ },
+ popular: {
+ title: 'No popular threads yet',
+ description:
+ 'Threads need a few votes before this feed has something to rank.',
+ },
+ latest: {
+ title: 'No recent threads yet',
+ description:
+ 'The newest posts will land here as soon as they are published.',
+ },
+ }
+
+ const copy = copyByFeed[feed]
+
+ return (
+
+
+
+
+
+ {copy.title}
+ {copy.description}
+
+
+ )
+}
+
+const ThreadListPaginationError = ({
+ message,
+ onRetry,
+}: {
+ message: string
+ onRetry: () => void
+}) => {
+ return (
+
+
+ Couldn't load more threads. {message}
+
+
+
+ )
+}
+
+export const ThreadListPendingState = () => {
+ return (
+
+ )
+}
+
+export const ThreadListRoutePendingState = () => {
+ return (
+
+
+
+ )
+}
+
+export const ThreadListLoadingMoreState = () => {
+ return (
+
+
+ Loading more threads
+
+ )
+}
diff --git a/apps/web/src/features/threads/hooks/use-threads.ts b/apps/web/src/features/threads/hooks/use-threads.ts
new file mode 100644
index 0000000..d81c263
--- /dev/null
+++ b/apps/web/src/features/threads/hooks/use-threads.ts
@@ -0,0 +1,13 @@
+import { useSuspenseInfiniteQuery } from '@tanstack/react-query'
+
+import { listThreadsInfiniteQueryOptions } from '#/features/threads/utils/thread-query-options'
+import type { ListThreadsInfiniteQueryOptions } from '#/features/threads/utils/thread-query-options'
+
+export const useListThreadsInfiniteQuery = ({
+ feed,
+ limit,
+}: ListThreadsInfiniteQueryOptions) => {
+ return useSuspenseInfiniteQuery(
+ listThreadsInfiniteQueryOptions({ feed, limit })
+ )
+}
diff --git a/apps/web/src/features/threads/utils/thread-query-options.ts b/apps/web/src/features/threads/utils/thread-query-options.ts
new file mode 100644
index 0000000..12cc559
--- /dev/null
+++ b/apps/web/src/features/threads/utils/thread-query-options.ts
@@ -0,0 +1,24 @@
+import type { RouterInputs } from '@trid/api/routers'
+
+import { threadORPC } from '#/libs/orpc'
+
+type ListThreadsInput = RouterInputs['threads']['list']
+
+export type ListThreadsInfiniteQueryOptions = {
+ feed?: ListThreadsInput['feed']
+ limit?: number
+}
+
+export const listThreadsInfiniteQueryOptions = ({
+ feed,
+ limit,
+}: ListThreadsInfiniteQueryOptions) =>
+ threadORPC.list.infiniteOptions({
+ input: (pageParam: string | null) => ({
+ limit,
+ feed,
+ cursor: typeof pageParam === 'string' ? pageParam : undefined,
+ }),
+ initialPageParam: null,
+ getNextPageParam: (lastPage) => lastPage.nextCursor,
+ })
diff --git a/apps/web/src/features/votes/hooks/use-votes.ts b/apps/web/src/features/votes/hooks/use-votes.ts
new file mode 100644
index 0000000..6e6f545
--- /dev/null
+++ b/apps/web/src/features/votes/hooks/use-votes.ts
@@ -0,0 +1,50 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query'
+
+import { toast } from 'sonner'
+
+import {
+ applyVoteThreadResult,
+ applyOptimisticVoteThread,
+ snapshotVoteThreadQueries,
+ restoreVoteThreadSnapshot,
+ cancelVoteThreadQueries,
+ invalidateVoteThreadQueries,
+} from '#/features/votes/utils/votes-optimistic-cache'
+import { voteORPC } from '#/libs/orpc'
+
+export const useToggleThreadVoteMutation = () => {
+ const queryClient = useQueryClient()
+
+ return useMutation(
+ voteORPC.thread.mutationOptions({
+ onMutate: async ({ direction, slug }) => {
+ await cancelVoteThreadQueries(queryClient, slug)
+
+ const snapshot = snapshotVoteThreadQueries({ queryClient, slug })
+ applyOptimisticVoteThread(queryClient, slug, direction)
+ return { snapshot }
+ },
+
+ onError: (error, _input, context) => {
+ toast.error('Failed to vote', {
+ description: error.message,
+ })
+
+ if (context?.snapshot) {
+ restoreVoteThreadSnapshot(queryClient, context.snapshot)
+ }
+ },
+
+ onSuccess: (result, variables) => {
+ applyVoteThreadResult(queryClient, variables.slug, result)
+
+ toast.success('Thread voted', {
+ description: 'Your vote has been cast.',
+ })
+ },
+ onSettled: (_result, _error, variables) => {
+ invalidateVoteThreadQueries(queryClient, variables.slug)
+ },
+ })
+ )
+}
diff --git a/apps/web/src/features/votes/utils/votes-optimistic-cache.ts b/apps/web/src/features/votes/utils/votes-optimistic-cache.ts
new file mode 100644
index 0000000..44a9f16
--- /dev/null
+++ b/apps/web/src/features/votes/utils/votes-optimistic-cache.ts
@@ -0,0 +1,211 @@
+import type { InfiniteData, QueryClient, QueryKey } from '@tanstack/react-query'
+
+import type { RouterOutputs } from '@trid/api/routers'
+
+import type { VoteDirection } from '@trid/shared'
+
+import { threadORPC } from '#/libs/orpc'
+
+type VoteState = {
+ userVote: VoteDirection | null
+ votesScoresCount: number
+}
+
+type ListThreadItemOutput = RouterOutputs['threads']['list']['items'][number]
+type ListThreadsOutput = RouterOutputs['threads']['list']
+type TopThreadsOutput = RouterOutputs['threads']['top']
+type TopThreadItemOutput = RouterOutputs['threads']['top']['items'][number]
+type DetailThreadOutput = RouterOutputs['threads']['get']
+
+type ListThreadsInfiniteData = InfiniteData
+type TopThreadsInfiniteData = InfiniteData
+
+export type VoteThreadSnapshot = {
+ lists: Array<[QueryKey, ListThreadsInfiniteData | undefined]>
+ tops: Array<[QueryKey, TopThreadsInfiniteData | undefined]>
+ detail: [QueryKey, DetailThreadOutput | undefined]
+}
+
+export const getOptimisticVoteState = (
+ current: VoteState,
+ direction: VoteDirection
+): VoteState => {
+ const isSameVote = current.userVote === direction
+ const wasOpposite =
+ current.userVote !== null && current.userVote !== direction
+
+ const scoreDelta = isSameVote
+ ? direction === 'UPVOTE'
+ ? -1
+ : 1
+ : wasOpposite
+ ? direction === 'UPVOTE'
+ ? 2
+ : -2
+ : direction === 'UPVOTE'
+ ? 1
+ : -1
+
+ return {
+ userVote: isSameVote ? null : direction,
+ votesScoresCount: current.votesScoresCount + scoreDelta,
+ }
+}
+
+const threadsKeys = {
+ list: () => threadORPC.list.key({ type: 'infinite' }),
+ top: () => threadORPC.top.key({ type: 'infinite' }),
+ detail: (slug: string) =>
+ threadORPC.get.key({ type: 'query', input: { slug } }),
+}
+
+export const snapshotVoteThreadQueries = ({
+ queryClient,
+ slug,
+}: {
+ queryClient: QueryClient
+ slug: string
+}): VoteThreadSnapshot => {
+ const listKey = threadsKeys.list()
+ const topKey = threadsKeys.top()
+ const detailKey = threadsKeys.detail(slug)
+
+ return {
+ lists: queryClient.getQueriesData({
+ queryKey: listKey,
+ }),
+
+ tops: queryClient.getQueriesData({
+ queryKey: topKey,
+ }),
+
+ detail: [
+ detailKey,
+ queryClient.getQueryData(detailKey),
+ ],
+ }
+}
+
+export const restoreVoteThreadSnapshot = (
+ queryClient: QueryClient,
+ snapshot: VoteThreadSnapshot
+) => {
+ for (const [key, data] of snapshot.lists) {
+ queryClient.setQueryData(key, data)
+ }
+
+ for (const [key, data] of snapshot.tops) {
+ queryClient.setQueryData(key, data)
+ }
+
+ const [key, data] = snapshot.detail
+ queryClient.setQueryData(key, data)
+}
+
+export const updateVoteThreadsInListCaches = (
+ queryClient: QueryClient,
+ slug: string,
+ updater: (thread: ListThreadItemOutput) => ListThreadItemOutput
+) => {
+ queryClient.setQueriesData(
+ { queryKey: threadsKeys.list() },
+ (old) => {
+ if (!old) return old
+
+ return {
+ ...old,
+ pages: old.pages.map((page) => ({
+ ...page,
+ items: page.items.map((thread) =>
+ thread.slug === slug ? updater(thread) : thread
+ ),
+ })),
+ }
+ }
+ )
+}
+
+export const updateVoteThreadsInTopCaches = (
+ queryClient: QueryClient,
+ slug: string,
+ updater: (thread: TopThreadItemOutput) => TopThreadItemOutput
+) => {
+ queryClient.setQueriesData(
+ { queryKey: threadsKeys.top() },
+ (old) => {
+ if (!old) return old
+
+ return {
+ ...old,
+ pages: old.pages.map((page) => ({
+ ...page,
+ items: page.items.map((thread) =>
+ thread.slug === slug ? updater(thread) : thread
+ ),
+ })),
+ }
+ }
+ )
+}
+
+export const updateVoteThreadsInDetailCache = (
+ queryClient: QueryClient,
+ slug: string,
+ updater: (thread: DetailThreadOutput) => DetailThreadOutput
+) => {
+ queryClient.setQueryData(
+ threadsKeys.detail(slug),
+ (old) => (old ? updater(old) : old)
+ )
+}
+
+export const applyOptimisticVoteThread = (
+ queryClient: QueryClient,
+ slug: string,
+ direction: VoteDirection
+) => {
+ const update = (thread: TThread): TThread => ({
+ ...thread,
+ ...getOptimisticVoteState(thread, direction),
+ })
+
+ updateVoteThreadsInListCaches(queryClient, slug, update)
+ updateVoteThreadsInTopCaches(queryClient, slug, update)
+ updateVoteThreadsInDetailCache(queryClient, slug, update)
+}
+
+export const applyVoteThreadResult = (
+ queryClient: QueryClient,
+ slug: string,
+ result: RouterOutputs['votes']['thread']
+) => {
+ const update = (thread: TThread): TThread => ({
+ ...thread,
+ userVote: result.userVote,
+ votesScoresCount: result.votesScoresCount,
+ })
+
+ updateVoteThreadsInListCaches(queryClient, slug, update)
+ updateVoteThreadsInTopCaches(queryClient, slug, update)
+ updateVoteThreadsInDetailCache(queryClient, slug, update)
+}
+
+export const invalidateVoteThreadQueries = (
+ queryClient: QueryClient,
+ slug: string
+) => {
+ void queryClient.invalidateQueries({ queryKey: threadsKeys.list() })
+ void queryClient.invalidateQueries({ queryKey: threadsKeys.top() })
+ void queryClient.invalidateQueries({ queryKey: threadsKeys.detail(slug) })
+}
+
+export const cancelVoteThreadQueries = async (
+ queryClient: QueryClient,
+ slug: string
+) => {
+ await Promise.all([
+ queryClient.cancelQueries({ queryKey: threadsKeys.list() }),
+ queryClient.cancelQueries({ queryKey: threadsKeys.top() }),
+ queryClient.cancelQueries({ queryKey: threadsKeys.detail(slug) }),
+ ])
+}
diff --git a/apps/web/src/integrations/tanstack-query/devtools.tsx b/apps/web/src/integrations/tanstack-query/devtools.tsx
deleted file mode 100644
index 94c68c9..0000000
--- a/apps/web/src/integrations/tanstack-query/devtools.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools'
-
-export default {
- name: 'Tanstack Query',
- render: ,
-}
diff --git a/apps/web/src/integrations/tanstack-query/root-provider.tsx b/apps/web/src/integrations/tanstack-query/root-provider.tsx
deleted file mode 100644
index a4ff9d7..0000000
--- a/apps/web/src/integrations/tanstack-query/root-provider.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import { QueryClient } from '@tanstack/react-query'
-
-export function getContext() {
- const queryClient = new QueryClient()
-
- return {
- queryClient,
- }
-}
-export default function TanstackQueryProvider() {}
diff --git a/apps/web/src/libs/auth-client.ts b/apps/web/src/libs/auth-client.ts
new file mode 100644
index 0000000..e4ee5e5
--- /dev/null
+++ b/apps/web/src/libs/auth-client.ts
@@ -0,0 +1,21 @@
+import {
+ adminClient,
+ inferAdditionalFields,
+ multiSessionClient,
+ usernameClient,
+} from 'better-auth/client/plugins'
+import { createAuthClient } from 'better-auth/react'
+
+import type { auth } from '@trid/auth'
+
+import { env } from '@trid/env/client'
+
+export const authClient = createAuthClient({
+ baseURL: env.VITE_APP_URL,
+ plugins: [
+ usernameClient(),
+ multiSessionClient(),
+ adminClient(),
+ inferAdditionalFields(),
+ ],
+})
diff --git a/apps/web/src/libs/orpc.ts b/apps/web/src/libs/orpc.ts
index 41a585f..7d86a43 100644
--- a/apps/web/src/libs/orpc.ts
+++ b/apps/web/src/libs/orpc.ts
@@ -10,6 +10,8 @@ import { createORPCContext } from '@trid/api'
import { orpcRouters } from '@trid/api/routers'
import type { ORPCRouterClient } from '@trid/api/routers'
+import { env } from '@trid/env/client'
+
const getORPCClient = createIsomorphicFn()
.server(() =>
createRouterClient(orpcRouters, {
@@ -18,7 +20,7 @@ const getORPCClient = createIsomorphicFn()
)
.client((): ORPCRouterClient => {
const link = new RPCLink({
- url: `${window.location.origin}/api/rpc`,
+ url: `${env.VITE_APP_URL}/api/rpc`,
fetch(url, options) {
return fetch(url, {
...options,
@@ -32,3 +34,13 @@ const getORPCClient = createIsomorphicFn()
const client: ORPCRouterClient = getORPCClient()
export const orpc = createTanstackQueryUtils(client)
+
+export const threadORPC = createTanstackQueryUtils(client.threads, {
+ path: ['threads'],
+})
+export const replyORPC = createTanstackQueryUtils(client.replies, {
+ path: ['replies'],
+})
+export const voteORPC = createTanstackQueryUtils(client.votes, {
+ path: ['votes'],
+})
diff --git a/apps/web/src/libs/query-client.ts b/apps/web/src/libs/query-client.ts
index 9103428..d07eaf4 100644
--- a/apps/web/src/libs/query-client.ts
+++ b/apps/web/src/libs/query-client.ts
@@ -4,7 +4,8 @@ const createQueryClient = () =>
new QueryClient({
defaultOptions: {
queries: {
- staleTime: 60 * 1000, // 1 minute
+ staleTime: 30 * 1000, // 30 seconds
+ gcTime: 5 * 60 * 1000, // 5 minutes
retry: 3,
},
},
diff --git a/apps/web/src/routeTree.gen.ts b/apps/web/src/routeTree.gen.ts
index 2f3376e..dc97576 100644
--- a/apps/web/src/routeTree.gen.ts
+++ b/apps/web/src/routeTree.gen.ts
@@ -9,14 +9,36 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
-import { Route as IndexRouteImport } from './routes/index'
+import { Route as AuthRouteImport } from './routes/_auth'
+import { Route as AppRouteImport } from './routes/_app'
+import { Route as AppIndexRouteImport } from './routes/_app/index'
+import { Route as AuthSignInRouteImport } from './routes/_auth/sign-in'
+import { Route as AuthOnBoardingRouteImport } from './routes/_auth/on-boarding'
import { Route as ApiRpcSplatRouteImport } from './routes/api/rpc.$'
import { Route as ApiAuthSplatRouteImport } from './routes/api/auth.$'
-const IndexRoute = IndexRouteImport.update({
+const AuthRoute = AuthRouteImport.update({
+ id: '/_auth',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const AppRoute = AppRouteImport.update({
+ id: '/_app',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const AppIndexRoute = AppIndexRouteImport.update({
id: '/',
path: '/',
- getParentRoute: () => rootRouteImport,
+ getParentRoute: () => AppRoute,
+} as any)
+const AuthSignInRoute = AuthSignInRouteImport.update({
+ id: '/sign-in',
+ path: '/sign-in',
+ getParentRoute: () => AuthRoute,
+} as any)
+const AuthOnBoardingRoute = AuthOnBoardingRouteImport.update({
+ id: '/on-boarding',
+ path: '/on-boarding',
+ getParentRoute: () => AuthRoute,
} as any)
const ApiRpcSplatRoute = ApiRpcSplatRouteImport.update({
id: '/api/rpc/$',
@@ -30,44 +52,89 @@ const ApiAuthSplatRoute = ApiAuthSplatRouteImport.update({
} as any)
export interface FileRoutesByFullPath {
- '/': typeof IndexRoute
+ '/': typeof AppIndexRoute
+ '/on-boarding': typeof AuthOnBoardingRoute
+ '/sign-in': typeof AuthSignInRoute
'/api/auth/$': typeof ApiAuthSplatRoute
'/api/rpc/$': typeof ApiRpcSplatRoute
}
export interface FileRoutesByTo {
- '/': typeof IndexRoute
+ '/': typeof AppIndexRoute
+ '/on-boarding': typeof AuthOnBoardingRoute
+ '/sign-in': typeof AuthSignInRoute
'/api/auth/$': typeof ApiAuthSplatRoute
'/api/rpc/$': typeof ApiRpcSplatRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
- '/': typeof IndexRoute
+ '/_app': typeof AppRouteWithChildren
+ '/_auth': typeof AuthRouteWithChildren
+ '/_auth/on-boarding': typeof AuthOnBoardingRoute
+ '/_auth/sign-in': typeof AuthSignInRoute
+ '/_app/': typeof AppIndexRoute
'/api/auth/$': typeof ApiAuthSplatRoute
'/api/rpc/$': typeof ApiRpcSplatRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
- fullPaths: '/' | '/api/auth/$' | '/api/rpc/$'
+ fullPaths: '/' | '/on-boarding' | '/sign-in' | '/api/auth/$' | '/api/rpc/$'
fileRoutesByTo: FileRoutesByTo
- to: '/' | '/api/auth/$' | '/api/rpc/$'
- id: '__root__' | '/' | '/api/auth/$' | '/api/rpc/$'
+ to: '/' | '/on-boarding' | '/sign-in' | '/api/auth/$' | '/api/rpc/$'
+ id:
+ | '__root__'
+ | '/_app'
+ | '/_auth'
+ | '/_auth/on-boarding'
+ | '/_auth/sign-in'
+ | '/_app/'
+ | '/api/auth/$'
+ | '/api/rpc/$'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
- IndexRoute: typeof IndexRoute
+ AppRoute: typeof AppRouteWithChildren
+ AuthRoute: typeof AuthRouteWithChildren
ApiAuthSplatRoute: typeof ApiAuthSplatRoute
ApiRpcSplatRoute: typeof ApiRpcSplatRoute
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
- '/': {
- id: '/'
- path: '/'
+ '/_auth': {
+ id: '/_auth'
+ path: ''
+ fullPath: '/'
+ preLoaderRoute: typeof AuthRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/_app': {
+ id: '/_app'
+ path: ''
fullPath: '/'
- preLoaderRoute: typeof IndexRouteImport
+ preLoaderRoute: typeof AppRouteImport
parentRoute: typeof rootRouteImport
}
+ '/_app/': {
+ id: '/_app/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof AppIndexRouteImport
+ parentRoute: typeof AppRoute
+ }
+ '/_auth/sign-in': {
+ id: '/_auth/sign-in'
+ path: '/sign-in'
+ fullPath: '/sign-in'
+ preLoaderRoute: typeof AuthSignInRouteImport
+ parentRoute: typeof AuthRoute
+ }
+ '/_auth/on-boarding': {
+ id: '/_auth/on-boarding'
+ path: '/on-boarding'
+ fullPath: '/on-boarding'
+ preLoaderRoute: typeof AuthOnBoardingRouteImport
+ parentRoute: typeof AuthRoute
+ }
'/api/rpc/$': {
id: '/api/rpc/$'
path: '/api/rpc/$'
@@ -85,8 +152,31 @@ declare module '@tanstack/react-router' {
}
}
+interface AppRouteChildren {
+ AppIndexRoute: typeof AppIndexRoute
+}
+
+const AppRouteChildren: AppRouteChildren = {
+ AppIndexRoute: AppIndexRoute,
+}
+
+const AppRouteWithChildren = AppRoute._addFileChildren(AppRouteChildren)
+
+interface AuthRouteChildren {
+ AuthOnBoardingRoute: typeof AuthOnBoardingRoute
+ AuthSignInRoute: typeof AuthSignInRoute
+}
+
+const AuthRouteChildren: AuthRouteChildren = {
+ AuthOnBoardingRoute: AuthOnBoardingRoute,
+ AuthSignInRoute: AuthSignInRoute,
+}
+
+const AuthRouteWithChildren = AuthRoute._addFileChildren(AuthRouteChildren)
+
const rootRouteChildren: RootRouteChildren = {
- IndexRoute: IndexRoute,
+ AppRoute: AppRouteWithChildren,
+ AuthRoute: AuthRouteWithChildren,
ApiAuthSplatRoute: ApiAuthSplatRoute,
ApiRpcSplatRoute: ApiRpcSplatRoute,
}
diff --git a/apps/web/src/router.tsx b/apps/web/src/router.tsx
index 3c37a87..7eb6bac 100644
--- a/apps/web/src/router.tsx
+++ b/apps/web/src/router.tsx
@@ -3,6 +3,9 @@ import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query
import { routeTree } from './routeTree.gen'
+import { DefaultErrorComponent } from '#/components/error-components'
+import { DefaultNotFoundComponent } from '#/components/not-found-components'
+import { DefaultPendingComponent } from '#/components/pending-components'
import { orpc } from '#/libs/orpc'
import { getQueryClient } from '#/libs/query-client'
@@ -15,6 +18,9 @@ export function getRouter() {
scrollRestoration: true,
defaultPreload: 'intent',
defaultPreloadStaleTime: 0,
+ defaultPendingComponent: DefaultPendingComponent,
+ defaultNotFoundComponent: DefaultNotFoundComponent,
+ defaultErrorComponent: DefaultErrorComponent,
})
setupRouterSsrQueryIntegration({ router, queryClient })
diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx
index 71e82d2..1742318 100644
--- a/apps/web/src/routes/__root.tsx
+++ b/apps/web/src/routes/__root.tsx
@@ -8,6 +8,11 @@ import {
} from '@tanstack/react-router'
import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'
+import { Toaster } from '@trid/ui/components/sonner'
+import { TooltipProvider } from '@trid/ui/components/tooltip'
+
+import { TanStackRouterProgressProvider } from '#/components/tanstack-router-progress-provider'
+import { ThemeProvider } from '#/components/theme-provider'
import { getAuthFn } from '#/functions/get-auth'
import type { orpc } from '#/libs/orpc'
import appCss from '#/styles.css?url'
@@ -23,6 +28,7 @@ export const Route = createRootRouteWithContext()({
return { auth }
},
+ shellComponent: RootDocument,
head: () => ({
meta: [
{
@@ -43,17 +49,30 @@ export const Route = createRootRouteWithContext()({
},
],
}),
- shellComponent: RootDocument,
})
function RootDocument({ children }: { children: React.ReactNode }) {
return (
-
+
- {children}
+
+
+ {children}
+
+
+
+
{
+ if (auth && !auth.user.onBoardingCompleted) {
+ throw redirect({
+ to: '/on-boarding',
+ replace: true,
+ viewTransition: true,
+ })
+ }
+ },
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return (
+
+
+
+
+
+
+ )
+}
diff --git a/apps/web/src/routes/_app/index.tsx b/apps/web/src/routes/_app/index.tsx
new file mode 100644
index 0000000..6df2063
--- /dev/null
+++ b/apps/web/src/routes/_app/index.tsx
@@ -0,0 +1,87 @@
+import { createFileRoute, useRouter } from '@tanstack/react-router'
+
+import { IconAlertTriangle, IconRefresh } from '@tabler/icons-react'
+import { z } from 'zod'
+
+import { Button } from '@trid/ui/components/button'
+import {
+ Empty,
+ EmptyContent,
+ EmptyDescription,
+ EmptyHeader,
+ EmptyMedia,
+ EmptyTitle,
+} from '@trid/ui/components/empty'
+
+import { THREAD_FEEDS } from '@trid/shared'
+
+import {
+ ThreadListRoutePendingState,
+ ThreadsListCard,
+} from '#/features/threads/components/thread-list-card'
+import { listThreadsInfiniteQueryOptions } from '#/features/threads/utils/thread-query-options'
+
+const querySchema = z.object({
+ feed: z.enum(THREAD_FEEDS).optional(),
+})
+
+export const Route = createFileRoute('/_app/')({
+ validateSearch: querySchema,
+ beforeLoad: async ({ context, search }) => {
+ const { queryClient } = context
+ const { feed = 'discover' } = search
+
+ const threads = await queryClient.ensureInfiniteQueryData(
+ listThreadsInfiniteQueryOptions({ feed })
+ )
+
+ return { threads }
+ },
+ pendingComponent: ThreadListRoutePendingState,
+ errorComponent: ThreadListRouteErrorState,
+ component: Home,
+})
+
+function Home() {
+ const { auth } = Route.useRouteContext()
+ const { feed = 'discover' } = Route.useSearch()
+
+ return (
+
+
+
+ )
+}
+
+function ThreadListRouteErrorState({ error }: { error: Error }) {
+ const router = useRouter()
+
+ const handleRetry = () => {
+ void router.invalidate()
+ }
+
+ return (
+
+
+
+
+
+
+ Threads did not load
+
+ {error.message || 'Refresh the feed and try again.'}
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/web/src/routes/_auth.tsx b/apps/web/src/routes/_auth.tsx
new file mode 100644
index 0000000..a54dda6
--- /dev/null
+++ b/apps/web/src/routes/_auth.tsx
@@ -0,0 +1,42 @@
+import { createFileRoute, Link, Outlet } from '@tanstack/react-router'
+
+import { ArrowLeftIcon } from 'lucide-react'
+import { z } from 'zod'
+
+import { Button } from '@trid/ui/components/button'
+
+function sanitizeCallbackURL(url: string): string {
+ if (!url.startsWith('/') || url.startsWith('//')) {
+ return '/'
+ }
+ return url
+}
+
+const searchSchema = z.object({
+ callbackURL: z.string().optional().default('/').transform(sanitizeCallbackURL),
+})
+
+export const Route = createFileRoute('/_auth')({
+ validateSearch: searchSchema,
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return (
+
+ )
+}
diff --git a/apps/web/src/routes/_auth/on-boarding.tsx b/apps/web/src/routes/_auth/on-boarding.tsx
new file mode 100644
index 0000000..9db11e6
--- /dev/null
+++ b/apps/web/src/routes/_auth/on-boarding.tsx
@@ -0,0 +1,173 @@
+import { useForm } from '@tanstack/react-form'
+import { createFileRoute, redirect, useNavigate } from '@tanstack/react-router'
+
+import { toast } from 'sonner'
+import { z } from 'zod'
+
+import { env } from '@trid/env/client'
+
+import { Button } from '@trid/ui/components/button'
+import {
+ Card,
+ CardContent,
+ CardHeader,
+ CardTitle,
+ CardDescription,
+} from '@trid/ui/components/card'
+import {
+ Field,
+ FieldError,
+ FieldGroup,
+ FieldLabel,
+} from '@trid/ui/components/field'
+import {
+ InputGroup,
+ InputGroupAddon,
+ InputGroupInput,
+ InputGroupText,
+} from '@trid/ui/components/input-group'
+import { Spinner } from '@trid/ui/components/spinner'
+
+import { MarvIcon } from '#/components/marv-icon'
+import { authClient } from '#/libs/auth-client'
+
+export const Route = createFileRoute('/_auth/on-boarding')({
+ component: RouteComponent,
+ beforeLoad: ({ context: { auth }, search }) => {
+ if (auth?.user.onBoardingCompleted) {
+ throw redirect({
+ to: search.callbackURL,
+ replace: true,
+ viewTransition: true,
+ })
+ }
+ },
+})
+
+const onBoardingFormSchema = z.object({
+ username: z
+ .string()
+ .min(3, 'Username must be at least 3 characters long')
+ .max(30, 'Username must be at most 30 characters long')
+ .regex(
+ /^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){1,28}$/i,
+ 'Username must be at least 3 characters long, can only contain letters, numbers, and hyphens, and must start and end with a letter or number'
+ ),
+})
+
+function RouteComponent() {
+ const { callbackURL } = Route.useSearch()
+ const navigate = useNavigate()
+
+ const form = useForm({
+ defaultValues: {
+ username: '',
+ },
+ validators: {
+ onChange: onBoardingFormSchema,
+ onSubmit: onBoardingFormSchema,
+ },
+ onSubmit: async ({ value }) => {
+ await authClient.updateUser({
+ username: value.username,
+ onBoardingCompleted: true,
+
+ fetchOptions: {
+ onSuccess: () => {
+ void navigate({
+ to: callbackURL,
+ replace: true,
+ viewTransition: true,
+ })
+ },
+
+ onError: ({ error }) => {
+ toast.error('Failed to update user', {
+ description: error.message,
+ })
+ },
+ },
+ })
+ },
+ })
+
+ return (
+
+
+
+
+
+ Welcome to {env.VITE_APP_NAME}
+
+
+ Let's get to know you better
+
+
+
+
+ [state.canSubmit, state.isSubmitting]}
+ children={([canSubmit, isSubmitting]) => (
+
+ )}
+ />
+
+
+
+ )
+}
diff --git a/apps/web/src/routes/_auth/sign-in.tsx b/apps/web/src/routes/_auth/sign-in.tsx
new file mode 100644
index 0000000..306e5a2
--- /dev/null
+++ b/apps/web/src/routes/_auth/sign-in.tsx
@@ -0,0 +1,44 @@
+import { createFileRoute, redirect } from '@tanstack/react-router'
+
+import {
+ Card,
+ CardContent,
+ CardHeader,
+ CardTitle,
+ CardDescription,
+} from '@trid/ui/components/card'
+
+import { MarvIcon } from '#/components/marv-icon'
+import { SignInSocialButtons } from '#/components/sign-in-social-buttons'
+
+export const Route = createFileRoute('/_auth/sign-in')({
+ component: RouteComponent,
+ beforeLoad: ({ context: { auth }, search }) => {
+ if (auth) {
+ throw redirect({
+ to: search.callbackURL,
+ replace: true,
+ viewTransition: true,
+ })
+ }
+ },
+})
+
+function RouteComponent() {
+ return (
+
+
+
+
+ Welcome back
+
+ Sign in to your account
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/web/src/routes/index.tsx b/apps/web/src/routes/index.tsx
deleted file mode 100644
index 9d4d51c..0000000
--- a/apps/web/src/routes/index.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import { useQuery } from '@tanstack/react-query'
-import { createFileRoute } from '@tanstack/react-router'
-
-import { orpc } from '#/libs/orpc'
-
-export const Route = createFileRoute('/')({ component: Home })
-
-function Home() {
- const { auth } = Route.useRouteContext()
- const { data } = useQuery(orpc.me.queryOptions())
- return (
-
-
Welcome to TanStack Start
-
- Edit src/routes/index.tsx to get started.
-
-
- {auth ? (
-
- {JSON.stringify(data, null, 2)}
-
- ) : (
-
User Not Login
- )}
-
- )
-}
diff --git a/bun.lock b/bun.lock
index 106f341..d4df65b 100644
--- a/bun.lock
+++ b/bun.lock
@@ -3,7 +3,7 @@
"configVersion": 1,
"workspaces": {
"": {
- "name": "marvticle-monorepo",
+ "name": "trid",
"dependencies": {
"@trid/env": "workspace:*",
"dotenv": "catalog:",
@@ -25,6 +25,9 @@
"apps/web": {
"name": "web",
"dependencies": {
+ "@bprogress/react": "catalog:",
+ "@dicebear/collection": "catalog:",
+ "@dicebear/core": "catalog:",
"@orpc/client": "catalog:",
"@orpc/contract": "catalog:",
"@orpc/json-schema": "catalog:",
@@ -32,6 +35,7 @@
"@orpc/server": "catalog:",
"@orpc/tanstack-query": "catalog:",
"@orpc/zod": "catalog:",
+ "@tabler/icons-react": "catalog:",
"@tailwindcss/typography": "catalog:",
"@tailwindcss/vite": "catalog:",
"@tanstack/react-devtools": "catalog:",
@@ -49,10 +53,16 @@
"@trid/shared": "workspace:*",
"@trid/ui": "workspace:*",
"better-auth": "catalog:",
+ "date-fns": "catalog:",
"lucide-react": "catalog:",
- "nitro": "npm:nitro-nightly@latest",
+ "motion": "catalog:",
+ "next-themes": "catalog:",
+ "nitro": "catalog:",
"react": "catalog:",
"react-dom": "catalog:",
+ "react-icons": "catalog:",
+ "react-intersection-observer": "catalog:",
+ "sonner": "catalog:",
"tailwindcss": "catalog:",
"zod": "catalog:",
},
@@ -181,10 +191,15 @@
"@tabler/icons-react": "catalog:",
"class-variance-authority": "catalog:",
"clsx": "catalog:",
+ "next-themes": "catalog:",
"radix-ui": "catalog:",
+ "react": "catalog:",
+ "react-dom": "catalog:",
"shadcn": "catalog:",
+ "sonner": "catalog:",
"tailwind-merge": "catalog:",
"tw-animate-css": "catalog:",
+ "vaul": "^1.1.2",
"zod": "catalog:",
},
"devDependencies": {
@@ -201,6 +216,9 @@
},
"catalog": {
"@better-auth/infra": "^0.2.8",
+ "@bprogress/react": "^1.2.7",
+ "@dicebear/collection": "^9.4.2",
+ "@dicebear/core": "^9.4.2",
"@fontsource-variable/inter": "^5.2.8",
"@fontsource-variable/outfit": "^5.2.8",
"@orpc/client": "^1.14.3",
@@ -223,12 +241,12 @@
"@tanstack/eslint-plugin-router": "^1.162.0",
"@tanstack/react-devtools": "latest",
"@tanstack/react-form": "latest",
- "@tanstack/react-query": "latest",
- "@tanstack/react-query-devtools": "latest",
- "@tanstack/react-router": "latest",
- "@tanstack/react-router-devtools": "latest",
- "@tanstack/react-router-ssr-query": "latest",
- "@tanstack/react-start": "latest",
+ "@tanstack/react-query": "^5.100.14",
+ "@tanstack/react-query-devtools": "^5.100.14",
+ "@tanstack/react-router": "^1.170.8",
+ "@tanstack/react-router-devtools": "^1.167.0",
+ "@tanstack/react-router-ssr-query": "^1.167.0",
+ "@tanstack/react-start": "^1.168.9",
"@tanstack/router-plugin": "^1.132.0",
"@testing-library/dom": "^10.4.1",
"@testing-library/react": "^16.3.0",
@@ -242,6 +260,7 @@
"bun-types": "^1.3.14",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "date-fns": "^4.3.0",
"dotenv": "^17.4.2",
"drizzle-kit": "^0.31.10",
"drizzle-orm": "^0.45.2",
@@ -249,7 +268,10 @@
"jsdom": "^28.1.0",
"limax": "^4.2.3",
"lucide-react": "^0.545.0",
+ "motion": "^12.40.0",
"nanoid": "^5.1.11",
+ "next-themes": "^0.4.6",
+ "nitro": "^3.0.260522-beta",
"oxfmt": "^0.51.0",
"oxlint": "^1.66.0",
"oxlint-tsgolint": "^0.23.0",
@@ -257,8 +279,11 @@
"radix-ui": "^1.4.3",
"react": "^19.2.0",
"react-dom": "^19.2.0",
+ "react-icons": "^5.6.0",
+ "react-intersection-observer": "^10.0.3",
"resend": "^6.12.3",
"shadcn": "^4.7.0",
+ "sonner": "^2.0.7",
"tailwind-merge": "^3.6.0",
"tailwindcss": "^4.1.18",
"turbo": "^2.9.14",
@@ -361,6 +386,10 @@
"@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="],
+ "@bprogress/core": ["@bprogress/core@1.3.4", "", {}, "sha512-q/AqpurI/1uJzOrQROuZWixn/+ARekh+uvJGwLCP6HQ/EqAX4SkvNf618tSBxL4NysC0MwqAppb/mRw6Tzi61w=="],
+
+ "@bprogress/react": ["@bprogress/react@1.2.7", "", { "dependencies": { "@bprogress/core": "^1.3.4" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-MqJfHW+R5CQeWqyqrLxUjdBRHk24Xl63OkBLo5DMWqUqocUikRTfCIc/jtQQbPk7BRfdr5OP3Lx7YlfQ9QOZMQ=="],
+
"@bramus/specificity": ["@bramus/specificity@2.4.2", "", { "dependencies": { "css-tree": "^3.0.0" }, "bin": { "specificity": "bin/cli.js" } }, "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw=="],
"@csstools/color-helpers": ["@csstools/color-helpers@6.0.2", "", {}, "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q=="],
@@ -375,7 +404,73 @@
"@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="],
- "@dotenvx/dotenvx": ["@dotenvx/dotenvx@1.66.0", "", { "dependencies": { "commander": "^11.1.0", "dotenv": "^17.2.1", "eciesjs": "^0.4.10", "execa": "^5.1.1", "fdir": "^6.2.0", "ignore": "^5.3.0", "object-treeify": "1.1.33", "picomatch": "^4.0.4", "which": "^4.0.0", "yocto-spinner": "^1.1.0" }, "bin": { "dotenvx": "src/cli/dotenvx.js" } }, "sha512-qlQFhHUjhRDybrinqLAD0MClVZDOrsq80O8eD5iSjz3Qa/4f3Jg7SQrOaSobrRyP1QaWIYLGtGpj2c7H0D8NUw=="],
+ "@dicebear/adventurer": ["@dicebear/adventurer@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-jqYp834ZmGDA9HBBDQAdgF1O2UTCwHF4vVrktXWa2Dppp1JczPL5HnVOWsjtrLmXNn61Wd6OLmBb2e6rhzp3ig=="],
+
+ "@dicebear/adventurer-neutral": ["@dicebear/adventurer-neutral@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-5xgkG/mNL4j3Q4SJGQLBU/KnU90tng8Ze5ofThD+55wi0oeY/nSAUowg6UFCmHrktjifj/MEx3CQqbpcPWtfIA=="],
+
+ "@dicebear/avataaars": ["@dicebear/avataaars@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-3x9jKFkOkFSPmpTbt9xvhiU2E1GX7beCSsX0tXRUShj8x6+5Ks9yBRT1VlkySbnXrZ/GglADGg7vJ/D2uIx1Yw=="],
+
+ "@dicebear/avataaars-neutral": ["@dicebear/avataaars-neutral@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-/eNrp0YCNJRwQXqOloLm1+3Ss2C+pMpUQIGkbEnGsP1UK+13Ge80ggDDof1HpdqvG9HAZcKa7hnbG/0HSwyDSw=="],
+
+ "@dicebear/big-ears": ["@dicebear/big-ears@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-mNfz3ppNA7UBq0IO3nXCiV5pFPG7c1DfzRB0foNU2Wo1XXT8FIcSY2BvDlYqorZTOUOz7dHb0vx06hqvG0HP5w=="],
+
+ "@dicebear/big-ears-neutral": ["@dicebear/big-ears-neutral@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-M8Ozmzza4eY4hpLOYULgJxMYmBA0CsBnrE15/xw6LZkEREXnrX5z0NJsf8hUfdyF6BWZ+RBgzoiav32DAC5zcg=="],
+
+ "@dicebear/big-smile": ["@dicebear/big-smile@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-hmT5i7rcPPhStjZyg28pbIhdTnnMBzK3RObI0vKCpY30EFrzaPkkdDL6Ck5fAFBdvDIW1EpOJkenyR0XPmhgbQ=="],
+
+ "@dicebear/bottts": ["@dicebear/bottts@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-tsx+dII7EFUCVA8URj66G1GqORCCVduCAx4dY2prEY2IeFianVpkntXuFsWZ9BBGx1NZFndvDith5oTwKMQPbQ=="],
+
+ "@dicebear/bottts-neutral": ["@dicebear/bottts-neutral@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-kFNwWt6j+gzZ5n5Pz7WVwePubREAQOF8ZwWA9ztwVYDVMLnOChWbAofy5FED4j5md2MXFH2EgLCFCMr5K2BmIA=="],
+
+ "@dicebear/collection": ["@dicebear/collection@9.4.2", "", { "dependencies": { "@dicebear/adventurer": "9.4.2", "@dicebear/adventurer-neutral": "9.4.2", "@dicebear/avataaars": "9.4.2", "@dicebear/avataaars-neutral": "9.4.2", "@dicebear/big-ears": "9.4.2", "@dicebear/big-ears-neutral": "9.4.2", "@dicebear/big-smile": "9.4.2", "@dicebear/bottts": "9.4.2", "@dicebear/bottts-neutral": "9.4.2", "@dicebear/croodles": "9.4.2", "@dicebear/croodles-neutral": "9.4.2", "@dicebear/dylan": "9.4.2", "@dicebear/fun-emoji": "9.4.2", "@dicebear/glass": "9.4.2", "@dicebear/icons": "9.4.2", "@dicebear/identicon": "9.4.2", "@dicebear/initials": "9.4.2", "@dicebear/lorelei": "9.4.2", "@dicebear/lorelei-neutral": "9.4.2", "@dicebear/micah": "9.4.2", "@dicebear/miniavs": "9.4.2", "@dicebear/notionists": "9.4.2", "@dicebear/notionists-neutral": "9.4.2", "@dicebear/open-peeps": "9.4.2", "@dicebear/personas": "9.4.2", "@dicebear/pixel-art": "9.4.2", "@dicebear/pixel-art-neutral": "9.4.2", "@dicebear/rings": "9.4.2", "@dicebear/shapes": "9.4.2", "@dicebear/thumbs": "9.4.2", "@dicebear/toon-head": "9.4.2" }, "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-KArubv7if8H7j9sIfpDK2hJJqrdNVR5zMPAMOSpIU2JPyXx8TC9o5wsmXb8il5wOHgaS9Q/cla7jUNIiDD7Gsg=="],
+
+ "@dicebear/core": ["@dicebear/core@9.4.2", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-MF0042+Z3s8PGZKZLySfhft28bUa3B1iq0e5NSjCvY8gfMi5aIH/iRJGRJa1N9Jz1BNkxYb4yvJ/N9KO8Z6Y+w=="],
+
+ "@dicebear/croodles": ["@dicebear/croodles@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-6VoO0JviIf7dKKMBTL/SMXxWhnXHaZuzufX90G0nXxS77ELG1YkGNMaZzawizN4C09Gbya2gJkozqrWiJN/aGw=="],
+
+ "@dicebear/croodles-neutral": ["@dicebear/croodles-neutral@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-oG5IeUdtiYshQ89gkAVcl5w3xAEi5UZX2fTzIyelpBPCG176l7VuuFzlxi2umnB3E6LVHYy06DXvUo/p+rXB2Q=="],
+
+ "@dicebear/dylan": ["@dicebear/dylan@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-1vQvRu9x9DrwFxhFaIU2rf0EUL04yDTbAt7fHyAjM0mEsKzTD4mRNf95tCRuavCoW6W48u7A/OY6jyIub6kxLQ=="],
+
+ "@dicebear/fun-emoji": ["@dicebear/fun-emoji@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-kqB6LPkdYCdEU/mwbyz34xLzoNUKL6ARcoo3fr5ASq9D6ZE07qIKybC3xv5+CPz7VmspJ1Q3c/VVWVMDRP7Twg=="],
+
+ "@dicebear/glass": ["@dicebear/glass@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-z5qUogHQ1b6UJ2zCqT848mU2U9DKbVDhiX6GPDjD7tYLisCCJVisH9p6WyNdHvflUd4SHkA6gRqVJIh2v2HnTA=="],
+
+ "@dicebear/icons": ["@dicebear/icons@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-QSMMz0NA03ypSGhXC8HQX8FSj8lYT+/5yqH+/N03OH2IjL0q7wwGZ7nqsrtlRp76O5WqMTwGfSbTUUYPjFr+Xw=="],
+
+ "@dicebear/identicon": ["@dicebear/identicon@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-JVDSmZsv11mSWqwAktK5x9Bslht2xY3TFUn8xzu6slAYe1Z7hEXZ76eb+UJ6F4qEzdwZ7xPWzAS6Nb0Y3A0pww=="],
+
+ "@dicebear/initials": ["@dicebear/initials@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-yePuIUasmwtl9IrtB6rEzE/zb5fImKP/neW0CdcTC2MwLgMuP1GLHEGRgg1zI8exIh+PMv1YdLGyyUuRTE2Qpw=="],
+
+ "@dicebear/lorelei": ["@dicebear/lorelei@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-YMv6vnriW6VLFDsreKuOnUFFno6SRe7+7X7R7zPY0rZ+MaHX9V3jcioIG+1PSjIHEDfOLUHpr5vd1JBWv8y7UA=="],
+
+ "@dicebear/lorelei-neutral": ["@dicebear/lorelei-neutral@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-yspanTthA5vh6iCdeLzn6xZ4yYMYRcfcxblcgSvHTF1ut0bjAXtw5SXzZ6aJTrJWiHkzYOQuTOR6GVYiW80Q7w=="],
+
+ "@dicebear/micah": ["@dicebear/micah@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-e4D3W/OlChSsLo7Llwsy0J18vk0azJqF/uFoY+EKACCNHBc1HGNsqVvu2CTf+OWOA8wTyAK6UkjBN5p01r7D+g=="],
+
+ "@dicebear/miniavs": ["@dicebear/miniavs@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-wLwyFNNUnDRd3BbhSBhXR0XEpX8sG0/xDA5M/OkDoapLqZnnI48YLUSDd2N5QTAVMmcSEuZOYxkcnj7WW79vlg=="],
+
+ "@dicebear/notionists": ["@dicebear/notionists@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-ZCySq+nxcD/x4xyYgytcj2N9uY3gxrL+qpnmOdp2BdA221KacVrxlsUPpIgEMqxS2rMmBQXfxg129Pzn4ycIpA=="],
+
+ "@dicebear/notionists-neutral": ["@dicebear/notionists-neutral@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-AyD9kEfVxQUwDGf4Op059gVmYIOAkTKg3dtE9h9mEKP7zl/kMy5B67BFFOo7sB0mXCjzAegZ6ekGU02E8+hIHw=="],
+
+ "@dicebear/open-peeps": ["@dicebear/open-peeps@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-i01tLgtp2g937T81sVeAOVlqsCtiTck/Kw20g7hN80+7xrXjOUepz2HPLy3HeiMjwjMGRy5o54kSd0/8Ht4Dqg=="],
+
+ "@dicebear/personas": ["@dicebear/personas@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-NJlkvI5F5gugt6t2+7QrYNTwQC7+4IQZS3vG0dYk2BncxOHax0BuLovdSdiAesTL4ZkytFYIydWmKmV2/xcUwg=="],
+
+ "@dicebear/pixel-art": ["@dicebear/pixel-art@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-peHf7oKICDgBZ8dUyj+txPnS7VZEWgvKE+xW4mNQqBt6dYZIjmva2shOVHn0b1JU+FDxMx3uIkWVixKdUq4WGg=="],
+
+ "@dicebear/pixel-art-neutral": ["@dicebear/pixel-art-neutral@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-9e9Lz554uQvWaXV2P17ss+hPa6rTyuAKBtB8zk8ECjHiZzIl61N/KcTVLZ4dILVZwj7gYriaLo16QEqvL2GJCg=="],
+
+ "@dicebear/rings": ["@dicebear/rings@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-Pc3ymWrRDQPJFNrbbLt7RJrzGvUuuxUiDkrfLhoVE+B6mZWEL1PC78DPbS1yUWYLErJOpJuM2GSwXmTbVjWf+g=="],
+
+ "@dicebear/shapes": ["@dicebear/shapes@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-AFL6jAaiLztvcqyq+ds+lWZu6Vbp3PlGWhJeJRm842jxtiluJpl6r4f6nUXP2fdMz7MNpDzXfLooQK9E04NbUQ=="],
+
+ "@dicebear/thumbs": ["@dicebear/thumbs@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-ccWvDBqbkWS5uzHbsg5L6uML6vBfX7jT3J3jHCQksvz8haHItxTK02w+6e1UavZUsvza4lG5X/XY3eji3siJ4Q=="],
+
+ "@dicebear/toon-head": ["@dicebear/toon-head@9.4.2", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-lwFeSXyAnaKnCfMt9TiJwnD1cXQUGkey/0h6i/+4TVHVMCz5/Ri5u1ynovPNHy1SnBf858QwoXHkxilGLwQX/g=="],
+
+ "@dotenvx/dotenvx": ["@dotenvx/dotenvx@1.68.0", "", { "dependencies": { "commander": "^11.1.0", "dotenv": "^17.2.1", "eciesjs": "^0.4.10", "enquirer": "^2.4.1", "execa": "^5.1.1", "fdir": "^6.2.0", "ignore": "^5.3.0", "object-treeify": "1.1.33", "picomatch": "^4.0.4", "which": "^4.0.0", "yocto-spinner": "^1.1.0" }, "bin": { "dotenvx": "src/cli/dotenvx.js" } }, "sha512-YGApsPyDmonlqPFGd5d9Gp5O4JQjmdQsoLYtXHTZb0bhlaCCjj69lWbXKYucCplUgvsorQqc5RRqA+tynjb1lA=="],
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="],
@@ -447,19 +542,17 @@
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
- "@eslint/config-array": ["@eslint/config-array@0.21.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" } }, "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw=="],
-
- "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="],
+ "@eslint/config-array": ["@eslint/config-array@0.23.5", "", { "dependencies": { "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA=="],
- "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
+ "@eslint/config-helpers": ["@eslint/config-helpers@0.6.0", "", { "dependencies": { "@eslint/core": "^1.2.1" } }, "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA=="],
- "@eslint/eslintrc": ["@eslint/eslintrc@3.3.5", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg=="],
+ "@eslint/core": ["@eslint/core@1.2.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ=="],
"@eslint/js": ["@eslint/js@10.0.1", "", { "peerDependencies": { "eslint": "^10.0.0" }, "optionalPeers": ["eslint"] }, "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA=="],
- "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="],
+ "@eslint/object-schema": ["@eslint/object-schema@3.0.5", "", {}, "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw=="],
- "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
+ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.1", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ=="],
"@exodus/bytes": ["@exodus/bytes@1.15.1", "", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q=="],
@@ -979,7 +1072,7 @@
"@tanstack/eslint-config": ["@tanstack/eslint-config@0.4.0", "", { "dependencies": { "@eslint/js": "^10.0.1", "@stylistic/eslint-plugin": "^5.8.0", "eslint-plugin-import-x": "^4.16.1", "eslint-plugin-n": "^17.24.0", "globals": "^17.3.0", "typescript-eslint": "^8.55.0", "vue-eslint-parser": "^10.4.0" }, "peerDependencies": { "eslint": "^9.0.0 || ^10.0.0" } }, "sha512-V+Cd81W/f65dqKJKpytbwTGx9R+IwxKAHsG/uJ3nSLYEh36hlAr54lRpstUhggQB8nf/cP733cIw8DuD2dzQUg=="],
- "@tanstack/eslint-plugin-query": ["@tanstack/eslint-plugin-query@5.100.11", "", { "dependencies": { "@typescript-eslint/utils": "^8.58.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": "^5.4.0 || ^6.0.0" }, "optionalPeers": ["typescript"] }, "sha512-4JfaSf6/ql9AFAsRWaWulz40gS86bDgSr15pWCI3o+oX3sdZ0ZR8AOeNrCEqyIrV6wFxnCfhFi1kWjOlZ+66Ew=="],
+ "@tanstack/eslint-plugin-query": ["@tanstack/eslint-plugin-query@5.100.14", "", { "dependencies": { "@typescript-eslint/utils": "^8.58.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": "^5.4.0 || ^6.0.0" }, "optionalPeers": ["typescript"] }, "sha512-NbpiBCmeHTRuVHeV5+U+1bzmxyTW5Dzp2sCeE6Hx+ZJTJWFK9dsm8VZmRc7LQP9/ZORsF620PvgUk67AwiBo4A=="],
"@tanstack/eslint-plugin-router": ["@tanstack/eslint-plugin-router@1.162.0", "", { "dependencies": { "@typescript-eslint/utils": "^8.23.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0" } }, "sha512-0mv+5fOnWXeorh6zd3VyHVfpejIzc7+z68Ts0CQMDzMiwjM5rvea46fyl2m50zx5JFennsu7RO/QJAfnqkKJXw=="],
@@ -989,7 +1082,7 @@
"@tanstack/pacer-lite": ["@tanstack/pacer-lite@0.1.1", "", {}, "sha512-y/xtNPNt/YeyoVxE/JCx+T7yjEzpezmbb+toK8DDD1P4m7Kzs5YR956+7OKexG3f8aXgC3rLZl7b1V+yNUSy5w=="],
- "@tanstack/query-core": ["@tanstack/query-core@5.100.11", "", {}, "sha512-lmE0994apShXPj8CUxgx4ch5yUJhE9k/+tVwihBvPOyerACWdBocfFg24t8+0RhtlTd7tEgchDkhlCxNssvDxw=="],
+ "@tanstack/query-core": ["@tanstack/query-core@5.100.14", "", {}, "sha512-5X41dGpxgeaHISCRW2oYwcSycZeULZzAunaudXT9ov1KOTj9xwt0CH6hbwqP1/z74ZWF7rYFnDpyYH07XFcZew=="],
"@tanstack/query-devtools": ["@tanstack/query-devtools@5.100.14", "", {}, "sha512-g96SmSSQecYTYcyuAMRXr895GplJv01UGt7qttQWPOUyZ5EGz5tbRc589bMc2m5BsPFD6O0PCEAHdbDYNP6UBw=="],
@@ -1083,6 +1176,8 @@
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
+ "@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="],
+
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
@@ -1197,6 +1292,8 @@
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
+ "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="],
+
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
@@ -1219,9 +1316,9 @@
"babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="],
- "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
+ "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
- "baseline-browser-mapping": ["baseline-browser-mapping@2.10.31", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q=="],
+ "baseline-browser-mapping": ["baseline-browser-mapping@2.10.32", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg=="],
"better-auth": ["better-auth@1.6.11", "", { "dependencies": { "@better-auth/core": "1.6.11", "@better-auth/drizzle-adapter": "1.6.11", "@better-auth/kysely-adapter": "1.6.11", "@better-auth/memory-adapter": "1.6.11", "@better-auth/mongo-adapter": "1.6.11", "@better-auth/prisma-adapter": "1.6.11", "@better-auth/telemetry": "1.6.11", "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.5", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.17", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": "^0.45.2", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-Wwt6+q07dwIhsp6XiM7L1qSXVUWBEtNl+eZvwM778CguFqDZFBN9Pt6LtFaHl55t8Z+Zc//5kxcbgDY8/79vFQ=="],
@@ -1233,7 +1330,7 @@
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
- "brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="],
+ "brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
@@ -1287,9 +1384,7 @@
"commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
- "comment-parser": ["comment-parser@1.4.6", "", {}, "sha512-ObxuY6vnbWTN6Od72xfwN9DbzC7Y2vv8u1Soi9ahRKL37gb6y1qk6/dgjs+3JWuXJHWvsg3BXIwzd/rkmAwavg=="],
-
- "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
+ "comment-parser": ["comment-parser@1.4.7", "", {}, "sha512-0h+uSNtQGW3D98eQt3jJ8L06Fves8hncB4V/PKdw/Qb8Hnk19VaKuTr55UNRYiSoVa7WwrFls+rh3ux9agmkeQ=="],
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
@@ -1329,6 +1424,8 @@
"data-urls": ["data-urls@7.0.0", "", { "dependencies": { "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.0" } }, "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA=="],
+ "date-fns": ["date-fns@4.3.0", "", {}, "sha512-OYcL+3N/jyWbYdFGqoMAhytDgxP9pbYPUUiRCOgn4Fewaadk9l/Wam4Avciiyp2BgkpfQyBV9B+ehnVJych+eQ=="],
+
"dayjs": ["dayjs@1.11.20", "", {}, "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ=="],
"db0": ["db0@0.3.4", "", { "peerDependencies": { "@electric-sql/pglite": "*", "@libsql/client": "*", "better-sqlite3": "*", "drizzle-orm": "*", "mysql2": "*", "sqlite3": "*" }, "optionalPeers": ["@electric-sql/pglite", "@libsql/client", "better-sqlite3", "drizzle-orm", "mysql2", "sqlite3"] }, "sha512-RiXXi4WaNzPTHEOu8UPQKMooIbqOEyqA1t7Z6MsdxSCeb8iUC9ko3LcmsLmeUt2SM5bctfArZKkRQggKZz7JNw=="],
@@ -1385,7 +1482,7 @@
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
- "electron-to-chromium": ["electron-to-chromium@1.5.360", "", {}, "sha512-GkcBt6YYAw9SxFWn+xVar4cLVGlXVuswwtRLBozi2zp0GjXs4ZnOrqV4zbXzg35n7w81hCkyJNYicgXlVHAmBA=="],
+ "electron-to-chromium": ["electron-to-chromium@1.5.361", "", {}, "sha512-Q6Hts7N9FnJc5LeGRINFvLhCI9xZmNtTDe5ZbcVezQz7cU4a8Aua3GH1b8J2XY8Al9PF+OCwYqhgsOOheMdvkA=="],
"emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
@@ -1393,7 +1490,9 @@
"encoding-sniffer": ["encoding-sniffer@0.2.1", "", { "dependencies": { "iconv-lite": "^0.6.3", "whatwg-encoding": "^3.1.1" } }, "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw=="],
- "enhanced-resolve": ["enhanced-resolve@5.21.6", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ=="],
+ "enhanced-resolve": ["enhanced-resolve@5.22.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-xYcDWrpELkFzz9SpZ3PlI6Eu6eD93Yf0WLDRxikGhWJ3MAir2SNZTIVCVZqZ/NUyx8AdMc2gT9C0gPiw18kG+A=="],
+
+ "enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="],
"entities": ["entities@8.0.0", "", {}, "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA=="],
@@ -1409,7 +1508,7 @@
"es-module-lexer": ["es-module-lexer@2.1.0", "", {}, "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ=="],
- "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
+ "es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="],
"esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
@@ -1419,7 +1518,7 @@
"escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
- "eslint": ["eslint@9.39.4", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="],
+ "eslint": ["eslint@10.4.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.6.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ=="],
"eslint-compat-utils": ["eslint-compat-utils@0.5.1", "", { "dependencies": { "semver": "^7.5.4" }, "peerDependencies": { "eslint": ">=6.0.0" } }, "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q=="],
@@ -1431,7 +1530,7 @@
"eslint-plugin-n": ["eslint-plugin-n@17.24.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.5.0", "enhanced-resolve": "^5.17.1", "eslint-plugin-es-x": "^7.8.0", "get-tsconfig": "^4.8.1", "globals": "^15.11.0", "globrex": "^0.1.2", "ignore": "^5.3.2", "semver": "^7.6.3", "ts-declaration-location": "^1.0.6" }, "peerDependencies": { "eslint": ">=8.23.0" } }, "sha512-/gC7/KAYmfNnPNOb3eu8vw+TdVnV0zhdQwexsw6FLXbhzroVj20vRn2qL8lDWDGnAQ2J8DhdfvXxX9EoxvERvw=="],
- "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
+ "eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="],
"eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
@@ -1513,6 +1612,8 @@
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
+ "framer-motion": ["framer-motion@12.40.0", "", { "dependencies": { "motion-dom": "^12.40.0", "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-uaBd3qC1v3KQqBEjwTUd183K6PbS+j0yR9w9VmEOLWA/tnUcSn8Xa3uck7t4dgpDoUss8xQTcj8W2L07lrnLFg=="],
+
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
"fs-extra": ["fs-extra@11.3.5", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg=="],
@@ -1559,8 +1660,6 @@
"h3-v2": ["h3@2.0.1-rc.20", "", { "dependencies": { "rou3": "^0.8.1", "srvx": "^0.11.13" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"], "bin": { "h3": "bin/h3.mjs" } }, "sha512-28ljodXuUp0fZovdiSRq4G9OgrxCztrJe5VdYzXAB7ueRvI7pIUqLU14Xi3XqdYJ/khXjfpUOOD2EQa6CmBgsg=="],
- "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
-
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="],
@@ -1569,7 +1668,7 @@
"hepburn": ["hepburn@1.2.2", "", {}, "sha512-DeykBc4XmfAWsnN+Y1Svi9uaQnnz21Q/ARuGWvIBxP1iUFeMIWL41DfVkgTh7tU23LFIbmIBO2Bk17BTPu0kVA=="],
- "hono": ["hono@4.12.21", "", {}, "sha512-uV63apnb0kyPtAUwoWgaGh9HyIFcv8lgmzPZSiTBQAFOFGIzka5EZ1dZocmGnn0XdX0+XTqJ6Tqv7selMuGLRQ=="],
+ "hono": ["hono@4.12.22", "", {}, "sha512-7fvVPbB92zNRsQke+uiRGwtTuef0tB2Dg4hWxYfFNvkQhIltWoyi0ONReM5LWA+jJWS3nfT5lTq+qbsIpX0IQw=="],
"hookable": ["hookable@6.1.1", "", {}, "sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ=="],
@@ -1681,7 +1780,7 @@
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
- "libphonenumber-js": ["libphonenumber-js@1.13.2", "", {}, "sha512-S3kmBrptp3yRTm83NUcHy9g1vbwiWMzI8WvY22+koBJ6zkRteLnedBL2VX0MIAGwx2yiyxX4J85pceZyQ6ffgg=="],
+ "libphonenumber-js": ["libphonenumber-js@1.13.3", "", {}, "sha512-xMkdAMqcyG7iN2WZZmGIfWbYxW4orRkny+0/AXIbwL0xll2zkDX0Vzo/BXFa6+7mh2UvJl9MbcTtHk0YXkFtBA=="],
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
@@ -1713,8 +1812,6 @@
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
- "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
-
"log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="],
"lru-cache": ["lru-cache@11.5.0", "", {}, "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA=="],
@@ -1749,10 +1846,16 @@
"mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
- "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
+ "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
+ "motion": ["motion@12.40.0", "", { "dependencies": { "framer-motion": "^12.40.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-yjrHUrBFW6kQvjJwRsoiPSAhC5tRwRqNGJWmiJ4CrGnbKp0V88AdzkhBmDoqIsIPfarOe0Uddd37Xq43/gIocA=="],
+
+ "motion-dom": ["motion-dom@12.40.0", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg=="],
+
+ "motion-utils": ["motion-utils@12.39.0", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="],
+
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"msw": ["msw@2.14.6", "", { "dependencies": { "@inquirer/confirm": "^6.0.11", "@mswjs/interceptors": "^0.41.3", "@open-draft/deferred-promise": "^3.0.0", "@types/statuses": "^2.0.6", "cookie": "^1.1.1", "graphql": "^16.13.2", "headers-polyfill": "^5.0.1", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", "rettime": "^0.11.11", "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.1", "type-fest": "^5.5.0", "until-async": "^3.0.2", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": ">= 4.8.x" }, "optionalPeers": ["typescript"], "bin": { "msw": "cli/index.js" } }, "sha512-ALe+N10S72cyx94cMcy3Zs4HhXCj35sgeAL4c+WTvKi0zWnbd8/h0lcFqv0mb2P+aSgAdD7p9HzvA0DiUPxsyg=="],
@@ -1769,9 +1872,11 @@
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
+ "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
+
"nf3": ["nf3@0.3.17", "", {}, "sha512-N9zEWySuJFw+gR0lhS5863YsvNeudOdqRyFvNb+jMXbeTJOdrjDqkCpDginIZfUm0LzT1t1nCRiDeqQm/8kirQ=="],
- "nitro": ["nitro-nightly@3.0.260522-beta", "", { "dependencies": { "consola": "^3.4.2", "crossws": "^0.4.5", "db0": "^0.3.4", "env-runner": "^0.1.9", "h3": "^2.0.1-rc.22", "hookable": "^6.1.1", "nf3": "^0.3.17", "ocache": "^0.1.4", "ofetch": "^2.0.0-alpha.3", "ohash": "^2.0.11", "rolldown": "^1.0.2", "srvx": "^0.11.15", "unenv": "^2.0.0-rc.24", "unstorage": "^2.0.0-alpha.7" }, "peerDependencies": { "@vercel/queue": "^0.2.0", "dotenv": "*", "giget": "*", "jiti": "^2.6.1", "rollup": "^4.60.3", "vite": "^7 || ^8", "xml2js": "^0.6.2", "zephyr-agent": "^0.2.0" }, "optionalPeers": ["@vercel/queue", "dotenv", "giget", "jiti", "rollup", "vite", "xml2js", "zephyr-agent"], "bin": { "nitro": "dist/cli/index.mjs" } }, "sha512-Pm0AiQ1nLcreUFZKJcmVI4l9K/ygXoFamIPhc44XJHj9vmt25Rlqjv55frw6sS0L1qHrySpo3xyVVBmR9aTBqA=="],
+ "nitro": ["nitro@3.0.260522-beta", "", { "dependencies": { "consola": "^3.4.2", "crossws": "^0.4.5", "db0": "^0.3.4", "env-runner": "^0.1.9", "h3": "^2.0.1-rc.22", "hookable": "^6.1.1", "nf3": "^0.3.17", "ocache": "^0.1.4", "ofetch": "^2.0.0-alpha.3", "ohash": "^2.0.11", "rolldown": "^1.0.2", "srvx": "^0.11.15", "unenv": "^2.0.0-rc.24", "unstorage": "^2.0.0-alpha.7" }, "peerDependencies": { "@vercel/queue": "^0.2.0", "dotenv": "*", "giget": "*", "jiti": "^2.6.1", "rollup": "^4.60.3", "vite": "^7 || ^8", "xml2js": "^0.6.2", "zephyr-agent": "^0.2.0" }, "optionalPeers": ["@vercel/queue", "dotenv", "giget", "jiti", "rollup", "vite", "xml2js", "zephyr-agent"], "bin": { "nitro": "dist/cli/index.mjs" } }, "sha512-L/z2eOWgkiQHc65kv+SEMgau505afSRF7NJlbooaaZEZscFrNSD7rXZzeVubQlgIzPbhOG8o73bk9soIiGTHRA=="],
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
@@ -1779,7 +1884,7 @@
"node-forge": ["node-forge@1.4.0", "", {}, "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ=="],
- "node-releases": ["node-releases@2.0.45", "", {}, "sha512-iIbHXV9eBB2nB0wa7oTsrrXq+qQt+9SIlx9AX3T96YgobtEQfis5n6TJ6vV+3QP8DwdriEAcGhARaFCu37peBg=="],
+ "node-releases": ["node-releases@2.0.46", "", {}, "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ=="],
"node-rsa": ["node-rsa@1.1.1", "", { "dependencies": { "asn1": "^0.2.4" } }, "sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw=="],
@@ -1933,6 +2038,10 @@
"react-dom": ["react-dom@19.2.6", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.6" } }, "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g=="],
+ "react-icons": ["react-icons@5.6.0", "", { "peerDependencies": { "react": "*" } }, "sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA=="],
+
+ "react-intersection-observer": ["react-intersection-observer@10.0.3", "", { "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["react-dom"] }, "sha512-luICLMbs0zxTO/70Zy7K5jOXkABPEVSAF8T3FdZUlctsrIaPLmx8TZe2SSA+CY2HGWfz2INyNTnp82pxNNsShA=="],
+
"react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
"react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="],
@@ -1995,13 +2104,13 @@
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
- "shadcn": ["shadcn@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/plugin-transform-typescript": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@dotenvx/dotenvx": "^1.48.4", "@modelcontextprotocol/sdk": "^1.26.0", "@types/validate-npm-package-name": "^4.0.2", "browserslist": "^4.26.2", "commander": "^14.0.0", "cosmiconfig": "^9.0.0", "dedent": "^1.6.0", "deepmerge": "^4.3.1", "diff": "^8.0.2", "execa": "^9.6.0", "fast-glob": "^3.3.3", "fs-extra": "^11.3.1", "fuzzysort": "^3.1.0", "https-proxy-agent": "^7.0.6", "kleur": "^4.1.5", "msw": "^2.10.4", "node-fetch": "^3.3.2", "open": "^11.0.0", "ora": "^8.2.0", "postcss": "^8.5.6", "postcss-selector-parser": "^7.1.0", "prompts": "^2.4.2", "recast": "^0.23.11", "stringify-object": "^5.0.0", "tailwind-merge": "^3.0.1", "ts-morph": "^26.0.0", "tsconfig-paths": "^4.2.0", "validate-npm-package-name": "^7.0.1", "zod": "^3.24.1", "zod-to-json-schema": "^3.24.6" }, "bin": { "shadcn": "dist/index.js" } }, "sha512-70fwnesNrY1GgeD7Kdzn+3SsYeyfibm8immsA5L68+OusoPTvYF01oWExl8/latKpMpvVXcbgdbbE6VFBJQ38w=="],
+ "shadcn": ["shadcn@4.8.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/plugin-transform-typescript": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@dotenvx/dotenvx": "^1.48.4", "@modelcontextprotocol/sdk": "^1.26.0", "@types/validate-npm-package-name": "^4.0.2", "browserslist": "^4.26.2", "commander": "^14.0.0", "cosmiconfig": "^9.0.0", "dedent": "^1.6.0", "deepmerge": "^4.3.1", "diff": "^8.0.2", "execa": "^9.6.0", "fast-glob": "^3.3.3", "fs-extra": "^11.3.1", "fuzzysort": "^3.1.0", "https-proxy-agent": "^7.0.6", "kleur": "^4.1.5", "msw": "^2.10.4", "node-fetch": "^3.3.2", "open": "^11.0.0", "ora": "^8.2.0", "postcss": "^8.5.6", "postcss-selector-parser": "^7.1.0", "prompts": "^2.4.2", "recast": "^0.23.11", "stringify-object": "^5.0.0", "tailwind-merge": "^3.0.1", "ts-morph": "^26.0.0", "tsconfig-paths": "^4.2.0", "validate-npm-package-name": "^7.0.1", "zod": "^3.24.1", "zod-to-json-schema": "^3.24.6" }, "bin": { "shadcn": "dist/index.js" } }, "sha512-LAm3I/1FdoU/zu5GVG8Hbna4X9zlzEG5TeeCPXqsopkjvGk8QUF9OFhqeRN8oM6Oh/ynUI/yQHZxQAO3Ymcqsg=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
- "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],
+ "shell-quote": ["shell-quote@1.8.4", "", {}, "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ=="],
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
@@ -2019,6 +2128,8 @@
"solid-js": ["solid-js@1.9.13", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-6hJeJMOcEX8ktqjpDoJZEmld3ijvcvWBDtiXBm7f4332SiFN66QeAQI1REQshvyUoISsSeJ4PHDauKYbwao9JQ=="],
+ "sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="],
+
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
@@ -2029,7 +2140,7 @@
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
- "srvx": ["srvx@0.11.15", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-iXsux0UcOjdvs0LCMa2Ws3WwcDUozA3JN3BquNXkaFPP7TpRqgunKdEgoZ/uwb1J6xaYHfxtz9Twlh6yzwM6Tg=="],
+ "srvx": ["srvx@0.11.16", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-bp07zRuycfTY43IjAvvTFnmnJi8ikW0VFiHwOhhYcVW/L4xQ1XY4PAd4Nuum1rsA17C39zL7x+CDhrn5AL32Rw=="],
"stable-hash-x": ["stable-hash-x@0.2.0", "", {}, "sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ=="],
@@ -2055,12 +2166,8 @@
"strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="],
- "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
-
"strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="],
- "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
-
"svix": ["svix@1.92.2", "", { "dependencies": { "standardwebhooks": "1.0.0" } }, "sha512-ZmuA3UVvlnF9EgxlzmPtF7CKjQb64Z6OFlyfdDfU0sdcC7dJa+3aOYX5B9mA+RS6ch1AxBa4UP/l6KmqfGtWBQ=="],
"symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="],
@@ -2077,7 +2184,7 @@
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
- "tinyexec": ["tinyexec@1.1.2", "", {}, "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA=="],
+ "tinyexec": ["tinyexec@1.2.2", "", {}, "sha512-M/Q0B2cp4K7kynaT/vnED1j8TlLY+Pp7C6Wl2bl/7u/F0mUVwdyOpwomQb8JpYLitHUssAJRmLZdMCGsrx7i+g=="],
"tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
@@ -2163,6 +2270,8 @@
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
+ "vaul": ["vaul@1.1.2", "", { "dependencies": { "@radix-ui/react-dialog": "^1.1.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA=="],
+
"vite": ["vite@8.0.14", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", "rolldown": "1.0.2", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw=="],
"vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="],
@@ -2199,7 +2308,7 @@
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
- "ws": ["ws@8.20.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w=="],
+ "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
"wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="],
@@ -2251,8 +2360,6 @@
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
- "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
-
"@modelcontextprotocol/sdk/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
"@mswjs/interceptors/@open-draft/deferred-promise": ["@open-draft/deferred-promise@2.2.0", "", {}, "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA=="],
@@ -2273,19 +2380,13 @@
"@tailwindcss/typography/postcss-selector-parser": ["postcss-selector-parser@6.0.10", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="],
- "@tanstack/react-query/@tanstack/query-core": ["@tanstack/query-core@5.100.14", "", {}, "sha512-5X41dGpxgeaHISCRW2oYwcSycZeULZzAunaudXT9ov1KOTj9xwt0CH6hbwqP1/z74ZWF7rYFnDpyYH07XFcZew=="],
-
"@tanstack/start-plugin-core/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
"@tanstack/start-plugin-core/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
- "@ts-morph/common/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
-
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
- "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
-
- "@typescript-eslint/typescript-estree/semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="],
+ "@typescript-eslint/typescript-estree/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="],
"@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
@@ -2311,21 +2412,21 @@
"encoding-sniffer/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
- "eslint/@eslint/js": ["@eslint/js@9.39.4", "", {}, "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw=="],
-
- "eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
+ "enquirer/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"eslint/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
- "eslint-compat-utils/semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="],
+ "eslint/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
+
+ "eslint/espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="],
- "eslint-plugin-import-x/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
+ "eslint-compat-utils/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="],
- "eslint-plugin-import-x/semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="],
+ "eslint-plugin-import-x/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="],
"eslint-plugin-n/globals": ["globals@15.15.0", "", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="],
- "eslint-plugin-n/semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="],
+ "eslint-plugin-n/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="],
"express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
@@ -2361,7 +2462,7 @@
"strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
- "tough-cookie/tldts": ["tldts@7.0.30", "", { "dependencies": { "tldts-core": "^7.0.30" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw=="],
+ "tough-cookie/tldts": ["tldts@7.1.1", "", { "dependencies": { "tldts-core": "^7.1.1" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-VuvOq9QVVdzQyIwynB0MRZlEup+u5BD62FjgmKvRDFO8u1RgAzpeg7Qd70hUmrxwkkecqoz1N6t1yGMygx7rnA=="],
"tsx/esbuild": ["esbuild@0.28.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.0", "@esbuild/android-arm": "0.28.0", "@esbuild/android-arm64": "0.28.0", "@esbuild/android-x64": "0.28.0", "@esbuild/darwin-arm64": "0.28.0", "@esbuild/darwin-x64": "0.28.0", "@esbuild/freebsd-arm64": "0.28.0", "@esbuild/freebsd-x64": "0.28.0", "@esbuild/linux-arm": "0.28.0", "@esbuild/linux-arm64": "0.28.0", "@esbuild/linux-ia32": "0.28.0", "@esbuild/linux-loong64": "0.28.0", "@esbuild/linux-mips64el": "0.28.0", "@esbuild/linux-ppc64": "0.28.0", "@esbuild/linux-riscv64": "0.28.0", "@esbuild/linux-s390x": "0.28.0", "@esbuild/linux-x64": "0.28.0", "@esbuild/netbsd-arm64": "0.28.0", "@esbuild/netbsd-x64": "0.28.0", "@esbuild/openbsd-arm64": "0.28.0", "@esbuild/openbsd-x64": "0.28.0", "@esbuild/openharmony-arm64": "0.28.0", "@esbuild/sunos-x64": "0.28.0", "@esbuild/win32-arm64": "0.28.0", "@esbuild/win32-ia32": "0.28.0", "@esbuild/win32-x64": "0.28.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw=="],
@@ -2369,7 +2470,9 @@
"vue-eslint-parser/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
- "vue-eslint-parser/semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="],
+ "vue-eslint-parser/espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="],
+
+ "vue-eslint-parser/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="],
"whatwg-encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
@@ -2441,10 +2544,6 @@
"@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
- "@ts-morph/common/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],
-
- "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],
-
"ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"cheerio/htmlparser2/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
@@ -2455,15 +2554,11 @@
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
- "eslint-plugin-import-x/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],
-
- "eslint/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
-
"parse5-htmlparser2-tree-adapter/parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
"parse5-parser-stream/parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
- "tough-cookie/tldts/tldts-core": ["tldts-core@7.0.30", "", {}, "sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q=="],
+ "tough-cookie/tldts/tldts-core": ["tldts-core@7.1.1", "", {}, "sha512-v9zYcyFEAJBeyG7g4+y/HFL9i2cHqpV+9cHohNZIhA6xjO2MSVgijFgx6quQaRBDzM5FT8fs5NPjsNITOhlCzg=="],
"tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA=="],
@@ -2522,11 +2617,5 @@
"yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
-
- "@ts-morph/common/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
-
- "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
-
- "eslint-plugin-import-x/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
}
}
diff --git a/opencode.json b/opencode.json
new file mode 100644
index 0000000..284ef66
--- /dev/null
+++ b/opencode.json
@@ -0,0 +1,10 @@
+{
+ "$schema": "https://opencode.ai/config.json",
+ "mcp": {
+ "better-auth": {
+ "type": "remote",
+ "url": "https://mcp.better-auth.com/mcp",
+ "enabled": true
+ }
+ }
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index 5a8494f..6a28538 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,7 @@
],
"catalog": {
"@better-auth/infra": "^0.2.8",
+ "@bprogress/react": "^1.2.7",
"@fontsource-variable/inter": "^5.2.8",
"@fontsource-variable/outfit": "^5.2.8",
"@orpc/client": "^1.14.3",
@@ -31,12 +32,12 @@
"@tanstack/eslint-plugin-router": "^1.162.0",
"@tanstack/react-devtools": "latest",
"@tanstack/react-form": "latest",
- "@tanstack/react-query": "latest",
- "@tanstack/react-query-devtools": "latest",
- "@tanstack/react-router": "latest",
- "@tanstack/react-router-devtools": "latest",
- "@tanstack/react-router-ssr-query": "latest",
- "@tanstack/react-start": "latest",
+ "@tanstack/react-query": "^5.100.14",
+ "@tanstack/react-query-devtools": "^5.100.14",
+ "@tanstack/react-router": "^1.170.8",
+ "@tanstack/react-router-devtools": "^1.167.0",
+ "@tanstack/react-router-ssr-query": "^1.167.0",
+ "@tanstack/react-start": "^1.168.9",
"@tanstack/router-plugin": "^1.132.0",
"@testing-library/dom": "^10.4.1",
"@testing-library/react": "^16.3.0",
@@ -51,13 +52,19 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dotenv": "^17.4.2",
+ "date-fns": "^4.3.0",
+ "@dicebear/collection": "^9.4.2",
+ "@dicebear/core": "^9.4.2",
"drizzle-kit": "^0.31.10",
"drizzle-orm": "^0.45.2",
"drizzle-zod": "^0.8.3",
"jsdom": "^28.1.0",
"limax": "^4.2.3",
"lucide-react": "^0.545.0",
+ "motion": "^12.40.0",
"nanoid": "^5.1.11",
+ "next-themes": "^0.4.6",
+ "nitro": "^3.0.260522-beta",
"oxfmt": "^0.51.0",
"oxlint": "^1.66.0",
"oxlint-tsgolint": "^0.23.0",
@@ -65,8 +72,11 @@
"radix-ui": "^1.4.3",
"react": "^19.2.0",
"react-dom": "^19.2.0",
+ "react-icons": "^5.6.0",
+ "react-intersection-observer": "^10.0.3",
"resend": "^6.12.3",
"shadcn": "^4.7.0",
+ "sonner": "^2.0.7",
"tailwind-merge": "^3.6.0",
"tailwindcss": "^4.1.18",
"turbo": "^2.9.14",
diff --git a/packages/api/src/routers/threads.router.ts b/packages/api/src/routers/threads.router.ts
index 4afbbc4..494f5bc 100644
--- a/packages/api/src/routers/threads.router.ts
+++ b/packages/api/src/routers/threads.router.ts
@@ -16,6 +16,11 @@ import { CursorError, generateSlug } from '@trid/shared/utils'
const MAX_SLUG_ATTEMPTS = 3
+type SlugRetryErrors = {
+ CONFLICT: (args: { message: string }) => Error
+ INTERNAL_SERVER_ERROR: (args: { message: string }) => Error
+}
+
const createThreadHandler = protectedProcedure.threads.create.handler(
async ({ context, errors, input }) => {
const { user } = context.auth
@@ -172,10 +177,7 @@ export const orpcThreadsRouter = {
async function createWithSlugRetry(
fn: (attempt: number) => Promise>,
- errors: {
- CONFLICT: (args: { message: string }) => unknown
- INTERNAL_SERVER_ERROR: (args: { message: string }) => unknown
- },
+ errors: SlugRetryErrors,
attempts: number,
currentAttempt = 0
): Promise {
@@ -196,7 +198,11 @@ async function createWithSlugRetry(
return result
} catch (error) {
- if (isUniqueViolation(error)) {
+ if (
+ typeof error === 'object' &&
+ error !== null &&
+ isUniqueViolation(error)
+ ) {
return createWithSlugRetry(fn, errors, attempts, currentAttempt + 1)
}
@@ -216,15 +222,14 @@ function mapOwnThread(
username: user.username,
image: user.image,
},
- uservote: null,
+ userVote: null,
}
}
-function isUniqueViolation(error: unknown) {
- return (
- typeof error === 'object' &&
- error !== null &&
- 'code' in error &&
- error.code === '23505'
- )
+function isUniqueViolation(error: object) {
+ return hasStringCode(error) && error.code === '23505'
+}
+
+function hasStringCode(error: object): error is { code: string } {
+ return 'code' in error && typeof error.code === 'string'
}
diff --git a/packages/api/src/routers/votes.router.ts b/packages/api/src/routers/votes.router.ts
index 2f9f132..25ebb42 100644
--- a/packages/api/src/routers/votes.router.ts
+++ b/packages/api/src/routers/votes.router.ts
@@ -3,7 +3,10 @@ import { protectedProcedure } from '../middlewares'
import { VoteError, voteReplyQuery, voteThreadQuery } from '@trid/db/queries'
import type { VoteErrorCode } from '@trid/db/queries'
-const voteErrorToStatus: Record = {
+const voteErrorToStatus: Record<
+ VoteErrorCode,
+ 'NOT_FOUND' | 'BAD_REQUEST' | 'INTERNAL_SERVER_ERROR'
+> = {
THREAD_NOT_FOUND: 'NOT_FOUND',
REPLY_NOT_FOUND: 'NOT_FOUND',
DELETED_REPLY: 'BAD_REQUEST',
diff --git a/packages/auth/src/index.ts b/packages/auth/src/index.ts
index e56b046..cc4f3d3 100644
--- a/packages/auth/src/index.ts
+++ b/packages/auth/src/index.ts
@@ -12,7 +12,9 @@ import { Resend } from 'resend'
import { db } from '@trid/db'
import * as schema from '@trid/db/schemas'
+
import { env } from '@trid/env/server'
+
import { PasswordResetEmail } from '@trid/ui/components/email/email-reset-password'
const resend = new Resend(env.RESEND_API_KEY)
@@ -92,6 +94,7 @@ export function createAuth() {
unique: true,
input: true,
fieldName: 'username',
+ defaultValue: `u_${crypto.randomUUID().slice(0, 8)}`,
},
banner: {
type: 'string',
@@ -123,6 +126,13 @@ export function createAuth() {
required: true,
input: false,
},
+ onBoardingCompleted: {
+ type: 'boolean',
+ defaultValue: false,
+ required: true,
+ input: true,
+ fieldName: 'onBoardingCompleted',
+ },
},
},
})
diff --git a/packages/db/src/queries/reply.query.ts b/packages/db/src/queries/reply.query.ts
index 7481df0..9dbadf3 100644
--- a/packages/db/src/queries/reply.query.ts
+++ b/packages/db/src/queries/reply.query.ts
@@ -36,6 +36,16 @@ const PUBLISHED_STATUS = 'PUBLISHED'
const LIST_LIMIT_EXTRA = 1
const CHILD_REPLY_PREVIEW_LIMIT = 2
+type ReplyListCursorItem = {
+ id: string
+ createdAt: Date
+ votesScoresCount: number
+}
+
+type RowWithNullableVote = {
+ userVote: TVote | null
+}
+
type ReplyRow = typeof repliesTable.$inferSelect
type ReplyListItem = Awaited>[number]
@@ -650,27 +660,22 @@ function getListCursorFilter(
function getListCursorValues(
sort: ReplySort,
- item: unknown
+ item: ReplyListCursorItem | undefined
): CursorValueSchemaWithKey {
if (!item) throw new CursorError()
- const reply = item as {
- id: string
- createdAt: Date
- votesScoresCount: number
- }
switch (sort) {
case 'top':
return {
- votesScoresCount: reply.votesScoresCount,
- createdAt: reply.createdAt.toISOString(),
- id: reply.id,
+ votesScoresCount: item.votesScoresCount,
+ createdAt: item.createdAt.toISOString(),
+ id: item.id,
}
case 'latest':
case 'oldest':
return {
- createdAt: reply.createdAt.toISOString(),
- id: reply.id,
+ createdAt: item.createdAt.toISOString(),
+ id: item.id,
}
}
}
@@ -683,11 +688,13 @@ function childRepliesCursorScope(parentReplyId: string, sort: ReplySort) {
return `replies:list:parent:${parentReplyId}:${sort}`
}
-function normalizeRows(rows: Array) {
+function normalizeRows>(
+ rows: Array
+) {
return rows.map((row) => normalizeRow(row))
}
-function normalizeRow(row: T) {
+function normalizeRow>(row: T) {
return {
...row,
userVote: row.userVote ?? null,
diff --git a/packages/db/src/queries/thread.query.ts b/packages/db/src/queries/thread.query.ts
index ea4a688..4c04987 100644
--- a/packages/db/src/queries/thread.query.ts
+++ b/packages/db/src/queries/thread.query.ts
@@ -19,6 +19,24 @@ import type { CursorValueSchemaWithKey } from '@trid/shared/utils'
const PUBLISHED_STATUS = 'PUBLISHED'
const LIST_LIMIT_EXTRA = 1
+type ThreadListCursorItem = {
+ id: string
+ createdAt: Date
+ lastActivityAt: Date
+ votesScoresCount: number
+ repliesCount: number
+}
+
+type ThreadTopCursorItem = {
+ id: string
+ createdAt: Date
+ rankingScore: number
+}
+
+type RowWithNullableVote = {
+ userVote: TVote | null
+}
+
export type CreateThreadValues = {
authorId: string
title: string
@@ -69,7 +87,7 @@ export async function listThreadsQuery(
.select({
...threadListSelection,
author: authorSelection,
- uservote: viewerVotesTable.direction,
+ userVote: viewerVotesTable.direction,
})
.from(threadsTable)
.innerJoin(userTable, eq(userTable.id, threadsTable.authorId))
@@ -119,7 +137,7 @@ export async function topThreadsQuery(
.select({
...threadListSelection,
author: authorSelection,
- uservote: viewerVotesTable.direction,
+ userVote: viewerVotesTable.direction,
rankingScore,
})
.from(threadsTable)
@@ -157,7 +175,7 @@ export async function topThreadsQuery(
.select({
...threadListSelection,
author: authorSelection,
- uservote: viewerVotesTable.direction,
+ userVote: viewerVotesTable.direction,
rankingScore,
})
.from(threadsTable)
@@ -211,7 +229,7 @@ export async function getThreadBySlugQuery(
.select({
...threadDetailSelection,
author: authorSelection,
- uservote: viewerVotesTable.direction,
+ userVote: viewerVotesTable.direction,
})
.from(threadsTable)
.innerJoin(userTable, eq(userTable.id, threadsTable.authorId))
@@ -439,52 +457,42 @@ function getTopCursorFilter(
function getListCursorValues(
feed: ThreadFeed,
- item: unknown
+ item: ThreadListCursorItem | undefined
): CursorValueSchemaWithKey {
if (!item) throw new CursorError()
- const thread = item as {
- id: string
- createdAt: Date
- lastActivityAt: Date
- votesScoresCount: number
- repliesCount: number
- }
switch (feed) {
case 'discover':
return {
- lastActivityAt: thread.lastActivityAt.toISOString(),
- votesScoresCount: thread.votesScoresCount,
- createdAt: thread.createdAt.toISOString(),
- id: thread.id,
+ lastActivityAt: item.lastActivityAt.toISOString(),
+ votesScoresCount: item.votesScoresCount,
+ createdAt: item.createdAt.toISOString(),
+ id: item.id,
}
case 'popular':
return {
- votesScoresCount: thread.votesScoresCount,
- repliesCount: thread.repliesCount,
- createdAt: thread.createdAt.toISOString(),
- id: thread.id,
+ votesScoresCount: item.votesScoresCount,
+ repliesCount: item.repliesCount,
+ createdAt: item.createdAt.toISOString(),
+ id: item.id,
}
case 'latest':
return {
- createdAt: thread.createdAt.toISOString(),
- id: thread.id,
+ createdAt: item.createdAt.toISOString(),
+ id: item.id,
}
}
}
-function getTopCursorValues(item: unknown): CursorValueSchemaWithKey {
+function getTopCursorValues(
+ item: ThreadTopCursorItem | undefined
+): CursorValueSchemaWithKey {
if (!item) throw new CursorError()
- const thread = item as {
- id: string
- createdAt: Date
- rankingScore: number
- }
return {
- rankingScore: thread.rankingScore,
- createdAt: thread.createdAt.toISOString(),
- id: thread.id,
+ rankingScore: item.rankingScore,
+ createdAt: item.createdAt.toISOString(),
+ id: item.id,
}
}
@@ -517,13 +525,15 @@ function getPeriodStart(
}
}
-function normalizeRows(rows: Array) {
+function normalizeRows>(
+ rows: Array
+) {
return rows.map((row) => normalizeRow(row))
}
-function normalizeRow(row: T) {
+function normalizeRow>(row: T) {
return {
...row,
- uservote: row.uservote ?? null,
+ userVote: row.userVote ?? null,
}
}
diff --git a/packages/db/src/queries/vote.query.ts b/packages/db/src/queries/vote.query.ts
index 3bb6575..cc2c112 100644
--- a/packages/db/src/queries/vote.query.ts
+++ b/packages/db/src/queries/vote.query.ts
@@ -6,7 +6,6 @@ import { and, eq, sql } from 'drizzle-orm'
import type { Database } from '@trid/db'
import type {
- VoteAction,
VoteDirectionNullable,
VoteReplyInput,
VoteReplyOutput,
@@ -38,7 +37,6 @@ export const voteThreadQuery = (
return db.transaction(async (tx) => {
const { slug, direction } = input
- let voteAction: VoteAction
let voteDirection: VoteDirectionNullable
let voteScoreCount: number
@@ -75,7 +73,6 @@ export const voteThreadQuery = (
direction,
})
- voteAction = 'VOTED'
voteDirection = direction
voteScoreCount = direction === 'UPVOTE' ? 1 : -1
} else if (existingVote.direction === direction) {
@@ -88,7 +85,6 @@ export const voteThreadQuery = (
)
)
- voteAction = 'UNVOTED'
voteDirection = null
voteScoreCount = direction === 'UPVOTE' ? -1 : 1
} else {
@@ -104,7 +100,6 @@ export const voteThreadQuery = (
)
)
- voteAction = 'CHANGED'
voteDirection = direction
voteScoreCount = direction === 'UPVOTE' ? 2 : -2
}
@@ -125,9 +120,8 @@ export const voteThreadQuery = (
}
return {
- voteAction,
- voteDirection,
- voteScoreCount: updateThread.votesScoresCount,
+ userVote: voteDirection,
+ votesScoresCount: updateThread.votesScoresCount,
}
})
}
@@ -140,7 +134,6 @@ export const voteReplyQuery = (
return db.transaction(async (tx) => {
const { id, direction } = input
- let voteAction: VoteAction
let voteDirection: VoteDirectionNullable
let voteScoreCount: number
@@ -182,7 +175,6 @@ export const voteReplyQuery = (
direction,
})
- voteAction = 'VOTED'
voteDirection = direction
voteScoreCount = direction === 'UPVOTE' ? 1 : -1
} else if (existingVote.direction === direction) {
@@ -195,7 +187,6 @@ export const voteReplyQuery = (
)
)
- voteAction = 'UNVOTED'
voteDirection = null
voteScoreCount = direction === 'UPVOTE' ? -1 : 1
} else {
@@ -211,7 +202,6 @@ export const voteReplyQuery = (
)
)
- voteAction = 'CHANGED'
voteDirection = direction
voteScoreCount = direction === 'UPVOTE' ? 2 : -2
}
@@ -232,9 +222,8 @@ export const voteReplyQuery = (
}
return {
- voteAction,
- voteDirection,
- voteScoreCount: updateThread.votesScoresCount,
+ userVote: voteDirection,
+ votesScoresCount: updateThread.votesScoresCount,
}
})
}
diff --git a/packages/db/src/schemas/auth.ts b/packages/db/src/schemas/auth.ts
index 97370cf..21a4404 100644
--- a/packages/db/src/schemas/auth.ts
+++ b/packages/db/src/schemas/auth.ts
@@ -24,6 +24,7 @@ export const userTable = pgTable('user', {
location: text('location'),
verified: boolean('verified').default(false).notNull(),
role: text('role'),
+ onBoardingCompleted: boolean('onboarding_completed').default(false).notNull(),
banned: boolean('banned').default(false),
banReason: text('ban_reason'),
banExpires: timestamp('ban_expires'),
diff --git a/packages/shared/src/constants/index.ts b/packages/shared/src/constants/index.ts
index aee59c8..d65009d 100644
--- a/packages/shared/src/constants/index.ts
+++ b/packages/shared/src/constants/index.ts
@@ -15,3 +15,7 @@ export type VoteDirection = (typeof VOTE_DIRECTIONS)[number]
export type ThreadFeed = (typeof THREAD_FEEDS)[number]
export type ThreadTopPeriod = (typeof THREAD_TOP_PERIODS)[number]
export type ReplySort = (typeof REPLY_SORTS)[number]
+
+export const THREADS_DEFAULT_LIMIT = 10
+export const THREADS_MAX_LIMIT = 50
+export const THREADS_MIN_LIMIT = 1
diff --git a/packages/shared/src/schemas/threads.test.ts b/packages/shared/src/schemas/threads.test.ts
index 92b25e3..270863e 100644
--- a/packages/shared/src/schemas/threads.test.ts
+++ b/packages/shared/src/schemas/threads.test.ts
@@ -2,13 +2,19 @@
import {
createThreadInputSchema,
+ threadDetailOutputSchema,
listThreadsInputSchema,
topThreadsInputSchema,
updateThreadInputSchema,
} from './threads'
import { describe, expect, test } from 'bun:test'
+import { THREADS_DEFAULT_LIMIT } from '@trid/shared/constants'
+
describe('thread schemas', () => {
+ const THREAD_ID = '7c5f6dfa-21f9-4498-a30e-07d960297ad0'
+ const USER_ID = '645128a8-4bf4-46ed-bb70-4f7a6f746393'
+
test('validates create thread input', () => {
const input = createThreadInputSchema.parse({
title: ' Hello world ',
@@ -24,35 +30,90 @@ describe('thread schemas', () => {
test('applies list defaults', () => {
expect(listThreadsInputSchema.parse({})).toEqual({
feed: 'discover',
- limit: 20,
+ limit: THREADS_DEFAULT_LIMIT,
})
})
test('applies top defaults', () => {
expect(topThreadsInputSchema.parse({})).toEqual({
period: 'week',
- limit: 20,
+ limit: THREADS_DEFAULT_LIMIT,
})
})
test('rejects empty update payload', () => {
expect(() =>
updateThreadInputSchema.parse({
- id: '7c5f6dfa-21f9-4498-a30e-07d960297ad0',
+ slug: 'hello-world',
})
).toThrow()
})
- test('does not accept slug updates', () => {
+ test('does not accept id updates', () => {
const input = updateThreadInputSchema.parse({
- id: '7c5f6dfa-21f9-4498-a30e-07d960297ad0',
+ slug: 'hello-world',
title: 'Renamed thread',
- slug: 'renamed-thread',
+ id: THREAD_ID,
})
expect(input).toEqual({
- id: '7c5f6dfa-21f9-4498-a30e-07d960297ad0',
+ slug: 'hello-world',
title: 'Renamed thread',
})
})
+
+ test('accepts thread output with userVote', () => {
+ const now = new Date()
+ const thread = threadDetailOutputSchema.parse({
+ id: THREAD_ID,
+ authorId: USER_ID,
+ title: 'Hello world',
+ slug: 'hello-world',
+ status: 'PUBLISHED',
+ votesScoresCount: 1,
+ repliesCount: 0,
+ lastActivityAt: now,
+ createdAt: now,
+ updatedAt: now,
+ deletedAt: null,
+ content: 'First post',
+ author: {
+ id: USER_ID,
+ name: 'Jane',
+ username: 'jane',
+ image: null,
+ },
+ userVote: 'UPVOTE',
+ })
+
+ expect(thread.userVote).toBe('UPVOTE')
+ })
+
+ test('rejects old uservote output field', () => {
+ const now = new Date()
+
+ expect(() =>
+ threadDetailOutputSchema.parse({
+ id: THREAD_ID,
+ authorId: USER_ID,
+ title: 'Hello world',
+ slug: 'hello-world',
+ status: 'PUBLISHED',
+ votesScoresCount: 1,
+ repliesCount: 0,
+ lastActivityAt: now,
+ createdAt: now,
+ updatedAt: now,
+ deletedAt: null,
+ content: 'First post',
+ author: {
+ id: USER_ID,
+ name: 'Jane',
+ username: 'jane',
+ image: null,
+ },
+ uservote: 'UPVOTE',
+ })
+ ).toThrow()
+ })
})
diff --git a/packages/shared/src/schemas/threads.ts b/packages/shared/src/schemas/threads.ts
index 6e562d4..8fe897a 100644
--- a/packages/shared/src/schemas/threads.ts
+++ b/packages/shared/src/schemas/threads.ts
@@ -4,6 +4,9 @@ import {
THREAD_FEEDS,
THREAD_STATUSES,
THREAD_TOP_PERIODS,
+ THREADS_DEFAULT_LIMIT,
+ THREADS_MAX_LIMIT,
+ THREADS_MIN_LIMIT,
} from '@trid/shared/constants'
import { publicUserOutputSchema } from '@trid/shared/schemas/user'
import { voteDirectionNullableSchema } from '@trid/shared/schemas/votes'
@@ -20,7 +23,12 @@ const contentSchema = z
.trim()
.min(1, { message: 'Content is required' })
const cursorSchema = z.string().optional()
-const limitSchema = z.coerce.number().int().min(1).max(50).default(20)
+const limitSchema = z.coerce
+ .number()
+ .int()
+ .min(THREADS_MIN_LIMIT)
+ .max(THREADS_MAX_LIMIT)
+ .default(THREADS_DEFAULT_LIMIT)
export const threadStatusSchema = z.enum(THREAD_STATUSES)
export const threadFeedSchema = z.enum(THREAD_FEEDS)
@@ -75,10 +83,12 @@ const threadBaseOutputSchema = z.object({
deletedAt: z.date().nullable(),
})
-export const threadListItemOutputSchema = threadBaseOutputSchema.extend({
- author: publicUserOutputSchema,
- uservote: voteDirectionNullableSchema,
-})
+export const threadListItemOutputSchema = threadBaseOutputSchema
+ .extend({
+ author: publicUserOutputSchema,
+ userVote: voteDirectionNullableSchema,
+ })
+ .strict()
export const topThreadListItemOutputSchema = threadListItemOutputSchema.extend({
rankingScore: z.number().int(),
diff --git a/packages/shared/src/schemas/votes.ts b/packages/shared/src/schemas/votes.ts
index 3233dac..0a78d80 100644
--- a/packages/shared/src/schemas/votes.ts
+++ b/packages/shared/src/schemas/votes.ts
@@ -1,12 +1,11 @@
import { z } from 'zod'
-import { VOTE_ACTIONS, VOTE_DIRECTIONS } from '@trid/shared/constants'
+import { VOTE_DIRECTIONS } from '@trid/shared/constants'
export const voteDirectionSchema = z.enum(VOTE_DIRECTIONS)
export const voteDirectionNullableSchema = voteDirectionSchema
.nullable()
.default(null)
-export const voteActionSchema = z.enum(VOTE_ACTIONS)
export const voteThreadInputSchema = z.object({
slug: z.string().min(1, { error: 'Thread slug is required' }),
@@ -14,9 +13,8 @@ export const voteThreadInputSchema = z.object({
})
export const voteThreadOutputSchema = z.object({
- voteAction: voteActionSchema,
- voteDirection: voteDirectionNullableSchema,
- voteScoreCount: z.number().int().default(0),
+ userVote: voteDirectionNullableSchema,
+ votesScoresCount: z.number().int().default(0),
})
export const voteReplyInputSchema = z.object({
@@ -25,14 +23,12 @@ export const voteReplyInputSchema = z.object({
})
export const voteReplyOutputSchema = z.object({
- voteAction: voteActionSchema,
- voteDirection: voteDirectionNullableSchema,
- voteScoreCount: z.number().int().default(0),
+ userVote: voteDirectionNullableSchema,
+ votesScoresCount: z.number().int().default(0),
})
export type VoteDirectionSchemaValue = z.infer
export type VoteDirectionNullable = z.infer
-export type VoteAction = z.infer
export type VoteThreadInput = z.infer
export type VoteThreadOutput = z.infer
diff --git a/packages/ui/components.json b/packages/ui/components.json
index 80e750d..49f1a5a 100644
--- a/packages/ui/components.json
+++ b/packages/ui/components.json
@@ -1,6 +1,6 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
- "style": "radix-lyra",
+ "style": "radix-mira",
"rsc": false,
"tsx": true,
"tailwind": {
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 45f1d24..c8a3790 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -16,10 +16,15 @@
"@tabler/icons-react": "catalog:",
"class-variance-authority": "catalog:",
"clsx": "catalog:",
+ "next-themes": "catalog:",
"radix-ui": "catalog:",
+ "react": "catalog:",
+ "react-dom": "catalog:",
"shadcn": "catalog:",
+ "sonner": "catalog:",
"tailwind-merge": "catalog:",
"tw-animate-css": "catalog:",
+ "vaul": "^1.1.2",
"zod": "catalog:"
},
"devDependencies": {
diff --git a/packages/ui/src/components/avatar.tsx b/packages/ui/src/components/avatar.tsx
new file mode 100644
index 0000000..0fb0b84
--- /dev/null
+++ b/packages/ui/src/components/avatar.tsx
@@ -0,0 +1,111 @@
+import * as React from 'react'
+
+import { Avatar as AvatarPrimitive } from 'radix-ui'
+
+import { cn } from '@trid/ui/lib/utils'
+
+function Avatar({
+ className,
+ size = 'default',
+ ...props
+}: React.ComponentProps & {
+ size?: 'default' | 'sm' | 'lg'
+}) {
+ return (
+
+ )
+}
+
+function AvatarImage({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AvatarFallback({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AvatarBadge({ className, ...props }: React.ComponentProps<'span'>) {
+ return (
+ svg]:hidden',
+ 'group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2',
+ 'group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2',
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function AvatarGroup({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+function AvatarGroupCount({
+ className,
+ ...props
+}: React.ComponentProps<'div'>) {
+ return (
+ svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3',
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+export {
+ Avatar,
+ AvatarImage,
+ AvatarFallback,
+ AvatarGroup,
+ AvatarGroupCount,
+ AvatarBadge,
+}
diff --git a/packages/ui/src/components/button-group.tsx b/packages/ui/src/components/button-group.tsx
new file mode 100644
index 0000000..34e895a
--- /dev/null
+++ b/packages/ui/src/components/button-group.tsx
@@ -0,0 +1,84 @@
+import { cva } from 'class-variance-authority'
+import type { VariantProps } from 'class-variance-authority'
+import { Slot } from 'radix-ui'
+
+import { Separator } from '@trid/ui/components/separator'
+import { cn } from '@trid/ui/lib/utils'
+
+const buttonGroupVariants = cva(
+ "group/button-group flex w-fit items-stretch *:focus-visible:relative *:focus-visible:z-10 has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
+ {
+ variants: {
+ orientation: {
+ horizontal:
+ '[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-r-md!',
+ vertical:
+ 'flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-b-md!',
+ },
+ },
+ defaultVariants: {
+ orientation: 'horizontal',
+ },
+ }
+)
+
+function ButtonGroup({
+ className,
+ orientation,
+ ...props
+}: React.ComponentProps<'div'> & VariantProps
) {
+ return (
+
+ )
+}
+
+function ButtonGroupText({
+ className,
+ asChild = false,
+ ...props
+}: React.ComponentProps<'div'> & {
+ asChild?: boolean
+}) {
+ const Comp = asChild ? Slot.Root : 'div'
+
+ return (
+
+ )
+}
+
+function ButtonGroupSeparator({
+ className,
+ orientation = 'vertical',
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export {
+ ButtonGroup,
+ ButtonGroupSeparator,
+ ButtonGroupText,
+ buttonGroupVariants,
+}
diff --git a/packages/ui/src/components/button.tsx b/packages/ui/src/components/button.tsx
index 76e755d..029d310 100644
--- a/packages/ui/src/components/button.tsx
+++ b/packages/ui/src/components/button.tsx
@@ -1,65 +1,67 @@
-import * as React from "react"
-import { cva, type VariantProps } from "class-variance-authority"
-import { Slot } from "radix-ui"
+import * as React from 'react'
-import { cn } from "@trid/ui/lib/utils"
+import { cva } from 'class-variance-authority'
+import type { VariantProps } from 'class-variance-authority'
+import { Slot } from 'radix-ui'
+
+import { cn } from '@trid/ui/lib/utils'
const buttonVariants = cva(
- "group/button inline-flex shrink-0 items-center justify-center rounded-none border border-transparent bg-clip-padding text-xs font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
- {
- variants: {
- variant: {
- default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
- outline:
- "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
- secondary:
- "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
- ghost:
- "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
- destructive:
- "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
- link: "text-primary underline-offset-4 hover:underline",
- },
- size: {
- default:
- "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
- xs: "h-6 gap-1 rounded-none px-2 text-xs has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
- sm: "h-7 gap-1 rounded-none px-2.5 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
- lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
- icon: "size-8",
- "icon-xs": "size-6 rounded-none [&_svg:not([class*='size-'])]:size-3",
- "icon-sm": "size-7 rounded-none",
- "icon-lg": "size-9",
- },
- },
- defaultVariants: {
- variant: "default",
- size: "default",
- },
- }
+ "group/button inline-flex shrink-0 items-center justify-center rounded-md border border-transparent bg-clip-padding text-xs/relaxed font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/30 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+ {
+ variants: {
+ variant: {
+ default: 'bg-primary text-primary-foreground hover:bg-primary/80',
+ outline:
+ 'border-border hover:bg-input/50 hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:bg-input/30',
+ secondary:
+ 'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
+ ghost:
+ 'hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50',
+ destructive:
+ 'bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40',
+ link: 'text-primary underline-offset-4 hover:underline',
+ },
+ size: {
+ default:
+ "h-7 gap-1 px-2 text-xs/relaxed has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
+ xs: "h-5 gap-1 rounded-sm px-2 text-[0.625rem] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-2.5",
+ sm: "h-6 gap-1 px-2 text-xs/relaxed has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
+ lg: "h-8 gap-1 px-2.5 text-xs/relaxed has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-4",
+ icon: "size-7 [&_svg:not([class*='size-'])]:size-3.5",
+ 'icon-xs': "size-5 rounded-sm [&_svg:not([class*='size-'])]:size-2.5",
+ 'icon-sm': "size-6 [&_svg:not([class*='size-'])]:size-3",
+ 'icon-lg': "size-8 [&_svg:not([class*='size-'])]:size-4",
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ size: 'default',
+ },
+ }
)
function Button({
- className,
- variant = "default",
- size = "default",
- asChild = false,
- ...props
-}: React.ComponentProps<"button"> &
- VariantProps & {
- asChild?: boolean
- }) {
- const Comp = asChild ? Slot.Root : "button"
+ className,
+ variant = 'default',
+ size = 'default',
+ asChild = false,
+ ...props
+}: React.ComponentProps<'button'> &
+ VariantProps & {
+ asChild?: boolean
+ }) {
+ const Comp = asChild ? Slot.Root : 'button'
- return (
-
- )
+ return (
+
+ )
}
export { Button, buttonVariants }
diff --git a/packages/ui/src/components/card.tsx b/packages/ui/src/components/card.tsx
new file mode 100644
index 0000000..e4f4da9
--- /dev/null
+++ b/packages/ui/src/components/card.tsx
@@ -0,0 +1,100 @@
+import * as React from 'react'
+
+import { cn } from '@trid/ui/lib/utils'
+
+function Card({
+ className,
+ size = 'default',
+ ...props
+}: React.ComponentProps<'div'> & { size?: 'default' | 'sm' }) {
+ return (
+ img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 *:[img:first-child]:rounded-t-lg *:[img:last-child]:rounded-b-lg',
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+export {
+ Card,
+ CardHeader,
+ CardFooter,
+ CardTitle,
+ CardAction,
+ CardDescription,
+ CardContent,
+}
diff --git a/packages/ui/src/components/dialog.tsx b/packages/ui/src/components/dialog.tsx
new file mode 100644
index 0000000..12dc45f
--- /dev/null
+++ b/packages/ui/src/components/dialog.tsx
@@ -0,0 +1,163 @@
+import * as React from 'react'
+
+import { IconX } from '@tabler/icons-react'
+import { Dialog as DialogPrimitive } from 'radix-ui'
+
+import { Button } from '@trid/ui/components/button'
+import { cn } from '@trid/ui/lib/utils'
+
+function Dialog({
+ ...props
+}: React.ComponentProps
) {
+ return
+}
+
+function DialogTrigger({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DialogPortal({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DialogClose({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DialogOverlay({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DialogContent({
+ className,
+ children,
+ showCloseButton = true,
+ ...props
+}: React.ComponentProps & {
+ showCloseButton?: boolean
+}) {
+ return (
+
+
+
+ {children}
+ {showCloseButton && (
+
+
+
+ )}
+
+
+ )
+}
+
+function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+function DialogFooter({
+ className,
+ showCloseButton = false,
+ children,
+ ...props
+}: React.ComponentProps<'div'> & {
+ showCloseButton?: boolean
+}) {
+ return (
+
+ {children}
+ {showCloseButton && (
+
+
+
+ )}
+
+ )
+}
+
+function DialogTitle({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DialogDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogOverlay,
+ DialogPortal,
+ DialogTitle,
+ DialogTrigger,
+}
diff --git a/packages/ui/src/components/drawer.tsx b/packages/ui/src/components/drawer.tsx
new file mode 100644
index 0000000..2a329d4
--- /dev/null
+++ b/packages/ui/src/components/drawer.tsx
@@ -0,0 +1,133 @@
+import * as React from 'react'
+
+import { Drawer as DrawerPrimitive } from 'vaul'
+
+import { cn } from '@trid/ui/lib/utils'
+
+function Drawer({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DrawerTrigger({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DrawerPortal({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DrawerClose({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DrawerOverlay({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DrawerContent({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+ {children}
+
+
+ )
+}
+
+function DrawerHeader({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+function DrawerFooter({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+function DrawerTitle({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DrawerDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export {
+ Drawer,
+ DrawerPortal,
+ DrawerOverlay,
+ DrawerTrigger,
+ DrawerClose,
+ DrawerContent,
+ DrawerHeader,
+ DrawerFooter,
+ DrawerTitle,
+ DrawerDescription,
+}
diff --git a/packages/ui/src/components/dropdown-menu.tsx b/packages/ui/src/components/dropdown-menu.tsx
new file mode 100644
index 0000000..514f343
--- /dev/null
+++ b/packages/ui/src/components/dropdown-menu.tsx
@@ -0,0 +1,274 @@
+'use client'
+
+import * as React from 'react'
+
+import { IconCheck, IconChevronRight } from '@tabler/icons-react'
+import { DropdownMenu as DropdownMenuPrimitive } from 'radix-ui'
+
+import { cn } from '@trid/ui/lib/utils'
+
+function DropdownMenu({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DropdownMenuPortal({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DropdownMenuTrigger({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DropdownMenuContent({
+ className,
+ align = 'start',
+ sideOffset = 4,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+function DropdownMenuGroup({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DropdownMenuItem({
+ className,
+ inset,
+ variant = 'default',
+ ...props
+}: React.ComponentProps & {
+ inset?: boolean
+ variant?: 'default' | 'destructive'
+}) {
+ return (
+
+ )
+}
+
+function DropdownMenuCheckboxItem({
+ className,
+ children,
+ checked,
+ inset,
+ ...props
+}: React.ComponentProps & {
+ inset?: boolean
+}) {
+ return (
+
+
+
+
+
+
+ {children}
+
+ )
+}
+
+function DropdownMenuRadioGroup({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DropdownMenuRadioItem({
+ className,
+ children,
+ inset,
+ ...props
+}: React.ComponentProps & {
+ inset?: boolean
+}) {
+ return (
+
+
+
+
+
+
+ {children}
+
+ )
+}
+
+function DropdownMenuLabel({
+ className,
+ inset,
+ ...props
+}: React.ComponentProps & {
+ inset?: boolean
+}) {
+ return (
+
+ )
+}
+
+function DropdownMenuSeparator({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DropdownMenuShortcut({
+ className,
+ ...props
+}: React.ComponentProps<'span'>) {
+ return (
+
+ )
+}
+
+function DropdownMenuSub({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DropdownMenuSubTrigger({
+ className,
+ inset,
+ children,
+ ...props
+}: React.ComponentProps & {
+ inset?: boolean
+}) {
+ return (
+
+ {children}
+
+
+ )
+}
+
+function DropdownMenuSubContent({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export {
+ DropdownMenu,
+ DropdownMenuPortal,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuGroup,
+ DropdownMenuLabel,
+ DropdownMenuItem,
+ DropdownMenuCheckboxItem,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuSub,
+ DropdownMenuSubTrigger,
+ DropdownMenuSubContent,
+}
diff --git a/packages/ui/src/components/empty.tsx b/packages/ui/src/components/empty.tsx
new file mode 100644
index 0000000..2d6037c
--- /dev/null
+++ b/packages/ui/src/components/empty.tsx
@@ -0,0 +1,105 @@
+import { cva } from 'class-variance-authority'
+import type { VariantProps } from 'class-variance-authority'
+
+import { cn } from '@trid/ui/lib/utils'
+
+function Empty({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+function EmptyHeader({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+const emptyMediaVariants = cva(
+ 'mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0',
+ {
+ variants: {
+ variant: {
+ default: 'bg-transparent',
+ icon: "flex size-8 shrink-0 items-center justify-center rounded-md bg-muted text-foreground [&_svg:not([class*='size-'])]:size-4",
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ },
+ }
+)
+
+function EmptyMedia({
+ className,
+ variant = 'default',
+ ...props
+}: React.ComponentProps<'div'> & VariantProps) {
+ return (
+
+ )
+}
+
+function EmptyTitle({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+function EmptyDescription({ className, ...props }: React.ComponentProps<'p'>) {
+ return (
+ a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary',
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function EmptyContent({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+export {
+ Empty,
+ EmptyHeader,
+ EmptyTitle,
+ EmptyDescription,
+ EmptyContent,
+ EmptyMedia,
+}
diff --git a/packages/ui/src/components/field.tsx b/packages/ui/src/components/field.tsx
new file mode 100644
index 0000000..8095a27
--- /dev/null
+++ b/packages/ui/src/components/field.tsx
@@ -0,0 +1,238 @@
+import { useMemo } from 'react'
+
+import { cva } from 'class-variance-authority'
+import type { VariantProps } from 'class-variance-authority'
+
+import { Label } from '@trid/ui/components/label'
+import { Separator } from '@trid/ui/components/separator'
+import { cn } from '@trid/ui/lib/utils'
+
+function FieldSet({ className, ...props }: React.ComponentProps<'fieldset'>) {
+ return (
+