feat: implement sharing

This commit is contained in:
shadcn 2023-06-16 15:20:42 +04:00
parent 385b31d42c
commit 8cc3fea048
19 changed files with 507 additions and 96 deletions

View file

@ -5,6 +5,7 @@ import { kv } from '@vercel/kv'
import { type Chat } from '@/lib/types'
import { currentUser } from '@clerk/nextjs'
import { nanoid } from '@/lib/utils'
export async function getChats(userId?: string | null) {
if (!userId) {
@ -60,3 +61,54 @@ export async function removeChat({ id, path }: { id: string; path: string }) {
revalidatePath('/')
revalidatePath(path)
}
export async function clearChats() {
const user = await currentUser()
if (!user) {
throw new Error('Unauthorized')
}
const chats: string[] = await kv.zrange(`user:chat:${user.id}`, 0, -1, {
rev: true
})
const pipeline = kv.pipeline()
for (const chat of chats) {
pipeline.del(chat)
pipeline.zrem(`user:chat:${user.id}`, chat)
}
await pipeline.exec()
revalidatePath('/')
}
export async function shareChat(chat: Chat) {
const user = await currentUser()
if (!user || chat.userId !== user.id) {
throw new Error('Unauthorized')
}
const id = nanoid()
const createdAt = Date.now()
const payload = {
id,
title: chat.title,
userId: user.id,
createdAt,
path: `/share/${id}`,
chat
}
await kv.hmset(`share:${id}`, payload)
await kv.zadd(`user:share:${user.id}`, {
score: createdAt,
member: `share:${id}`
})
return payload
}

View file

@ -43,11 +43,13 @@ export async function POST(req: Request) {
const userId = user.id
const id = json.id ?? nanoid()
const createdAt = Date.now()
const path = `/chat/${id}`
const payload = {
id,
title,
userId,
createdAt,
path,
messages: [
...messages,
{

View file

@ -1,6 +1,7 @@
import { Metadata } from 'next'
import { ClerkProvider } from '@clerk/nextjs'
import { Toaster } from 'react-hot-toast'
import '@/app/globals.css'
import { fontMono, fontSans } from '@/lib/fonts'
@ -42,6 +43,7 @@ export default function RootLayout({ children }: RootLayoutProps) {
fontMono.variable
)}
>
<Toaster />
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<div className="flex min-h-screen flex-col">
{/* @ts-ignore */}

View file

@ -10,10 +10,11 @@ import { ChatScrollAnchor } from '@/components/chat-scroll-anchor'
export interface ChatProps extends React.ComponentProps<'div'> {
initialMessages?: Message[]
id?: string
}
export function Chat({ id, initialMessages, className }: ChatProps) {
export function Chat({ id, title, initialMessages, className }: ChatProps) {
const { messages, append, reload, stop, isLoading, input, setInput } =
useChat({
initialMessages,

View file

@ -0,0 +1,65 @@
'use client'
import * as React from 'react'
import { useRouter } from 'next/navigation'
import { Button } from '@/components/ui/button'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger
} from '@/components/ui/alert-dialog'
import { IconSpinner } from '@/components/ui/icons'
interface ClearHistoryProps {
clearChats: () => Promise<void>
}
export function ClearHistory({ clearChats }: ClearHistoryProps) {
const [open, setOpen] = React.useState(false)
const [isPending, startTransition] = React.useTransition()
const router = useRouter()
return (
<AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialogTrigger asChild>
<Button variant="ghost" disabled={isPending}>
{isPending && <IconSpinner className="mr-2" />}
Clear history
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This will permanently delete your chat history and remove your data
from our servers.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isPending}>Cancel</AlertDialogCancel>
<AlertDialogAction
disabled={isPending}
onClick={event => {
event.preventDefault()
startTransition(async () => {
await clearChats()
setOpen(false)
router.push('/')
})
}}
>
{isPending && <IconSpinner className="mr-2 animate-spin" />}
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}

View file

@ -6,6 +6,10 @@ import { Sidebar } from '@/components/sidebar'
import { SidebarList } from '@/components/sidebar-list'
import { IconGitHub, IconSeparator, IconVercel } from '@/components/ui/icons'
import { UserButton, currentUser } from '@clerk/nextjs'
import { SidebarFooter } from '@/components/sidebar-footer'
import { ThemeToggle } from '@/components/theme-toggle'
import { ClearHistory } from '@/components/clear-history'
import { clearChats } from '@/app/actions'
export async function Header() {
const user = await currentUser()
@ -19,6 +23,10 @@ export async function Header() {
{/* @ts-ignore */}
<SidebarList userId={user?.id} />
</Suspense>
<SidebarFooter>
<ThemeToggle />
<ClearHistory clearChats={clearChats} />
</SidebarFooter>
</Sidebar>
<div className="flex items-center">
<IconSeparator className="h-6 w-6 text-muted-foreground/50" />

View file

@ -0,0 +1,164 @@
'use client'
import * as React from 'react'
import { useRouter } from 'next/navigation'
import { toast } from 'react-hot-toast'
import { Share, type Chat, ServerActionResult } from '@/lib/types'
import { formatDate } from '@/lib/utils'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle
} from '@/components/ui/alert-dialog'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle
} from '@/components/ui/dialog'
import { IconShare, IconSpinner, IconTrash } from '@/components/ui/icons'
interface SidebarActionsProps {
chat: Chat
removeChat: (args: { id: string; path: string }) => Promise<void>
shareChat: (chat: Chat) => ServerActionResult<Share>
}
export function SidebarActions({
chat,
removeChat,
shareChat
}: SidebarActionsProps) {
const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false)
const [shareDialogOpen, setShareDialogOpen] = React.useState(false)
const [isRemovePending, startRemoveTransition] = React.useTransition()
const [isSharePending, startShareTransition] = React.useTransition()
const router = useRouter()
return (
<>
<div className="space-x-1">
<Button
variant="ghost"
className="h-6 w-6 p-0 hover:bg-background"
onClick={() => setShareDialogOpen(true)}
>
<IconShare />
<span className="sr-only">Share</span>
</Button>
<Button
variant="ghost"
className="h-6 w-6 p-0 hover:bg-background"
disabled={isRemovePending}
onClick={() => setDeleteDialogOpen(true)}
>
<IconTrash />
<span className="sr-only">Delete</span>
</Button>
</div>
<Dialog open={shareDialogOpen} onOpenChange={setShareDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Share link to chat</DialogTitle>
<DialogDescription>
Messages you send after creating your link won&apos;t be shared.
Anyone with the URL will be able to view the shared chat.
</DialogDescription>
</DialogHeader>
<div className="space-y-1 rounded-md border p-4 text-sm">
<div className="font-medium">{chat.title}</div>
<div className="text-muted-foreground">
{formatDate(chat.createdAt)}
</div>
</div>
<DialogFooter className="items-center">
<p className="mr-auto text-sm text-muted-foreground">
Any link you have shared before will be deleted.
</p>
<Button
disabled={isSharePending}
onClick={() => {
startShareTransition(async () => {
const result = await shareChat(chat)
if (!('id' in result)) {
toast.error(result.message)
return
}
const url = new URL(window.location.href)
url.pathname = result.path
navigator.clipboard.writeText(url.toString())
setShareDialogOpen(false)
toast.success('Share link copied to clipboard', {
style: {
borderRadius: '10px',
background: '#333',
color: '#fff',
fontSize: '14px'
},
iconTheme: {
primary: 'white',
secondary: 'black'
}
})
})
}}
>
{isSharePending ? (
<>
<IconSpinner className="mr-2 animate-spin" />
Copying...
</>
) : (
<>Copy link</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This will permanently delete your chat message and remove your
data from our servers.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isRemovePending}>
Cancel
</AlertDialogCancel>
<AlertDialogAction
disabled={isRemovePending}
onClick={event => {
event.preventDefault()
startRemoveTransition(async () => {
await removeChat({
id: chat.id,
path: chat.path
})
setDeleteDialogOpen(false)
router.push('/')
})
}}
>
{isRemovePending && <IconSpinner className="mr-2 animate-spin" />}
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
)
}

View file

@ -0,0 +1,16 @@
import { cn } from '@/lib/utils'
export function SidebarFooter({
children,
className,
...props
}: React.ComponentProps<'div'>) {
return (
<div
className={cn('flex items-center justify-between p-4', className)}
{...props}
>
{children}
</div>
)
}

View file

@ -1,43 +1,29 @@
'use client'
import * as React from 'react'
import Link from 'next/link'
import { usePathname, useRouter } from 'next/navigation'
import { usePathname } from 'next/navigation'
import { type Chat } from '@/lib/types'
import { cn } from '@/lib/utils'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle
} from '@/components/ui/alert-dialog'
import { Button, buttonVariants } from '@/components/ui/button'
import { IconMessage, IconSpinner, IconTrash } from '@/components/ui/icons'
import { buttonVariants } from '@/components/ui/button'
import { IconMessage } from '@/components/ui/icons'
import { SidebarActions } from '@/components/sidebar-actions'
interface SidebarItemProps {
title: string
href: string
id: string
removeChat: (args: { id: string; path: string }) => Promise<void>
chat: Chat
children: React.ReactNode
}
export function SidebarItem({ title, href, id, removeChat }: SidebarItemProps) {
const [open, setIsOpen] = React.useState(false)
export function SidebarItem({ chat, children }: SidebarItemProps) {
const pathname = usePathname()
const router = useRouter()
const [isPending, startTransition] = React.useTransition()
const isActive = pathname === href
const isActive = pathname === chat.path
if (!id) return null
if (!chat?.id) return null
return (
<>
<Link
href={href}
href={chat.path || `/chat/${chat.id}`}
className={cn(
buttonVariants({ variant: 'ghost' }),
'group w-full px-2',
@ -47,51 +33,12 @@ export function SidebarItem({ title, href, id, removeChat }: SidebarItemProps) {
<IconMessage className="mr-2" />
<div
className="relative max-h-5 flex-1 select-none overflow-hidden text-ellipsis break-all"
title={title}
title={chat.title}
>
<span className="whitespace-nowrap">{title}</span>
<span className="whitespace-nowrap">{chat.title}</span>
</div>
{isActive && (
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
disabled={isPending}
onClick={() => setIsOpen(true)}
>
<IconTrash />
<span className="sr-only">Delete</span>
</Button>
)}
{children}
</Link>
<AlertDialog open={open} onOpenChange={setIsOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This will permanently delete your chat message and remove your
data from our servers.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isPending}>Cancel</AlertDialogCancel>
<AlertDialogAction
disabled={isPending}
onClick={event => {
event.preventDefault()
startTransition(async () => {
await removeChat({ id, path: href })
setIsOpen(false)
router.push('/')
})
}}
>
{isPending && <IconSpinner className="mr-2 animate-spin" />}
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
)
}

View file

@ -1,4 +1,5 @@
import { getChats, removeChat } from '@/app/actions'
import { getChats, removeChat, shareChat } from '@/app/actions'
import { SidebarActions } from '@/components/sidebar-actions'
import { SidebarItem } from '@/components/sidebar-item'
@ -9,19 +10,24 @@ export interface SidebarListProps {
export async function SidebarList({ userId }: SidebarListProps) {
const chats = await getChats(userId)
console.log(chats)
return (
<div className="flex-1 overflow-auto">
{chats?.length ? (
<div className="space-y-2 px-2">
{chats.map(chat => (
<SidebarItem
key={chat.id}
title={chat.title}
href={`/chat/${chat.id}`}
id={chat.id}
removeChat={removeChat}
/>
))}
{chats.map(
chat =>
chat && (
<SidebarItem key={chat?.id} chat={chat}>
<SidebarActions
chat={chat}
removeChat={removeChat}
shareChat={shareChat}
/>
</SidebarItem>
)
)}
</div>
) : (
<div className="p-8 text-center">

View file

@ -10,18 +10,14 @@ import {
SheetTitle,
SheetTrigger
} from '@/components/ui/sheet'
import { ThemeToggle } from '@/components/theme-toggle'
import { IconSidebar } from '@/components/ui/icons'
import { useClerk } from '@clerk/nextjs'
export interface SidebarProps {
userId?: string
children?: React.ReactNode
}
export function Sidebar({ userId, children }: SidebarProps) {
const { signOut } = useClerk()
export function Sidebar({ children }: SidebarProps) {
return (
<Sheet>
<SheetTrigger asChild>
@ -35,18 +31,6 @@ export function Sidebar({ userId, children }: SidebarProps) {
<SheetTitle className="text-sm">Chat History</SheetTitle>
</SheetHeader>
{children}
<div className="flex items-center p-4">
<ThemeToggle />
{userId && (
<Button
variant="ghost"
onClick={() => signOut()}
className="ml-auto"
>
Logout
</Button>
)}
</div>
</SheetContent>
</Sheet>
)

3
components/toaster.tsx Normal file
View file

@ -0,0 +1,3 @@
'use client'
export { Toaster } from 'react-hot-toast'

View file

@ -414,6 +414,20 @@ function IconEdit({ className, ...props }: React.ComponentProps<'svg'>) {
)
}
function IconShare({ className, ...props }: React.ComponentProps<'svg'>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
className={cn('h-4 w-4', className)}
viewBox="0 0 256 256"
{...props}
>
<path d="m237.66 106.35-80-80A8 8 0 0 0 144 32v40.35c-25.94 2.22-54.59 14.92-78.16 34.91-28.38 24.08-46.05 55.11-49.76 87.37a12 12 0 0 0 20.68 9.58c11-11.71 50.14-48.74 107.24-52V192a8 8 0 0 0 13.66 5.65l80-80a8 8 0 0 0 0-11.3ZM160 172.69V144a8 8 0 0 0-8-8c-28.08 0-55.43 7.33-81.29 21.8a196.17 196.17 0 0 0-36.57 26.52c5.8-23.84 20.42-46.51 42.05-64.86C99.41 99.77 127.75 88 152 88a8 8 0 0 0 8-8V51.32L220.69 112Z" />
</svg>
)
}
export {
IconEdit,
IconNextChat,
@ -437,5 +451,6 @@ export {
IconCopy,
IconCheck,
IconDownload,
IconClose
IconClose,
IconShare
}

26
components/ui/label.tsx Normal file
View file

@ -0,0 +1,26 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

29
components/ui/switch.tsx Normal file
View file

@ -0,0 +1,29 @@
"use client"
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }

View file

@ -5,5 +5,17 @@ export interface Chat extends Record<string, any> {
title: string
createdAt: Date
userId: string
path: string
messages: Message[]
}
export interface Share {
id: string
title: string
createdAt: number
userId: string
path: string
chat: Chat
}
export type ServerActionResult<Result> = Promise<Result | Error>

View file

@ -32,3 +32,12 @@ export async function fetcher<JSON = any>(
return res.json()
}
export function formatDate(input: string | number | Date): string {
const date = new Date(input)
return date.toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric'
})
}

View file

@ -17,8 +17,10 @@
"@clerk/nextjs": "^4.21.1",
"@radix-ui/react-alert-dialog": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.6",
"@vercel/analytics": "^1.0.0",
"@vercel/kv": "^0.2.1",

68
pnpm-lock.yaml generated
View file

@ -17,12 +17,18 @@ dependencies:
'@radix-ui/react-dialog':
specifier: ^1.0.4
version: 1.0.4(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-label':
specifier: ^2.0.2
version: 2.0.2(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-separator':
specifier: ^1.0.3
version: 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-slot':
specifier: ^1.0.2
version: 1.0.2(@types/react@18.2.6)(react@18.2.0)
'@radix-ui/react-switch':
specifier: ^1.0.3
version: 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-tooltip':
specifier: ^1.0.6
version: 1.0.6(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
@ -709,6 +715,27 @@ packages:
react: 18.2.0
dev: false
/@radix-ui/react-label@2.0.2(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-N5ehvlM7qoTLx7nWPodsPYPgMzA5WM8zZChQg8nyFJKnDO5WHdba1vv5/H6IO5LtJMfD2Q3wh1qHFGNtK0w3bQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.21.5
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
'@types/react': 18.2.6
'@types/react-dom': 18.2.4
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-popper@1.1.2(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-1CnGGfFi/bbqtJZZ0P/NQY20xdG3E0LALJaLUEoKwPLwl6PPPfbeiCqMVQnhoFRAxjJj4RpBRJzDmUgsex2tSg==}
peerDependencies:
@ -839,6 +866,33 @@ packages:
react: 18.2.0
dev: false
/@radix-ui/react-switch@1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.21.5
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.6)(react@18.2.0)
'@radix-ui/react-context': 1.0.1(@types/react@18.2.6)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.6)(react@18.2.0)
'@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.6)(react@18.2.0)
'@radix-ui/react-use-size': 1.0.1(@types/react@18.2.6)(react@18.2.0)
'@types/react': 18.2.6
'@types/react-dom': 18.2.4
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-tooltip@1.0.6(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-DmNFOiwEc2UDigsYj6clJENma58OelxD24O4IODoZ+3sQc3Zb+L8w1EP+y9laTuKCLAysPw4fD6/v0j4KNV8rg==}
peerDependencies:
@ -929,6 +983,20 @@ packages:
react: 18.2.0
dev: false
/@radix-ui/react-use-previous@1.0.1(@types/react@18.2.6)(react@18.2.0):
resolution: {integrity: sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.21.5
'@types/react': 18.2.6
react: 18.2.0
dev: false
/@radix-ui/react-use-rect@1.0.1(@types/react@18.2.6)(react@18.2.0):
resolution: {integrity: sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==}
peerDependencies: