Browse Source

feat(web): build webhook ui

pull/37/head
isra el 1 year ago
parent
commit
f04fc47a66
  1. 25
      web/app/(app)/dashboard/(components)/main-dashboard.tsx
  2. 198
      web/app/(app)/dashboard/(components)/webhooks/create-webhook-dialog.tsx
  3. 50
      web/app/(app)/dashboard/(components)/webhooks/delete-webhook-button.tsx
  4. 201
      web/app/(app)/dashboard/(components)/webhooks/edit-webhook-dialog.tsx
  5. 139
      web/app/(app)/dashboard/(components)/webhooks/webhook-card.tsx
  6. 169
      web/app/(app)/dashboard/(components)/webhooks/webhook-docs.tsx
  7. 178
      web/app/(app)/dashboard/(components)/webhooks/webhooks-section.tsx
  8. 49
      web/components/shared/copy-button.tsx
  9. 141
      web/components/ui/alert-dialog.tsx
  10. 17
      web/components/ui/code.tsx
  11. 15
      web/components/ui/skeleton.tsx
  12. 32
      web/components/ui/tooltip.tsx
  13. 3
      web/config/api.ts
  14. 3
      web/lib/constants.ts
  15. 17
      web/lib/types.ts
  16. 3
      web/package.json
  17. 413
      web/pnpm-lock.yaml

25
web/app/(app)/dashboard/(components)/main-dashboard.tsx

@ -1,23 +1,16 @@
'use client'
import { useRouter, usePathname } from 'next/navigation'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
// import Overview from "@/components/overview";
// import DeviceList from "@/components/device-list";
// import ApiKeys from "@/components/api-keys";
// import MessagingPanel from "@/components/messaging-panel";
import { Webhook, MessageSquare } from 'lucide-react'
import { useState } from 'react'
import Overview from './overview'
import DeviceList from './device-list'
import ApiKeys from './api-keys'
import Messaging from './messaging'
import WebhooksSection from './webhooks/webhooks-section'
export default function DashboardOverview() {
const router = useRouter()
const pathname = usePathname()
const [currentTab, setCurrentTab] = useState('overview')
@ -49,19 +42,7 @@ export default function DashboardOverview() {
<ApiKeys />
</div>
<Card>
<CardHeader>
<CardTitle>Webhooks (Coming Soon)</CardTitle>
</CardHeader>
<CardContent>
<Alert>
<AlertDescription>
Webhook support is coming soon! You&apos;ll be able to configure
endpoints to receive SMS notifications in real-time.
</AlertDescription>
</Alert>
</CardContent>
</Card>
<WebhooksSection />
</TabsContent>
<TabsContent value='messaging'>

198
web/app/(app)/dashboard/(components)/webhooks/create-webhook-dialog.tsx

@ -0,0 +1,198 @@
'use client'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import * as z from 'zod'
import { v4 as uuidv4 } from 'uuid'
import { WebhookData } from '@/lib/types'
import { WEBHOOK_EVENTS } from '@/lib/constants'
import httpBrowserClient from '@/lib/httpBrowserClient'
import { ApiEndpoints } from '@/config/api'
import { useToast } from '@/hooks/use-toast'
import { useMutation, useQueryClient } from '@tanstack/react-query'
const formSchema = z.object({
deliveryUrl: z.string().url({ message: 'Please enter a valid URL' }),
events: z.array(z.string()).min(1, { message: 'Select at least one event' }),
isActive: z.boolean().default(true),
signingSecret: z.string().min(1, { message: 'Signing secret is required' }),
})
interface CreateWebhookDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
}
export function CreateWebhookDialog({
open,
onOpenChange,
}: CreateWebhookDialogProps) {
const { toast } = useToast()
const queryClient = useQueryClient()
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
deliveryUrl: '',
events: [WEBHOOK_EVENTS.MESSAGE_RECEIVED],
isActive: true,
signingSecret: uuidv4(),
},
})
const createWebhookMutation = useMutation({
mutationFn: (values: z.infer<typeof formSchema>) =>
httpBrowserClient.post(ApiEndpoints.gateway.createWebhook(), values),
onSuccess: () => {
toast({
title: 'Success',
description: 'Webhook created successfully',
})
queryClient.invalidateQueries({ queryKey: ['webhooks'] })
onOpenChange(false)
form.reset()
},
onError: () => {
toast({
title: 'Error',
description: 'Failed to create webhook',
variant: 'destructive',
})
},
})
const onSubmit = (values: z.infer<typeof formSchema>) => {
createWebhookMutation.mutate(values)
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className='sm:max-w-[500px]'>
<DialogHeader>
<DialogTitle>Create Webhook</DialogTitle>
<DialogDescription>
Configure your webhook endpoint to receive real-time SMS
notifications.
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-6'>
<FormField
control={form.control}
name='deliveryUrl'
render={({ field }) => (
<FormItem>
<FormLabel>Delivery URL</FormLabel>
<FormControl>
<Input
placeholder='https://api.example.com/webhooks'
{...field}
/>
</FormControl>
<FormDescription>
The URL where webhook notifications will be sent via POST
requests
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='signingSecret'
render={({ field }) => (
<FormItem>
<FormLabel>Signing Secret</FormLabel>
<FormControl>
<div className='flex space-x-2'>
<Input {...field} type='text' />
<Button
type='button'
variant='outline'
onClick={() => field.onChange(uuidv4())}
>
Generate
</Button>
</div>
</FormControl>
<FormDescription>
Used to verify webhook payload authenticity
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='events'
render={({ field }) => (
<FormItem>
<FormLabel>Events</FormLabel>
<Select
value={field.value[0]}
onValueChange={(value) => field.onChange([value])}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder='Select events to subscribe to' />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value={WEBHOOK_EVENTS.MESSAGE_RECEIVED}>
SMS Received
</SelectItem>
</SelectContent>
</Select>
<FormDescription>
Choose the events you want to receive notifications for
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className='flex justify-end space-x-2'>
<Button
type='button'
variant='outline'
onClick={() => onOpenChange(false)}
>
Cancel
</Button>
<Button
type='submit'
disabled={createWebhookMutation.isPending}
>
{createWebhookMutation.isPending ? 'Creating...' : 'Create'}
</Button>
</div>
</form>
</Form>
</DialogContent>
</Dialog>
)
}

50
web/app/(app)/dashboard/(components)/webhooks/delete-webhook-button.tsx

@ -0,0 +1,50 @@
'use client'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog'
import { Button } from '@/components/ui/button'
import { Trash2 } from 'lucide-react'
interface DeleteWebhookButtonProps {
onDelete: () => void
}
export function DeleteWebhookButton({ onDelete }: DeleteWebhookButtonProps) {
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant='outline' size='sm' disabled>
<Trash2 className='h-4 w-4 text-destructive' />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Webhook</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete this webhook? This action cannot be
undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={onDelete}
disabled
className='bg-destructive text-destructive-foreground hover:bg-destructive/90'
>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}

201
web/app/(app)/dashboard/(components)/webhooks/edit-webhook-dialog.tsx

@ -0,0 +1,201 @@
'use client'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import * as z from 'zod'
import { useEffect, useState } from 'react'
import { v4 as uuidv4 } from 'uuid'
import { WebhookData } from '@/lib/types'
import { WEBHOOK_EVENTS } from '@/lib/constants'
import httpBrowserClient from '@/lib/httpBrowserClient'
import { ApiEndpoints } from '@/config/api'
import { useToast } from '@/hooks/use-toast'
import { useMutation, useQueryClient } from '@tanstack/react-query'
const formSchema = z.object({
deliveryUrl: z.string().url({ message: 'Please enter a valid URL' }),
events: z.array(z.string()).min(1, { message: 'Select at least one event' }),
isActive: z.boolean().default(true),
signingSecret: z.string().min(1, { message: 'Signing secret is required' }),
})
interface EditWebhookDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
webhook: WebhookData
}
export function EditWebhookDialog({
open,
onOpenChange,
webhook,
}: EditWebhookDialogProps) {
const queryClient = useQueryClient()
const { toast } = useToast()
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
values: {
deliveryUrl: webhook.deliveryUrl,
events: webhook.events,
isActive: webhook.isActive,
signingSecret: webhook.signingSecret,
},
})
const { mutate: updateWebhook, isPending } = useMutation({
mutationFn: async (values: z.infer<typeof formSchema>) => {
return httpBrowserClient.patch(
ApiEndpoints.gateway.updateWebhook(webhook._id),
values
)
},
onSuccess: () => {
toast({
title: 'Success',
description: 'Webhook updated successfully',
})
// Invalidate and refetch webhooks list
queryClient.invalidateQueries({ queryKey: ['webhooks'] })
onOpenChange(false)
},
onError: () => {
toast({
title: 'Error',
description: 'Failed to update webhook',
variant: 'destructive',
})
},
})
const onSubmit = (values: z.infer<typeof formSchema>) => {
updateWebhook(values)
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className='sm:max-w-[500px]'>
<DialogHeader>
<DialogTitle>Edit Webhook</DialogTitle>
<DialogDescription>
Update your webhook configuration.
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-6'>
<FormField
control={form.control}
name='deliveryUrl'
render={({ field }) => (
<FormItem>
<FormLabel>Delivery URL</FormLabel>
<FormControl>
<Input
placeholder='https://api.example.com/webhooks'
{...field}
/>
</FormControl>
<FormDescription>
The URL where webhook notifications will be sent via POST
requests
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='signingSecret'
render={({ field }) => (
<FormItem>
<FormLabel>Signing Secret</FormLabel>
<FormControl>
<div className='flex space-x-2'>
<Input {...field} type='text' />
<Button
type='button'
variant='outline'
onClick={() => field.onChange(uuidv4())}
>
Generate
</Button>
</div>
</FormControl>
<FormDescription>
Used to verify webhook payload authenticity
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='events'
render={({ field }) => (
<FormItem>
<FormLabel>Events</FormLabel>
<Select
value={field.value[0]}
onValueChange={(value) => field.onChange([value])}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder='Select events to subscribe to' />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value={WEBHOOK_EVENTS.MESSAGE_RECEIVED}>
SMS Received
</SelectItem>
</SelectContent>
</Select>
<FormDescription>
Choose the events you want to receive notifications for
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className='flex justify-end space-x-2'>
<Button
type='button'
variant='outline'
onClick={() => onOpenChange(false)}
>
Cancel
</Button>
<Button type='submit' disabled={isPending}>
{isPending ? 'Updating...' : 'Update'}
</Button>
</div>
</form>
</Form>
</DialogContent>
</Dialog>
)
}

139
web/app/(app)/dashboard/(components)/webhooks/webhook-card.tsx

@ -0,0 +1,139 @@
'use client'
import { Card, CardContent, CardHeader } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { DeleteWebhookButton } from './delete-webhook-button'
import { Edit2, Eye, EyeOff } from 'lucide-react'
import { Switch } from '@/components/ui/switch'
import { useState } from 'react'
import { useToast } from '@/hooks/use-toast'
import { CopyButton } from '@/components/shared/copy-button'
import { WebhookData } from '@/lib/types'
import httpBrowserClient from '@/lib/httpBrowserClient'
import { ApiEndpoints } from '@/config/api'
import { useQueryClient } from '@tanstack/react-query'
interface WebhookCardProps {
webhook: WebhookData
onEdit: () => void
onDelete?: () => void
}
export function WebhookCard({ webhook, onEdit, onDelete }: WebhookCardProps) {
const { toast } = useToast()
const [isLoading, setIsLoading] = useState(false)
const queryClient = useQueryClient()
const [showSecret, setShowSecret] = useState(false)
const handleToggle = async (checked: boolean) => {
setIsLoading(true)
try {
await httpBrowserClient.patch(
ApiEndpoints.gateway.updateWebhook(webhook._id),
{ isActive: checked }
)
await queryClient.invalidateQueries({
queryKey: ['webhooks']
})
toast({
title: `Webhook ${checked ? 'enabled' : 'disabled'}`,
description: `Webhook notifications are now ${
checked ? 'enabled' : 'disabled'
}.`,
})
} catch (error) {
toast({
title: 'Error',
description: `Failed to ${checked ? 'enable' : 'disable'} webhook`,
variant: 'destructive',
})
} finally {
setIsLoading(false)
}
}
const maskSecret = (secret: string) => {
// if the secret is less than 18 characters, show all
if (secret.length <= 18) {
return secret.slice(0, 18)
}
return secret.slice(0, 18) + '*'.repeat(secret.length - 24)
}
return (
<Card>
<CardHeader className='flex flex-row items-center justify-between'>
<div className='space-y-1'>
<div className='flex items-center space-x-2'>
<h3 className='text-lg font-semibold'>Webhook Endpoint</h3>
<Badge variant={webhook.isActive ? 'default' : 'secondary'}>
{webhook.isActive ? 'Active' : 'Inactive'}
</Badge>
</div>
<p className='text-sm text-muted-foreground'>
Notifications for SMS events
</p>
</div>
<div className='flex items-center space-x-2'>
<Switch
checked={webhook.isActive}
onCheckedChange={handleToggle}
disabled={isLoading}
/>
<Button variant='outline' size='sm' onClick={onEdit}>
<Edit2 className='h-4 w-4 mr-2' />
Edit
</Button>
<DeleteWebhookButton onDelete={onDelete} />
</div>
</CardHeader>
<CardContent>
<div className='space-y-4'>
<div>
<label className='text-sm font-medium'>Delivery URL</label>
<div className='flex items-center mt-1'>
<code className='flex-1 bg-muted px-3 py-2 rounded-md text-sm'>
{webhook.deliveryUrl}
</code>
<CopyButton value={webhook.deliveryUrl} label='Copy URL' />
</div>
</div>
<div>
<label className='text-sm font-medium'>Signing Secret</label>
<div className='flex items-center mt-1'>
<code className='flex-1 bg-muted px-3 py-2 rounded-md text-sm font-mono'>
{showSecret ? webhook.signingSecret : maskSecret(webhook.signingSecret)}
</code>
<Button
variant="ghost"
size="icon"
onClick={() => setShowSecret(!showSecret)}
className="mx-2"
>
{showSecret ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</Button>
<CopyButton value={webhook.signingSecret} label='Copy Secret' />
</div>
</div>
<div>
<label className='text-sm font-medium'>Events</label>
<div className='flex flex-wrap gap-2 mt-1'>
{webhook.events.map((event) => (
<Badge key={event} variant='secondary'>
{event}
</Badge>
))}
</div>
</div>
</div>
</CardContent>
</Card>
)
}

169
web/app/(app)/dashboard/(components)/webhooks/webhook-docs.tsx

@ -0,0 +1,169 @@
'use client'
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@/components/ui/accordion'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Code } from '@/components/ui/code'
import { AlertCircle } from 'lucide-react'
const SAMPLE_PAYLOAD = {
smsId: 'smsId',
sender: '+123456789',
message: 'message',
receivedAt: 'datetime',
deviceId: 'deviceId',
webhookSubscriptionId: 'webhookSubscriptionId',
webhookEvent: 'sms.received',
}
const VERIFICATION_CODE = `
// Node.js example using crypto
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const hmac = crypto.createHmac('sha256', secret);
const digest = hmac.update(JSON.stringify(payload)).digest('hex');
const signatureHash = signature.split('=')[1];
return crypto.timingSafeEqual(
Buffer.from(signatureHash),
Buffer.from(digest)
);
}
// Express middleware example
app.post('/webhook', (req, res) => {
const signature = req.headers['x-signature'];
const payload = req.body;
if (!verifyWebhookSignature(payload, signature, WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process the webhook
console.log('Webhook verified:', payload);
res.status(200).send('OK');
});
`
const PYTHON_CODE = `
# Python example using hmac
import hmac
import hashlib
import json
from flask import Flask, request
app = Flask(__name__)
def verify_signature(payload, signature, secret):
expected = hmac.new(
secret.encode('utf-8'),
json.dumps(payload).encode('utf-8'),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature.split('=')[1], expected)
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('X-Signature')
if not verify_signature(request.json, signature, WEBHOOK_SECRET):
return 'Invalid signature', 401
# Process the webhook
print('Webhook verified:', request.json)
return 'OK', 200
`
export function WebhookDocs() {
return (
<Accordion type='multiple' className='w-full space-y-4'>
<AccordionItem value='delivery' className='border rounded-lg'>
<AccordionTrigger className='px-4 hover:no-underline [&[data-state=open]>div]:bg-muted'>
<div className='flex items-center gap-2 py-2 -my-2 px-2 rounded-md'>
<AlertCircle className='h-4 w-4' />
<span>Webhook Delivery Information</span>
</div>
</AccordionTrigger>
<AccordionContent className='px-4 pb-4'>
<div className='space-y-2 mt-2 text-sm text-muted-foreground'>
<p>
When a new SMS is received, we&apos;ll send a POST request to your
webhook URL with the event data. Your endpoint should:
</p>
<ul className='list-disc pl-6 space-y-1'>
<li>Accept POST requests</li>
<li>Return a 2XX status code to acknowledge receipt</li>
<li>Process the request within 10 seconds</li>
</ul>
<p className='mt-2'>
If we don&apos;t receive a successful response, we&apos;ll retry the
delivery at increasing intervals: 3 minutes, 5 minutes, 30 minutes,
1 hour, 6 hours, 1 day, 3 days, 7 days, 30 days.
</p>
</div>
</AccordionContent>
</AccordionItem>
<AccordionItem value='implementation' className='border rounded-lg'>
<AccordionTrigger className='px-4 hover:no-underline [&[data-state=open]>div]:bg-muted'>
<div className='flex items-center gap-2 py-2 -my-2 px-2 rounded-md'>
<AlertCircle className='h-4 w-4' />
<span>Security & Implementation Guide</span>
</div>
</AccordionTrigger>
<AccordionContent className='px-4 pb-4'>
<Tabs defaultValue='overview' className='w-full mt-4'>
<TabsList>
<TabsTrigger value='overview'>Overview</TabsTrigger>
<TabsTrigger value='payload'>Payload</TabsTrigger>
<TabsTrigger value='verification'>Verification</TabsTrigger>
</TabsList>
<TabsContent value='overview'>
<div className='space-y-2 mt-4 text-sm text-muted-foreground'>
<p>Each webhook request includes:</p>
<ul className='list-disc pl-6 space-y-1'>
<li>Payload in JSON format</li>
<li>X-Signature header for verification</li>
<li>
Signature format: sha256=HMAC_SHA256(payload, secret)
</li>
</ul>
</div>
</TabsContent>
<TabsContent value='payload'>
<div className='space-y-4 mt-4'>
<h4 className='text-sm font-medium'>Sample Payload</h4>
<Code>{JSON.stringify(SAMPLE_PAYLOAD, null, 2)}</Code>
</div>
</TabsContent>
<TabsContent value='verification'>
<div className='space-y-4 mt-4'>
<Tabs defaultValue='node'>
<TabsList>
<TabsTrigger value='node'>Node.js</TabsTrigger>
<TabsTrigger value='python'>Python</TabsTrigger>
</TabsList>
<TabsContent value='node'>
<Code>{VERIFICATION_CODE}</Code>
</TabsContent>
<TabsContent value='python'>
<Code>{PYTHON_CODE}</Code>
</TabsContent>
</Tabs>
</div>
</TabsContent>
</Tabs>
</AccordionContent>
</AccordionItem>
</Accordion>
)
}

178
web/app/(app)/dashboard/(components)/webhooks/webhooks-section.tsx

@ -0,0 +1,178 @@
'use client'
import { Button } from '@/components/ui/button'
import { PlusCircle, Webhook } from 'lucide-react'
import { useState } from 'react'
import { WebhookData } from '@/lib/types'
import { WebhookCard } from './webhook-card'
import { WebhookDocs } from './webhook-docs'
import { CreateWebhookDialog } from './create-webhook-dialog'
import { EditWebhookDialog } from './edit-webhook-dialog'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import httpBrowserClient from '@/lib/httpBrowserClient'
import { ApiEndpoints } from '@/config/api'
import { Skeleton } from '@/components/ui/skeleton'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
function WebhookCardSkeleton() {
return (
<div className='rounded-lg border p-6 space-y-4'>
<div className='flex items-center justify-between'>
<div className='space-y-2'>
<Skeleton className='h-5 w-[200px]' />
<Skeleton className='h-4 w-[150px]' />
</div>
<div className='flex space-x-2'>
<Skeleton className='h-9 w-9' />
<Skeleton className='h-9 w-16' />
<Skeleton className='h-9 w-9' />
</div>
</div>
<div className='space-y-4'>
<div>
<Skeleton className='h-4 w-[100px] mb-2' />
<Skeleton className='h-10 w-full' />
</div>
<div>
<Skeleton className='h-4 w-[100px] mb-2' />
<Skeleton className='h-10 w-full' />
</div>
<div>
<Skeleton className='h-4 w-[100px] mb-2' />
<div className='flex gap-2'>
<Skeleton className='h-6 w-20' />
<Skeleton className='h-6 w-20' />
</div>
</div>
</div>
</div>
)
}
export default function WebhooksSection() {
const [createDialogOpen, setCreateDialogOpen] = useState(false)
const [editDialogOpen, setEditDialogOpen] = useState(false)
const [selectedWebhook, setSelectedWebhook] = useState<WebhookData | null>(
null
)
const queryClient = useQueryClient()
const {
data: webhooks,
isLoading,
error,
} = useQuery({
queryKey: ['webhooks'],
queryFn: () =>
httpBrowserClient
.get(ApiEndpoints.gateway.getWebhooks())
.then((res) => res.data),
})
const handleCreateClick = () => {
setCreateDialogOpen(true)
}
const handleEditClick = (webhook: WebhookData) => {
setSelectedWebhook(webhook)
setEditDialogOpen(true)
}
return (
<div className='container mx-auto py-8'>
<div className='flex justify-between items-center mb-8'>
<div>
<h1 className='text-3xl font-bold flex items-center gap-2'>
<Webhook className='h-8 w-8' />
Webhooks
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<span className='text-xs font-medium px-2 py-1 rounded-full bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200'>
BETA
</span>
</TooltipTrigger>
<TooltipContent>
<p>This feature is in beta and may undergo changes. Use with caution in production environments.</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</h1>
<p className='text-muted-foreground mt-2'>
Manage webhook notifications for your SMS events
</p>
</div>
<Button
onClick={handleCreateClick}
disabled={webhooks?.data?.length > 0 || isLoading}
variant='default'
>
<PlusCircle className='mr-2 h-4 w-4' />
Create Webhook
</Button>
</div>
<div className='grid grid-cols-1 lg:grid-cols-2 gap-8'>
<div>
{isLoading ? (
<div className='grid gap-4'>
<WebhookCardSkeleton />
<WebhookCardSkeleton />
</div>
) : error ? (
<div className='rounded-lg border border-destructive/50 p-4 text-destructive'>
Error: {error.message}
</div>
) : webhooks?.data?.length > 0 ? (
<div className='grid gap-4'>
{webhooks.data.map((webhook) => (
<WebhookCard
key={webhook._id}
webhook={webhook}
onEdit={() => handleEditClick(webhook)}
/>
))}
</div>
) : (
<div className='bg-muted/50 rounded-lg p-8 text-center'>
<h3 className='text-lg font-medium mb-2'>No webhook configured</h3>
<p className='text-muted-foreground mb-4'>
Create a webhook to receive real-time notifications for SMS events
</p>
<Button onClick={handleCreateClick} variant='default'>
<PlusCircle className='mr-2 h-4 w-4' />
Create Webhook
</Button>
</div>
)}
</div>
<div className='hidden lg:block sticky top-8 self-start'>
<WebhookDocs />
</div>
</div>
<div className='block lg:hidden mt-8'>
<WebhookDocs />
</div>
<CreateWebhookDialog
open={createDialogOpen}
onOpenChange={setCreateDialogOpen}
/>
{selectedWebhook && (
<EditWebhookDialog
open={editDialogOpen}
onOpenChange={setEditDialogOpen}
webhook={selectedWebhook}
/>
)}
</div>
)
}

49
web/components/shared/copy-button.tsx

@ -0,0 +1,49 @@
"use client";
import { Button } from "@/components/ui/button";
import { Check, Copy } from "lucide-react";
import { useState } from "react";
import { useToast } from "@/hooks/use-toast";
interface CopyButtonProps {
value: string;
label: string;
}
export function CopyButton({ value, label }: CopyButtonProps) {
const [copied, setCopied] = useState(false);
const { toast } = useToast();
const copyToClipboard = async () => {
try {
await navigator.clipboard.writeText(value);
setCopied(true);
toast({
title: "Copied!",
description: `${label} copied to clipboard`,
});
setTimeout(() => setCopied(false), 2000);
} catch (err) {
toast({
title: "Failed to copy",
description: "Please try again",
variant: "destructive",
});
}
};
return (
<Button
variant="ghost"
size="sm"
onClick={copyToClipboard}
className="ml-2"
>
{copied ? (
<Check className="h-4 w-4 text-green-500" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
);
}

141
web/components/ui/alert-dialog.tsx

@ -0,0 +1,141 @@
"use client"
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
const AlertDialog = AlertDialogPrimitive.Root
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold", className)}
{...props}
/>
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
className
)}
{...props}
/>
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}

17
web/components/ui/code.tsx

@ -0,0 +1,17 @@
import { cn } from "@/lib/utils";
interface CodeProps extends React.HTMLAttributes<HTMLPreElement> {}
export function Code({ className, children, ...props }: CodeProps) {
return (
<pre
className={cn(
"rounded-lg bg-muted p-4 overflow-x-auto text-sm",
className
)}
{...props}
>
<code>{children}</code>
</pre>
);
}

15
web/components/ui/skeleton.tsx

@ -0,0 +1,15 @@
import { cn } from "@/lib/utils"
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-primary/10", className)}
{...props}
/>
)
}
export { Skeleton }

32
web/components/ui/tooltip.tsx

@ -0,0 +1,32 @@
"use client"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</TooltipPrimitive.Portal>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

3
web/config/api.ts

@ -23,6 +23,9 @@ export const ApiEndpoints = {
sendBulkSMS: (id: string) => `/gateway/devices/${id}/send-bulk-sms`,
getReceivedSMS: (id: string) => `/gateway/devices/${id}/get-received-sms`,
getWebhooks: () => '/webhooks',
createWebhook: () => '/webhooks',
updateWebhook: (id: string) => `/webhooks/${id}`,
getStats: () => '/gateway/stats',
},
}

3
web/lib/constants.ts

@ -0,0 +1,3 @@
export const WEBHOOK_EVENTS = {
MESSAGE_RECEIVED: 'MESSAGE_RECEIVED',
} as const

17
web/lib/types.ts

@ -0,0 +1,17 @@
export interface WebhookData {
_id?: string
deliveryUrl: string
events: string[]
isActive: boolean
signingSecret: string
}
export interface WebhookPayload {
smsId: string
sender: string
message: string
receivedAt: string
deviceId: string
webhookSubscriptionId: string
webhookEvent: string
}

3
web/package.json

@ -15,6 +15,7 @@
"@hookform/resolvers": "^3.9.1",
"@prisma/client": "^5.22.0",
"@radix-ui/react-accordion": "^1.2.1",
"@radix-ui/react-alert-dialog": "^1.1.4",
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.2",
@ -28,6 +29,7 @@
"@radix-ui/react-switch": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.1",
"@radix-ui/react-toast": "^1.2.2",
"@radix-ui/react-tooltip": "^1.1.6",
"@react-oauth/google": "^0.12.1",
"@tanstack/react-query": "^5.61.0",
"axios": "^1.6.5",
@ -48,6 +50,7 @@
"react-syntax-highlighter": "^15.5.0",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
"uuid": "^11.0.3",
"zod": "^3.23.8"
},
"engines": {

413
web/pnpm-lock.yaml

@ -17,6 +17,9 @@ importers:
'@radix-ui/react-accordion':
specifier: ^1.2.1
version: 1.2.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-alert-dialog':
specifier: ^1.1.4
version: 1.1.4(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-avatar':
specifier: ^1.1.1
version: 1.1.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@ -56,6 +59,9 @@ importers:
'@radix-ui/react-toast':
specifier: ^1.2.2
version: 1.2.2(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-tooltip':
specifier: ^1.1.6
version: 1.1.6(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@react-oauth/google':
specifier: ^0.12.1
version: 0.12.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@ -116,6 +122,9 @@ importers:
tailwindcss-animate:
specifier: ^1.0.7
version: 1.0.7(tailwindcss@3.4.14)
uuid:
specifier: ^11.0.3
version: 11.0.3
zod:
specifier: ^3.23.8
version: 3.23.8
@ -348,6 +357,9 @@ packages:
'@radix-ui/primitive@1.1.0':
resolution: {integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==}
'@radix-ui/primitive@1.1.1':
resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==}
'@radix-ui/react-accordion@1.2.1':
resolution: {integrity: sha512-bg/l7l5QzUjgsh8kjwDFommzAshnUsuVMV5NM56QVCm+7ZckYdd9P/ExR8xG/Oup0OajVxNLaHJ1tb8mXk+nzQ==}
peerDependencies:
@ -361,6 +373,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-alert-dialog@1.1.4':
resolution: {integrity: sha512-A6Kh23qZDLy3PSU4bh2UJZznOrUdHImIXqF8YtUa6CN73f8EOO9XlXSCd9IHyPvIquTaa/kwaSWzZTtUvgXVGw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-arrow@1.1.0':
resolution: {integrity: sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==}
peerDependencies:
@ -374,6 +399,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-arrow@1.1.1':
resolution: {integrity: sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-avatar@1.1.1':
resolution: {integrity: sha512-eoOtThOmxeoizxpX6RiEsQZ2wj5r4+zoeqAwO0cBaFQGjJwIH3dIX0OCxNrCyrrdxG+vBweMETh3VziQG7c1kw==}
peerDependencies:
@ -435,6 +473,15 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-compose-refs@1.1.1':
resolution: {integrity: sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-context@1.1.0':
resolution: {integrity: sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==}
peerDependencies:
@ -466,6 +513,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-dialog@1.1.4':
resolution: {integrity: sha512-Ur7EV1IwQGCyaAuyDRiOLA5JIUZxELJljF+MbM/2NC0BYwfuRrbpS30BiQBJrVruscgUkieKkqXYDOoByaxIoA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-direction@1.1.0':
resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==}
peerDependencies:
@ -488,6 +548,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-dismissable-layer@1.1.3':
resolution: {integrity: sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-dropdown-menu@2.1.2':
resolution: {integrity: sha512-GVZMR+eqK8/Kes0a36Qrv+i20bAPXSn8rCBTHx30w+3ECnR5o3xixAlqcVaYvLeyKUsm0aqyhWfmUcqufM8nYA==}
peerDependencies:
@ -523,6 +596,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-focus-scope@1.1.1':
resolution: {integrity: sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-icons@1.3.0':
resolution: {integrity: sha512-jQxj/0LKgp+j9BiTXz3O3sgs26RNet2iLWmsPyRz2SIcR4q/4SbazXfnYwbAr+vLYKSfc7qxzyGQA1HLlYiuNw==}
peerDependencies:
@ -589,6 +675,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-popper@1.2.1':
resolution: {integrity: sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-portal@1.1.2':
resolution: {integrity: sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==}
peerDependencies:
@ -602,6 +701,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-portal@1.1.3':
resolution: {integrity: sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-presence@1.1.1':
resolution: {integrity: sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==}
peerDependencies:
@ -615,6 +727,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-presence@1.1.2':
resolution: {integrity: sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-primitive@2.0.0':
resolution: {integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==}
peerDependencies:
@ -628,6 +753,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-primitive@2.0.1':
resolution: {integrity: sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-roving-focus@1.1.0':
resolution: {integrity: sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==}
peerDependencies:
@ -676,6 +814,15 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-slot@1.1.1':
resolution: {integrity: sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-switch@1.1.1':
resolution: {integrity: sha512-diPqDDoBcZPSicYoMWdWx+bCPuTRH4QSp9J+65IvtdS0Kuzt67bI6n32vCj8q6NZmYW/ah+2orOtMwcX5eQwIg==}
peerDependencies:
@ -715,6 +862,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-tooltip@1.1.6':
resolution: {integrity: sha512-TLB5D8QLExS1uDn7+wH/bjEmRurNMTzNrtq7IjaS4kjion9NtzsTGkvR5+i7yc9q01Pi2KMM2cN3f8UG4IvvXA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-use-callback-ref@1.1.0':
resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==}
peerDependencies:
@ -791,6 +951,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-visually-hidden@1.1.1':
resolution: {integrity: sha512-vVfA2IZ9q/J+gEamvj761Oq1FpWgCDaNOOIfbPVp2MVPLEomUr5+Vf7kJGwQ24YxZSlQVar7Bes8kyTo5Dshpg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/rect@1.1.0':
resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==}
@ -2081,6 +2254,16 @@ packages:
'@types/react':
optional: true
react-remove-scroll-bar@2.3.8:
resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': '*'
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@types/react':
optional: true
react-remove-scroll@2.6.0:
resolution: {integrity: sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ==}
engines: {node: '>=10'}
@ -2091,6 +2274,16 @@ packages:
'@types/react':
optional: true
react-remove-scroll@2.6.2:
resolution: {integrity: sha512-KmONPx5fnlXYJQqC62Q+lwIeAk64ws/cUw6omIumRzMRPqgnYqhSSti99nbj0Ry13bv7dF+BKn7NB+OqkdZGTw==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': '*'
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
react-style-singleton@2.2.1:
resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==}
engines: {node: '>=10'}
@ -2101,6 +2294,16 @@ packages:
'@types/react':
optional: true
react-style-singleton@2.2.3:
resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': '*'
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
react-syntax-highlighter@15.5.0:
resolution: {integrity: sha512-+zq2myprEnQmH5yw6Gqc8lD55QHnpKaU8TOcFeC/Lg/MQSs8UknEA0JC4nTZGFAXC2J2Hyj/ijJ7NlabyPi2gg==}
peerDependencies:
@ -2384,6 +2587,16 @@ packages:
'@types/react':
optional: true
use-callback-ref@1.3.3:
resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': '*'
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
use-sidecar@1.1.2:
resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==}
engines: {node: '>=10'}
@ -2397,6 +2610,10 @@ packages:
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
uuid@11.0.3:
resolution: {integrity: sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==}
hasBin: true
uuid@8.3.2:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
hasBin: true
@ -2621,6 +2838,8 @@ snapshots:
'@radix-ui/primitive@1.1.0': {}
'@radix-ui/primitive@1.1.1': {}
'@radix-ui/react-accordion@1.2.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/primitive': 1.1.0
@ -2638,6 +2857,20 @@ snapshots:
'@types/react': 18.2.48
'@types/react-dom': 18.2.18
'@radix-ui/react-alert-dialog@1.1.4(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/primitive': 1.1.1
'@radix-ui/react-compose-refs': 1.1.1(@types/react@18.2.48)(react@18.2.0)
'@radix-ui/react-context': 1.1.1(@types/react@18.2.48)(react@18.2.0)
'@radix-ui/react-dialog': 1.1.4(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-slot': 1.1.1(@types/react@18.2.48)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
optionalDependencies:
'@types/react': 18.2.48
'@types/react-dom': 18.2.18
'@radix-ui/react-arrow@1.1.0(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@ -2647,6 +2880,15 @@ snapshots:
'@types/react': 18.2.48
'@types/react-dom': 18.2.18
'@radix-ui/react-arrow@1.1.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
optionalDependencies:
'@types/react': 18.2.48
'@types/react-dom': 18.2.18
'@radix-ui/react-avatar@1.1.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/react-context': 1.1.1(@types/react@18.2.48)(react@18.2.0)
@ -2709,6 +2951,12 @@ snapshots:
optionalDependencies:
'@types/react': 18.2.48
'@radix-ui/react-compose-refs@1.1.1(@types/react@18.2.48)(react@18.2.0)':
dependencies:
react: 18.2.0
optionalDependencies:
'@types/react': 18.2.48
'@radix-ui/react-context@1.1.0(@types/react@18.2.48)(react@18.2.0)':
dependencies:
react: 18.2.0
@ -2743,6 +2991,28 @@ snapshots:
'@types/react': 18.2.48
'@types/react-dom': 18.2.18
'@radix-ui/react-dialog@1.1.4(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/primitive': 1.1.1
'@radix-ui/react-compose-refs': 1.1.1(@types/react@18.2.48)(react@18.2.0)
'@radix-ui/react-context': 1.1.1(@types/react@18.2.48)(react@18.2.0)
'@radix-ui/react-dismissable-layer': 1.1.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-focus-guards': 1.1.1(@types/react@18.2.48)(react@18.2.0)
'@radix-ui/react-focus-scope': 1.1.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-id': 1.1.0(@types/react@18.2.48)(react@18.2.0)
'@radix-ui/react-portal': 1.1.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-presence': 1.1.2(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-slot': 1.1.1(@types/react@18.2.48)(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.48)(react@18.2.0)
aria-hidden: 1.2.3
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-remove-scroll: 2.6.2(@types/react@18.2.48)(react@18.2.0)
optionalDependencies:
'@types/react': 18.2.48
'@types/react-dom': 18.2.18
'@radix-ui/react-direction@1.1.0(@types/react@18.2.48)(react@18.2.0)':
dependencies:
react: 18.2.0
@ -2762,6 +3032,19 @@ snapshots:
'@types/react': 18.2.48
'@types/react-dom': 18.2.18
'@radix-ui/react-dismissable-layer@1.1.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/primitive': 1.1.1
'@radix-ui/react-compose-refs': 1.1.1(@types/react@18.2.48)(react@18.2.0)
'@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.48)(react@18.2.0)
'@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@18.2.48)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
optionalDependencies:
'@types/react': 18.2.48
'@types/react-dom': 18.2.18
'@radix-ui/react-dropdown-menu@2.1.2(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/primitive': 1.1.0
@ -2794,6 +3077,17 @@ snapshots:
'@types/react': 18.2.48
'@types/react-dom': 18.2.18
'@radix-ui/react-focus-scope@1.1.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.1(@types/react@18.2.48)(react@18.2.0)
'@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.48)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
optionalDependencies:
'@types/react': 18.2.48
'@types/react-dom': 18.2.18
'@radix-ui/react-icons@1.3.0(react@18.2.0)':
dependencies:
react: 18.2.0
@ -2880,6 +3174,24 @@ snapshots:
'@types/react': 18.2.48
'@types/react-dom': 18.2.18
'@radix-ui/react-popper@1.2.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@floating-ui/react-dom': 2.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-arrow': 1.1.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-compose-refs': 1.1.1(@types/react@18.2.48)(react@18.2.0)
'@radix-ui/react-context': 1.1.1(@types/react@18.2.48)(react@18.2.0)
'@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.48)(react@18.2.0)
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.48)(react@18.2.0)
'@radix-ui/react-use-rect': 1.1.0(@types/react@18.2.48)(react@18.2.0)
'@radix-ui/react-use-size': 1.1.0(@types/react@18.2.48)(react@18.2.0)
'@radix-ui/rect': 1.1.0
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
optionalDependencies:
'@types/react': 18.2.48
'@types/react-dom': 18.2.18
'@radix-ui/react-portal@1.1.2(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@ -2890,6 +3202,16 @@ snapshots:
'@types/react': 18.2.48
'@types/react-dom': 18.2.18
'@radix-ui/react-portal@1.1.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.48)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
optionalDependencies:
'@types/react': 18.2.48
'@types/react-dom': 18.2.18
'@radix-ui/react-presence@1.1.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.48)(react@18.2.0)
@ -2900,6 +3222,16 @@ snapshots:
'@types/react': 18.2.48
'@types/react-dom': 18.2.18
'@radix-ui/react-presence@1.1.2(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.1(@types/react@18.2.48)(react@18.2.0)
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.48)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
optionalDependencies:
'@types/react': 18.2.48
'@types/react-dom': 18.2.18
'@radix-ui/react-primitive@2.0.0(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/react-slot': 1.1.0(@types/react@18.2.48)(react@18.2.0)
@ -2909,6 +3241,15 @@ snapshots:
'@types/react': 18.2.48
'@types/react-dom': 18.2.18
'@radix-ui/react-primitive@2.0.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/react-slot': 1.1.1(@types/react@18.2.48)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
optionalDependencies:
'@types/react': 18.2.48
'@types/react-dom': 18.2.18
'@radix-ui/react-roving-focus@1.1.0(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/primitive': 1.1.0
@ -2979,6 +3320,13 @@ snapshots:
optionalDependencies:
'@types/react': 18.2.48
'@radix-ui/react-slot@1.1.1(@types/react@18.2.48)(react@18.2.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.1(@types/react@18.2.48)(react@18.2.0)
react: 18.2.0
optionalDependencies:
'@types/react': 18.2.48
'@radix-ui/react-switch@1.1.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/primitive': 1.1.0
@ -3030,6 +3378,26 @@ snapshots:
'@types/react': 18.2.48
'@types/react-dom': 18.2.18
'@radix-ui/react-tooltip@1.1.6(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/primitive': 1.1.1
'@radix-ui/react-compose-refs': 1.1.1(@types/react@18.2.48)(react@18.2.0)
'@radix-ui/react-context': 1.1.1(@types/react@18.2.48)(react@18.2.0)
'@radix-ui/react-dismissable-layer': 1.1.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-id': 1.1.0(@types/react@18.2.48)(react@18.2.0)
'@radix-ui/react-popper': 1.2.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-portal': 1.1.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-presence': 1.1.2(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-slot': 1.1.1(@types/react@18.2.48)(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.48)(react@18.2.0)
'@radix-ui/react-visually-hidden': 1.1.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
optionalDependencies:
'@types/react': 18.2.48
'@types/react-dom': 18.2.18
'@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.2.48)(react@18.2.0)':
dependencies:
react: 18.2.0
@ -3085,6 +3453,15 @@ snapshots:
'@types/react': 18.2.48
'@types/react-dom': 18.2.18
'@radix-ui/react-visually-hidden@1.1.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
optionalDependencies:
'@types/react': 18.2.48
'@types/react-dom': 18.2.18
'@radix-ui/rect@1.1.0': {}
'@react-oauth/google@0.12.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
@ -4525,6 +4902,14 @@ snapshots:
optionalDependencies:
'@types/react': 18.2.48
react-remove-scroll-bar@2.3.8(@types/react@18.2.48)(react@18.2.0):
dependencies:
react: 18.2.0
react-style-singleton: 2.2.3(@types/react@18.2.48)(react@18.2.0)
tslib: 2.8.1
optionalDependencies:
'@types/react': 18.2.48
react-remove-scroll@2.6.0(@types/react@18.2.48)(react@18.2.0):
dependencies:
react: 18.2.0
@ -4536,6 +4921,17 @@ snapshots:
optionalDependencies:
'@types/react': 18.2.48
react-remove-scroll@2.6.2(@types/react@18.2.48)(react@18.2.0):
dependencies:
react: 18.2.0
react-remove-scroll-bar: 2.3.8(@types/react@18.2.48)(react@18.2.0)
react-style-singleton: 2.2.1(@types/react@18.2.48)(react@18.2.0)
tslib: 2.8.1
use-callback-ref: 1.3.3(@types/react@18.2.48)(react@18.2.0)
use-sidecar: 1.1.2(@types/react@18.2.48)(react@18.2.0)
optionalDependencies:
'@types/react': 18.2.48
react-style-singleton@2.2.1(@types/react@18.2.48)(react@18.2.0):
dependencies:
get-nonce: 1.0.1
@ -4545,6 +4941,14 @@ snapshots:
optionalDependencies:
'@types/react': 18.2.48
react-style-singleton@2.2.3(@types/react@18.2.48)(react@18.2.0):
dependencies:
get-nonce: 1.0.1
react: 18.2.0
tslib: 2.8.1
optionalDependencies:
'@types/react': 18.2.48
react-syntax-highlighter@15.5.0(react@18.2.0):
dependencies:
'@babel/runtime': 7.23.8
@ -4878,6 +5282,13 @@ snapshots:
optionalDependencies:
'@types/react': 18.2.48
use-callback-ref@1.3.3(@types/react@18.2.48)(react@18.2.0):
dependencies:
react: 18.2.0
tslib: 2.8.1
optionalDependencies:
'@types/react': 18.2.48
use-sidecar@1.1.2(@types/react@18.2.48)(react@18.2.0):
dependencies:
detect-node-es: 1.1.0
@ -4888,6 +5299,8 @@ snapshots:
util-deprecate@1.0.2: {}
uuid@11.0.3: {}
uuid@8.3.2: {}
which-boxed-primitive@1.0.2:

Loading…
Cancel
Save