Openstatus www.openstatus.dev

whatsapp notification (#1651)

* feat: whatsapp

* ci: apply automated fixes

* whatsapp

* ci: apply automated fixes

* small change

* typo

* use test template

* chore: minor updates

* add test button

* fix changelog

* why?

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Maximilian Kaske <maximilian@kaske.org>

authored by

Thibault Le Ouay
autofix-ci[bot]
Maximilian Kaske
and committed by
GitHub
175ecb24 26210b89

+849 -122
+1
apps/dashboard/package.json
··· 37 37 "@openstatus/notification-pagerduty": "workspace:*", 38 38 "@openstatus/notification-slack": "workspace:*", 39 39 "@openstatus/notification-telegram": "workspace:*", 40 + "@openstatus/notification-twillio-whatsapp": "workspace:*", 40 41 "@openstatus/notification-webhook": "workspace:*", 41 42 "@openstatus/react": "workspace:*", 42 43 "@openstatus/regions": "workspace:*",
+3 -1
apps/dashboard/src/app/(dashboard)/notifications/client.tsx
··· 93 93 94 94 if (key in workspace.limits) { 95 95 enabled = 96 - workspace.limits[key as "opsgenie" | "sms" | "opsgenie"]; 96 + workspace.limits[ 97 + key as "opsgenie" | "sms" | "opsgenie" | "whatsapp" 98 + ]; 97 99 } 98 100 99 101 if (limitReached) {
+231
apps/dashboard/src/components/forms/notifications/form-whatsapp.tsx
··· 1 + "use client"; 2 + 3 + import { Checkbox } from "@/components/ui/checkbox"; 4 + import { 5 + FormControl, 6 + FormDescription, 7 + FormField, 8 + FormItem, 9 + FormLabel, 10 + FormMessage, 11 + } from "@/components/ui/form"; 12 + 13 + import { 14 + FormCardContent, 15 + FormCardSeparator, 16 + } from "@/components/forms/form-card"; 17 + import { useFormSheetDirty } from "@/components/forms/form-sheet"; 18 + import { Button } from "@/components/ui/button"; 19 + import { Form } from "@/components/ui/form"; 20 + import { Input } from "@/components/ui/input"; 21 + import { Label } from "@/components/ui/label"; 22 + 23 + import { useTRPC } from "@/lib/trpc/client"; 24 + import { cn } from "@/lib/utils"; 25 + import { zodResolver } from "@hookform/resolvers/zod"; 26 + import { useMutation } from "@tanstack/react-query"; 27 + import { isTRPCClientError } from "@trpc/client"; 28 + import React, { useTransition } from "react"; 29 + import { useForm } from "react-hook-form"; 30 + import { toast } from "sonner"; 31 + import { z } from "zod"; 32 + 33 + const schema = z.object({ 34 + name: z.string(), 35 + provider: z.literal("whatsapp"), 36 + data: z.string(), 37 + monitors: z.array(z.number()), 38 + }); 39 + 40 + type FormValues = z.infer<typeof schema>; 41 + 42 + export function FormWhatsApp({ 43 + defaultValues, 44 + onSubmit, 45 + className, 46 + monitors, 47 + ...props 48 + }: Omit<React.ComponentProps<"form">, "onSubmit"> & { 49 + defaultValues?: FormValues; 50 + onSubmit: (values: FormValues) => Promise<void>; 51 + monitors: { id: number; name: string }[]; 52 + }) { 53 + const form = useForm<FormValues>({ 54 + resolver: zodResolver(schema), 55 + defaultValues: defaultValues ?? { 56 + name: "", 57 + provider: "whatsapp", 58 + data: "", 59 + monitors: [], 60 + }, 61 + }); 62 + const [isPending, startTransition] = useTransition(); 63 + const { setIsDirty } = useFormSheetDirty(); 64 + const trpc = useTRPC(); 65 + 66 + const sendTestMutation = useMutation( 67 + trpc.notification.sendTest.mutationOptions(), 68 + ); 69 + 70 + const formIsDirty = form.formState.isDirty; 71 + React.useEffect(() => { 72 + setIsDirty(formIsDirty); 73 + }, [formIsDirty, setIsDirty]); 74 + 75 + function submitAction(values: FormValues) { 76 + if (isPending) return; 77 + 78 + startTransition(async () => { 79 + try { 80 + const promise = onSubmit(values); 81 + toast.promise(promise, { 82 + loading: "Saving...", 83 + success: "Saved", 84 + error: (error) => { 85 + if (isTRPCClientError(error)) { 86 + return error.message; 87 + } 88 + return "Failed to save"; 89 + }, 90 + }); 91 + await promise; 92 + } catch (error) { 93 + console.error(error); 94 + } 95 + }); 96 + } 97 + 98 + function testAction() { 99 + if (isPending) return; 100 + 101 + startTransition(async () => { 102 + try { 103 + const provider = form.getValues("provider"); 104 + const data = form.getValues("data"); 105 + const promise = sendTestMutation.mutateAsync({ 106 + provider, 107 + data: { 108 + whatsapp: data, 109 + }, 110 + }); 111 + toast.promise(promise, { 112 + loading: "Sending test...", 113 + success: "Test sent", 114 + error: (error) => { 115 + if (error instanceof Error) { 116 + return error.message; 117 + } 118 + return "Failed to send test"; 119 + }, 120 + }); 121 + await promise; 122 + } catch (error) { 123 + console.error(error); 124 + } 125 + }); 126 + } 127 + 128 + return ( 129 + <Form {...form}> 130 + <form 131 + className={cn("grid gap-4", className)} 132 + onSubmit={form.handleSubmit(submitAction)} 133 + {...props} 134 + > 135 + <FormCardContent className="grid gap-4"> 136 + <FormField 137 + control={form.control} 138 + name="name" 139 + render={({ field }) => ( 140 + <FormItem> 141 + <FormLabel>Name</FormLabel> 142 + <FormControl> 143 + <Input placeholder="My Notifier" {...field} /> 144 + </FormControl> 145 + <FormMessage /> 146 + <FormDescription> 147 + Enter a descriptive name for your notifier. 148 + </FormDescription> 149 + </FormItem> 150 + )} 151 + /> 152 + <FormField 153 + control={form.control} 154 + name="data" 155 + render={({ field }) => ( 156 + <FormItem> 157 + <FormLabel>WhatsApp</FormLabel> 158 + <FormControl> 159 + <Input placeholder="+1234567890" type="tel" {...field} /> 160 + </FormControl> 161 + <FormMessage /> 162 + <FormDescription> 163 + Enter the phone number to send notifications to. 164 + </FormDescription> 165 + </FormItem> 166 + )} 167 + /> 168 + <div> 169 + <Button 170 + variant="outline" 171 + size="sm" 172 + type="button" 173 + onClick={testAction} 174 + > 175 + Send Test 176 + </Button> 177 + </div> 178 + </FormCardContent> 179 + <FormCardSeparator /> 180 + <FormCardContent> 181 + <FormField 182 + control={form.control} 183 + name="monitors" 184 + render={({ field }) => ( 185 + <FormItem> 186 + <FormLabel>Monitors</FormLabel> 187 + <FormDescription> 188 + Select the monitors you want to notify. 189 + </FormDescription> 190 + <div className="grid gap-3"> 191 + <div className="flex items-center gap-2"> 192 + <FormControl> 193 + <Checkbox 194 + id="all" 195 + checked={field.value?.length === monitors.length} 196 + onCheckedChange={(checked) => { 197 + field.onChange( 198 + checked ? monitors.map((m) => m.id) : [], 199 + ); 200 + }} 201 + /> 202 + </FormControl> 203 + <Label htmlFor="all">Select all</Label> 204 + </div> 205 + {monitors.map((item) => ( 206 + <div key={item.id} className="flex items-center gap-2"> 207 + <FormControl> 208 + <Checkbox 209 + id={String(item.id)} 210 + checked={field.value?.includes(item.id)} 211 + onCheckedChange={(checked) => { 212 + const newValue = checked 213 + ? [...(field.value || []), item.id] 214 + : field.value?.filter((id) => id !== item.id); 215 + field.onChange(newValue); 216 + }} 217 + /> 218 + </FormControl> 219 + <Label htmlFor={String(item.id)}>{item.name}</Label> 220 + </div> 221 + ))} 222 + </div> 223 + <FormMessage /> 224 + </FormItem> 225 + )} 226 + /> 227 + </FormCardContent> 228 + </form> 229 + </Form> 230 + ); 231 + }
+1
apps/dashboard/src/components/forms/notifications/form.tsx
··· 32 32 "pagerduty", 33 33 "ntfy", 34 34 "telegram", 35 + "whatsapp", 35 36 ]), 36 37 data: z.record(z.string(), z.string()).or(z.string()), 37 38 monitors: z.array(z.number()),
+9 -1
apps/dashboard/src/data/notifications.client.ts
··· 7 7 import { FormSms } from "@/components/forms/notifications/form-sms"; 8 8 import { FormTelegram } from "@/components/forms/notifications/form-telegram"; 9 9 import { FormWebhook } from "@/components/forms/notifications/form-webhook"; 10 - import { DiscordIcon, TelegramIcon } from "@openstatus/icons"; 10 + import { FormWhatsApp } from "@/components/forms/notifications/form-whatsapp"; 11 + import { DiscordIcon, TelegramIcon, WhatsappIcon } from "@openstatus/icons"; 11 12 import { OpsGenieIcon } from "@openstatus/icons"; 12 13 import { PagerDutyIcon } from "@openstatus/icons"; 13 14 import { SlackIcon } from "@openstatus/icons"; ··· 17 18 import { sendTest as sendTestPagerDuty } from "@openstatus/notification-pagerduty"; 18 19 import { sendTestSlackMessage as sendTestSlack } from "@openstatus/notification-slack"; 19 20 import { sendTest as sendTestTelegram } from "@openstatus/notification-telegram"; 21 + import { sendTest as sendWhatsAppTest } from "@openstatus/notification-twillio-whatsapp"; 20 22 import { sendTest as sendTestWebhook } from "@openstatus/notification-webhook"; 21 23 import { 22 24 BellIcon, ··· 111 113 label: "Telegram", 112 114 form: FormTelegram, 113 115 sendTest: sendTestTelegram, 116 + }, 117 + whatsapp: { 118 + icon: WhatsappIcon, 119 + label: "WhatsApp", 120 + form: FormWhatsApp, 121 + sendTest: sendWhatsAppTest, 114 122 }, 115 123 }; 116 124
+8 -3
apps/docs/src/content/docs/reference/notification.mdx
··· 22 22 23 23 ### SMS 24 24 25 - Require a phone number 25 + Require a phone number (international format e.g. +14155552671) 26 + 27 + Due to provider routing, SMS may not be delivered to all countries. Please contact support if you encounter any issues. Or use whatsapp notification instead. 28 + 29 + ### WhatsApp 30 + 31 + Require a phone number (international format e.g. +14155552671) 32 + 26 33 27 34 ### Telegram 28 35 ··· 31 38 > This integration is fully functional but accessing the chat id is not smooth yet. It requires manual effort to access your chat id. **You can ask RawDataBot `@raw_info_bot` for your chat id.** 32 39 33 40 The bot's id is `@openstatushq_bot` to make sure you are getting the correct notifications. 34 - 35 - Please 36 41 37 42 ### Webhook 38 43
apps/web/public/assets/changelog/whatsapp.png

This is a binary file and will not be displayed.

+10
apps/web/src/content/pages/changelog/whatsapp-notifications.mdx
··· 1 + --- 2 + title: "WhatsApp Notifications" 3 + description: "" 4 + image: "/assets/changelog/whatsapp.png" 5 + publishedAt: "2025-12-16" 6 + author: "openstatus" 7 + category: "notifications" 8 + --- 9 + 10 + You can now receive your uptime notifications via WhatsApp. It more reliable than SMS and works worldwide.
+2 -1
apps/web/src/content/pages/unrelated/pricing.mdx
··· 35 35 ["Password-protected", "", "+", "+"], 36 36 [<strong>Alerts</strong>, "", "", ""], 37 37 ["Slack, Discord, Email, Webhook, ntfy.sh", "+", "+", "+"], 38 + ["WhatsApp", "", "+", "+"], 38 39 ["SMS", "", "+", "+"], 39 40 ["PagerDuty", "", "+", "+"], 40 41 ["Number of notification channels", "1", "10", "20"], ··· 52 53 53 54 <ButtonLink href="https://app.openstatus.dev">Create Account</ButtonLink> 54 55 55 - --- 56 + ---
+1 -1
apps/workflows/.dockerignore
··· 1 - # This file is generated by Dofigen v2.5.0 1 + # This file is generated by Dofigen v2.5.1 2 2 # See https://github.com/lenra-io/dofigen 3 3 4 4 node_modules
+3 -2
apps/workflows/Dockerfile
··· 1 1 # syntax=docker/dockerfile:1.11 2 - # This file is generated by Dofigen v2.5.0 2 + # This file is generated by Dofigen v2.5.1 3 3 # See https://github.com/lenra-io/dofigen 4 4 5 5 # ca-certs ··· 41 41 --mount=type=bind,target=packages/notifications/pagerduty/package.json,source=packages/notifications/pagerduty/package.json \ 42 42 --mount=type=bind,target=packages/notifications/slack/package.json,source=packages/notifications/slack/package.json \ 43 43 --mount=type=bind,target=packages/notifications/telegram/package.json,source=packages/notifications/telegram/package.json \ 44 + --mount=type=bind,target=packages/notifications/twillio-whatsapp/package.json,source=packages/notifications/twillio-whatsapp/package.json \ 44 45 --mount=type=bind,target=packages/notifications/twillio-sms/package.json,source=packages/notifications/twillio-sms/package.json \ 45 46 --mount=type=bind,target=packages/notifications/webhook/package.json,source=packages/notifications/webhook/package.json \ 46 47 --mount=type=bind,target=packages/regions/package.json,source=packages/regions/package.json \ ··· 84 85 # runtime 85 86 FROM debian@sha256:530a3348fc4b5734ffe1a137ddbcee6850154285251b53c3425c386ea8fac77b AS runtime 86 87 LABEL \ 87 - io.dofigen.version="2.5.0" \ 88 + io.dofigen.version="2.5.1" \ 88 89 org.opencontainers.image.authors="OpenStatus Team" \ 89 90 org.opencontainers.image.base.digest="sha256:530a3348fc4b5734ffe1a137ddbcee6850154285251b53c3425c386ea8fac77b" \ 90 91 org.opencontainers.image.base.name="docker.io/debian:bullseye-slim" \
+35 -32
apps/workflows/dofigen.lock
··· 12 12 - /packages/error 13 13 - /packages/tracker 14 14 builders: 15 - build: 15 + docker: 16 16 fromImage: 17 17 path: oven/bun 18 18 digest: sha256:fbf8e67e9d3b806c86be7a2f2e9bae801f2d9212a21db4dcf8cc9889f5a3c9c4 19 19 label: 20 20 org.opencontainers.image.base.name: docker.io/oven/bun:1.3.3 21 - org.opencontainers.image.stage: build 22 21 org.opencontainers.image.base.digest: sha256:fbf8e67e9d3b806c86be7a2f2e9bae801f2d9212a21db4dcf8cc9889f5a3c9c4 23 22 workdir: /app/apps/workflows 24 - env: 25 - NODE_ENV: production 26 23 copy: 27 24 - paths: 28 25 - . 29 26 target: /app/ 30 - - fromBuilder: install 27 + run: 28 + - bun run src/build-docker.ts 29 + libsql: 30 + fromImage: 31 + path: oven/bun 32 + digest: sha256:fbf8e67e9d3b806c86be7a2f2e9bae801f2d9212a21db4dcf8cc9889f5a3c9c4 33 + label: 34 + org.opencontainers.image.base.name: docker.io/oven/bun:1.3.3 35 + org.opencontainers.image.base.digest: sha256:fbf8e67e9d3b806c86be7a2f2e9bae801f2d9212a21db4dcf8cc9889f5a3c9c4 36 + workdir: /app/ 37 + copy: 38 + - fromBuilder: docker 31 39 paths: 32 - - /app/node_modules 33 - target: /app/node_modules 40 + - /app/apps/build-docker/package.json 41 + target: /app/package.json 34 42 run: 35 - - bun build --compile --target bun --sourcemap --format=cjs src/index.ts --outfile=app --external '@libsql/*' --external libsql 43 + - bun install 36 44 install: 37 45 fromImage: 38 46 path: oven/bun ··· 73 81 source: packages/notifications/slack/package.json 74 82 - target: packages/notifications/telegram/package.json 75 83 source: packages/notifications/telegram/package.json 84 + - target: packages/notifications/twillio-whatsapp/package.json 85 + source: packages/notifications/twillio-whatsapp/package.json 76 86 - target: packages/notifications/twillio-sms/package.json 77 87 source: packages/notifications/twillio-sms/package.json 78 88 - target: packages/notifications/webhook/package.json ··· 89 99 source: packages/upstash/package.json 90 100 - target: packages/theme-store/package.json 91 101 source: packages/theme-store/package.json 92 - docker: 102 + build: 93 103 fromImage: 94 104 path: oven/bun 95 105 digest: sha256:fbf8e67e9d3b806c86be7a2f2e9bae801f2d9212a21db4dcf8cc9889f5a3c9c4 96 106 label: 97 107 org.opencontainers.image.base.name: docker.io/oven/bun:1.3.3 98 108 org.opencontainers.image.base.digest: sha256:fbf8e67e9d3b806c86be7a2f2e9bae801f2d9212a21db4dcf8cc9889f5a3c9c4 109 + org.opencontainers.image.stage: build 99 110 workdir: /app/apps/workflows 111 + env: 112 + NODE_ENV: production 100 113 copy: 101 114 - paths: 102 115 - . 103 116 target: /app/ 117 + - fromBuilder: install 118 + paths: 119 + - /app/node_modules 120 + target: /app/node_modules 104 121 run: 105 - - bun run src/build-docker.ts 122 + - bun build --compile --target bun --sourcemap --format=cjs src/index.ts --outfile=app --external '@libsql/*' --external libsql 106 123 ca-certs: 107 124 fromImage: 108 125 path: debian ··· 112 129 org.opencontainers.image.base.digest: sha256:530a3348fc4b5734ffe1a137ddbcee6850154285251b53c3425c386ea8fac77b 113 130 run: 114 131 - apt update && apt install -y ca-certificates && update-ca-certificates 115 - libsql: 116 - fromImage: 117 - path: oven/bun 118 - digest: sha256:fbf8e67e9d3b806c86be7a2f2e9bae801f2d9212a21db4dcf8cc9889f5a3c9c4 119 - label: 120 - org.opencontainers.image.base.name: docker.io/oven/bun:1.3.3 121 - org.opencontainers.image.base.digest: sha256:fbf8e67e9d3b806c86be7a2f2e9bae801f2d9212a21db4dcf8cc9889f5a3c9c4 122 - workdir: /app/ 123 - copy: 124 - - fromBuilder: docker 125 - paths: 126 - - /app/apps/build-docker/package.json 127 - target: /app/package.json 128 - run: 129 - - bun install 130 132 fromImage: 131 133 path: debian 132 134 digest: sha256:530a3348fc4b5734ffe1a137ddbcee6850154285251b53c3425c386ea8fac77b 133 135 label: 136 + org.opencontainers.image.description: Background job processing and probe scheduling for OpenStatus 137 + org.opencontainers.image.authors: OpenStatus Team 138 + io.dofigen.version: 2.5.1 134 139 org.opencontainers.image.source: https://github.com/openstatusHQ/openstatus 140 + org.opencontainers.image.base.digest: sha256:530a3348fc4b5734ffe1a137ddbcee6850154285251b53c3425c386ea8fac77b 135 141 org.opencontainers.image.vendor: OpenStatus 136 - org.opencontainers.image.title: OpenStatus Workflows 137 142 org.opencontainers.image.base.name: docker.io/debian:bullseye-slim 138 - org.opencontainers.image.base.digest: sha256:530a3348fc4b5734ffe1a137ddbcee6850154285251b53c3425c386ea8fac77b 139 - org.opencontainers.image.authors: OpenStatus Team 140 - io.dofigen.version: 2.5.1 141 - org.opencontainers.image.description: Background job processing and probe scheduling for OpenStatus 143 + org.opencontainers.image.title: OpenStatus Workflows 142 144 workdir: /app/ 143 145 copy: 144 146 - fromBuilder: build ··· 174 176 digest: sha256:530a3348fc4b5734ffe1a137ddbcee6850154285251b53c3425c386ea8fac77b 175 177 resources: 176 178 dofigen.yml: 177 - hash: 297382ef47be04c67f610380fadff8e2b9333ab28594ff6e0512b75a23d4c3b0 179 + hash: 7e59e1efd94f649c01720f89c8966945798a8a0c4572e2fb783d6e406e5384c4 178 180 content: | 179 181 ignore: 180 182 - node_modules ··· 208 210 - packages/notifications/pagerduty/package.json 209 211 - packages/notifications/slack/package.json 210 212 - packages/notifications/telegram/package.json 213 + - packages/notifications/twillio-whatsapp/package.json 211 214 - packages/notifications/twillio-sms/package.json 212 215 - packages/notifications/webhook/package.json 213 216 - packages/regions/package.json ··· 284 287 - fromBuilder: ca-certs 285 288 source: /etc/ssl/certs/ca-certificates.crt 286 289 target: /etc/ssl/certs/ 287 - expose: 3000 290 + expose: "3000" 288 291 entrypoint: /app/apps/workflows/app
+2 -1
apps/workflows/dofigen.yml
··· 30 30 - packages/notifications/pagerduty/package.json 31 31 - packages/notifications/slack/package.json 32 32 - packages/notifications/telegram/package.json 33 + - packages/notifications/twillio-whatsapp/package.json 33 34 - packages/notifications/twillio-sms/package.json 34 35 - packages/notifications/webhook/package.json 35 36 - packages/regions/package.json ··· 106 107 - fromBuilder: ca-certs 107 108 source: /etc/ssl/certs/ca-certificates.crt 108 109 target: /etc/ssl/certs/ 109 - expose: 3000 110 + expose: "3000" 110 111 entrypoint: /app/apps/workflows/app
+1
apps/workflows/package.json
··· 21 21 "@openstatus/notification-slack": "workspace:*", 22 22 "@openstatus/notification-telegram": "workspace:*", 23 23 "@openstatus/notification-twillio-sms": "workspace:*", 24 + "@openstatus/notification-twillio-whatsapp": "workspace:*", 24 25 "@openstatus/notification-webhook": "workspace:*", 25 26 "@openstatus/regions": "workspace:*", 26 27 "@openstatus/tinybird": "workspace:*",
+10
apps/workflows/src/checker/utils.ts
··· 45 45 sendRecovery as sendSmsRecovery, 46 46 } from "@openstatus/notification-twillio-sms"; 47 47 import { 48 + sendAlert as sendWhatsappAlert, 49 + sendDegraded as sendWhatsappDegraded, 50 + sendRecovery as sendWhatsappRecovery, 51 + } from "@openstatus/notification-twillio-whatsapp"; 52 + import { 48 53 sendAlert as sendWebhookAlert, 49 54 sendDegraded as sendWebhookDegraded, 50 55 sendRecovery as sendWebhookRecovery, ··· 116 121 sendAlert: sendWebhookAlert, 117 122 sendRecovery: sendWebhookRecovery, 118 123 sendDegraded: sendWebhookDegraded, 124 + }, 125 + whatsapp: { 126 + sendAlert: sendWhatsappAlert, 127 + sendRecovery: sendWhatsappRecovery, 128 + sendDegraded: sendWhatsappDegraded, 119 129 }, 120 130 telegram: { 121 131 sendAlert: sendTelegramAlert,
+1
packages/api/package.json
··· 14 14 "@openstatus/emails": "workspace:*", 15 15 "@openstatus/error": "workspace:*", 16 16 "@openstatus/notification-telegram": "workspace:*", 17 + "@openstatus/notification-twillio-whatsapp": "workspace:*", 17 18 "@openstatus/regions": "workspace:*", 18 19 "@openstatus/tinybird": "workspace:*", 19 20 "@openstatus/upstash": "workspace:*",
+14
packages/api/src/router/notification.ts
··· 12 12 selectMonitorSchema, 13 13 selectNotificationSchema, 14 14 telegramDataSchema, 15 + whatsappDataSchema, 15 16 } from "@openstatus/db/src/schema"; 16 17 17 18 import { Events } from "@openstatus/analytics"; 18 19 import { SchemaError } from "@openstatus/error"; 19 20 import { sendTest as sendTelegramTest } from "@openstatus/notification-telegram"; 21 + import { sendTest as sendWhatsAppTest } from "@openstatus/notification-twillio-whatsapp"; 20 22 import { createTRPCRouter, protectedProcedure } from "../trpc"; 21 23 22 24 export const notificationRouter = createTRPCRouter({ ··· 473 475 await sendTelegramTest({ 474 476 chatId: _data.data.telegram.chatId, 475 477 }); 478 + 479 + return; 480 + } 481 + if (opts.input.provider === "whatsapp") { 482 + const _data = whatsappDataSchema.safeParse(opts.input.data); 483 + if (!_data.success) { 484 + throw new TRPCError({ 485 + code: "BAD_REQUEST", 486 + message: SchemaError.fromZod(_data.error, opts.input).message, 487 + }); 488 + } 489 + await sendWhatsAppTest({ phoneNumber: _data.data.whatsapp }); 476 490 477 491 return; 478 492 }
+3 -2
packages/db/src/schema/notifications/constants.ts
··· 1 1 export const notificationProvider = [ 2 - "email", 3 2 "discord", 3 + "email", 4 4 "ntfy", 5 5 "pagerduty", 6 6 "opsgenie", 7 7 "slack", 8 8 "sms", 9 + "telegram", 9 10 "webhook", 10 - "telegram", 11 + "whatsapp", 11 12 ] as const;
+16 -5
packages/db/src/schema/notifications/validation.ts
··· 66 66 telegram: z.object({ chatId: z.string() }), 67 67 }); 68 68 69 + export const whatsappDataSchema = z.object({ 70 + whatsapp: phoneSchema, 71 + }); 72 + 69 73 export const NotificationDataSchema = z.union([ 74 + discordDataSchema, 70 75 emailDataSchema, 76 + ntfyDataSchema, 77 + opsgenieDataSchema, 78 + pagerdutyDataSchema, 71 79 phoneDataSchema, 80 + telegramDataSchema, 72 81 slackDataSchema, 73 - discordDataSchema, 74 - pagerdutyDataSchema, 75 - opsgenieDataSchema, 76 - ntfyDataSchema, 77 82 webhookDataSchema, 78 - telegramDataSchema, 83 + whatsappDataSchema, 79 84 ]); 80 85 81 86 export const InsertNotificationWithDataSchema = z.discriminatedUnion( ··· 127 132 z.object({ 128 133 provider: z.literal("webhook"), 129 134 data: webhookDataSchema, 135 + }), 136 + ), 137 + insertNotificationSchema.merge( 138 + z.object({ 139 + provider: z.literal("whatsapp"), 140 + data: whatsappDataSchema, 130 141 }), 131 142 ), 132 143 ],
+3
packages/db/src/schema/plan/config.ts
··· 54 54 "audit-log": false, 55 55 regions: [...FREE_FLY_REGIONS], 56 56 "private-locations": false, 57 + whatsapp: false, 57 58 }, 58 59 }, 59 60 starter: { ··· 93 94 "audit-log": false, 94 95 regions: [...AVAILABLE_REGIONS], 95 96 "private-locations": false, 97 + whatsapp: true, 96 98 }, 97 99 }, 98 100 team: { ··· 132 134 "audit-log": true, 133 135 regions: [...AVAILABLE_REGIONS], 134 136 "private-locations": true, 137 + whatsapp: true, 135 138 }, 136 139 }, 137 140 };
+1
packages/db/src/schema/plan/schema.ts
··· 39 39 notifications: z.boolean().default(true), 40 40 pagerduty: z.boolean().default(false), 41 41 opsgenie: z.boolean().default(false), 42 + whatsapp: z.boolean().default(false), 42 43 sms: z.boolean().default(false), 43 44 "sms-limit": z.number().default(0), 44 45 "notification-channels": z.number().default(1),
+1
packages/icons/src/index.tsx
··· 8 8 export * from "./railway"; 9 9 export * from "./koyeb"; 10 10 export * from "./telegram"; 11 + export * from "./whatsapp";
+15
packages/icons/src/whatsapp.tsx
··· 1 + export function WhatsappIcon(props: React.ComponentProps<"svg">) { 2 + return ( 3 + <svg 4 + role="img" 5 + viewBox="0 0 24 24" 6 + xmlns="http://www.w3.org/2000/svg" 7 + stroke="currentColor" 8 + fill="currentColor" 9 + {...props} 10 + > 11 + <title>WhatsApp</title> 12 + <path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413Z" /> 13 + </svg> 14 + ); 15 + }
+2
packages/notifications/twillio-whatsapp/.env.example
··· 1 + TWILLIO_AUTH_TOKEN=your_auth_token 2 + TWILLIO_ACCOUNT_ID=your_account_id
+21
packages/notifications/twillio-whatsapp/package.json
··· 1 + { 2 + "name": "@openstatus/notification-twillio-whatsapp", 3 + "version": "0.0.0", 4 + "main": "src/index.ts", 5 + "scripts": { 6 + "test": "bun test" 7 + }, 8 + "dependencies": { 9 + "@openstatus/db": "workspace:*", 10 + "@t3-oss/env-core": "0.7.1", 11 + "validator": "13.12.0", 12 + "zod": "3.25.76" 13 + }, 14 + "devDependencies": { 15 + "@openstatus/tsconfig": "workspace:*", 16 + "@types/node": "22.10.2", 17 + "@types/validator": "13.12.0", 18 + "bun-types": "1.3.1", 19 + "typescript": "5.7.2" 20 + } 21 + }
+30
packages/notifications/twillio-whatsapp/src/env.ts
··· 1 + import { createEnv } from "@t3-oss/env-core"; 2 + import { z } from "zod"; 3 + 4 + export const env = createEnv({ 5 + server: { 6 + TWILLIO_AUTH_TOKEN: z.string(), 7 + TWILLIO_ACCOUNT_ID: z.string(), 8 + }, 9 + 10 + /** 11 + * What object holds the environment variables at runtime. This is usually 12 + * `process.env` or `import.meta.env`. 13 + */ 14 + runtimeEnv: process.env, 15 + 16 + /** 17 + * By default, this library will feed the environment variables directly to 18 + * the Zod validator. 19 + * 20 + * This means that if you have an empty string for a value that is supposed 21 + * to be a number (e.g. `PORT=` in a ".env" file), Zod will incorrectly flag 22 + * it as a type mismatch violation. Additionally, if you have an empty string 23 + * for a value that is supposed to be a string with a default value (e.g. 24 + * `DOMAIN=` in an ".env" file), the default value will never be applied. 25 + * 26 + * In order to solve these issues, we recommend that all new projects 27 + * explicitly specify this option as true. 28 + */ 29 + skipValidation: true, 30 + });
+132
packages/notifications/twillio-whatsapp/src/index.test.ts
··· 1 + import { 2 + afterEach, 3 + beforeEach, 4 + describe, 5 + expect, 6 + jest, 7 + spyOn, 8 + test, 9 + } from "bun:test"; 10 + import { selectNotificationSchema } from "@openstatus/db/src/schema"; 11 + import { sendAlert, sendDegraded, sendRecovery } from "./index"; 12 + 13 + describe("whatsapp Notifications", () => { 14 + // biome-ignore lint/suspicious/noExplicitAny: <explanation> 15 + let fetchMock: any = undefined; 16 + 17 + beforeEach(() => { 18 + // @ts-expect-error 19 + fetchMock = spyOn(global, "fetch").mockImplementation(() => 20 + Promise.resolve(new Response(null, { status: 200 })), 21 + ); 22 + }); 23 + 24 + afterEach(() => { 25 + jest.resetAllMocks(); 26 + }); 27 + 28 + test("Send degraded", async () => { 29 + const monitor = { 30 + id: "monitor-1", 31 + name: "API Health Check", 32 + url: "https://api.example.com/health", 33 + jobType: "http" as const, 34 + periodicity: "5m" as const, 35 + status: "active" as const, // or "down", "degraded" 36 + createdAt: new Date(), 37 + updatedAt: new Date(), 38 + region: "us-east-1", 39 + }; 40 + 41 + const a = { 42 + id: 1, 43 + name: "slack Notification", 44 + provider: "slack", 45 + workspaceId: 1, 46 + createdAt: new Date(), 47 + updatedAt: new Date(), 48 + data: '{"whatsapp":"+33623456789"}', 49 + }; 50 + 51 + const n = selectNotificationSchema.parse(a); 52 + await sendDegraded({ 53 + // @ts-expect-error 54 + monitor, 55 + notification: n, 56 + statusCode: 500, 57 + message: "Something went wrong", 58 + cronTimestamp: Date.now(), 59 + }); 60 + expect(fetchMock).toHaveBeenCalled(); 61 + }); 62 + 63 + test("Send Recovered", async () => { 64 + const monitor = { 65 + id: "monitor-1", 66 + name: "API Health Check", 67 + url: "https://api.example.com/health", 68 + jobType: "http" as const, 69 + periodicity: "5m" as const, 70 + status: "active" as const, // or "down", "degraded" 71 + createdAt: new Date(), 72 + updatedAt: new Date(), 73 + region: "us-east-1", 74 + }; 75 + 76 + const a = { 77 + id: 1, 78 + name: "slack Notification", 79 + provider: "slack", 80 + workspaceId: 1, 81 + createdAt: new Date(), 82 + updatedAt: new Date(), 83 + data: '{"whatsapp":"+33623456789"}', 84 + }; 85 + 86 + const n = selectNotificationSchema.parse(a); 87 + await sendRecovery({ 88 + // @ts-expect-error 89 + monitor, 90 + notification: n, 91 + statusCode: 500, 92 + message: "Something went wrong", 93 + cronTimestamp: Date.now(), 94 + }); 95 + expect(fetchMock).toHaveBeenCalled(); 96 + }); 97 + 98 + test("Send Alert", async () => { 99 + const monitor = { 100 + id: "monitor-1", 101 + name: "API Health Check", 102 + url: "https://api.example.com/health", 103 + jobType: "http" as const, 104 + periodicity: "5m" as const, 105 + status: "active" as const, // or "down", "degraded" 106 + createdAt: new Date(), 107 + updatedAt: new Date(), 108 + region: "us-east-1", 109 + }; 110 + const a = { 111 + id: 1, 112 + name: "slack Notification", 113 + provider: "slack", 114 + workspaceId: 1, 115 + createdAt: new Date(), 116 + updatedAt: new Date(), 117 + data: '{"whatsapp":"+33623456789"}', 118 + }; 119 + 120 + const n = selectNotificationSchema.parse(a); 121 + 122 + await sendAlert({ 123 + // @ts-expect-error 124 + monitor, 125 + notification: n, 126 + statusCode: 500, 127 + message: "Something went wrong", 128 + cronTimestamp: Date.now(), 129 + }); 130 + expect(fetchMock).toHaveBeenCalled(); 131 + }); 132 + });
+175
packages/notifications/twillio-whatsapp/src/index.ts
··· 1 + import type { Monitor, Notification } from "@openstatus/db/src/schema"; 2 + import { whatsappDataSchema } from "@openstatus/db/src/schema"; 3 + import type { Region } from "@openstatus/db/src/schema/constants"; 4 + import { env } from "./env"; 5 + 6 + export const sendAlert = async ({ 7 + monitor, 8 + notification, 9 + statusCode, 10 + message, 11 + // biome-ignore lint/correctness/noUnusedVariables: <explanation> 12 + incidentId, 13 + }: { 14 + monitor: Monitor; 15 + notification: Notification; 16 + statusCode?: number; 17 + message?: string; 18 + incidentId?: string; 19 + cronTimestamp: number; 20 + latency?: number; 21 + region?: Region; 22 + }) => { 23 + const notificationData = whatsappDataSchema.parse( 24 + JSON.parse(notification.data), 25 + ); 26 + const contentVariables = JSON.stringify({ url: monitor.url }); 27 + 28 + const body = new FormData(); 29 + body.set("To", `whatsapp:${notificationData.whatsapp}`); 30 + body.set("From", "whatsapp:+14807252613"); 31 + body.set("ContentSid", "HX8282106bfaecb7939e69f9c5564babe5"); 32 + body.set("ContentVariables", contentVariables); 33 + 34 + try { 35 + await fetch( 36 + `https://api.twilio.com/2010-04-01/Accounts/${env.TWILLIO_ACCOUNT_ID}/Messages.json`, 37 + { 38 + method: "post", 39 + body, 40 + headers: { 41 + Authorization: `Basic ${btoa( 42 + `${env.TWILLIO_ACCOUNT_ID}:${env.TWILLIO_AUTH_TOKEN}`, 43 + )}`, 44 + }, 45 + }, 46 + ); 47 + } catch (err) { 48 + console.log(err); 49 + // Do something 50 + } 51 + }; 52 + 53 + export const sendRecovery = async ({ 54 + monitor, 55 + notification, 56 + // biome-ignore lint/correctness/noUnusedVariables: <explanation> 57 + statusCode, 58 + // biome-ignore lint/correctness/noUnusedVariables: <explanation> 59 + message, 60 + // biome-ignore lint/correctness/noUnusedVariables: <explanation> 61 + incidentId, 62 + }: { 63 + monitor: Monitor; 64 + notification: Notification; 65 + statusCode?: number; 66 + message?: string; 67 + incidentId?: string; 68 + cronTimestamp: number; 69 + latency?: number; 70 + region?: Region; 71 + }) => { 72 + const notificationData = whatsappDataSchema.parse( 73 + JSON.parse(notification.data), 74 + ); 75 + const contentVariables = JSON.stringify({ url: monitor.url }); 76 + 77 + const body = new FormData(); 78 + body.set("To", `whatsapp:${notificationData.whatsapp}`); 79 + body.set("From", "whatsapp:+14807252613"); 80 + body.set("ContentSid", "HX8fdeb4201bed18ac8838b3c0135bbf28"); 81 + body.set("ContentVariables", contentVariables); 82 + 83 + try { 84 + await fetch( 85 + `https://api.twilio.com/2010-04-01/Accounts/${env.TWILLIO_ACCOUNT_ID}/Messages.json`, 86 + { 87 + method: "post", 88 + body, 89 + headers: { 90 + Authorization: `Basic ${btoa( 91 + `${env.TWILLIO_ACCOUNT_ID}:${env.TWILLIO_AUTH_TOKEN}`, 92 + )}`, 93 + }, 94 + }, 95 + ); 96 + } catch (err) { 97 + console.log(err); 98 + // Do something 99 + } 100 + }; 101 + 102 + export const sendDegraded = async ({ 103 + monitor, 104 + notification, 105 + // biome-ignore lint/correctness/noUnusedVariables: <explanation> 106 + statusCode, 107 + // biome-ignore lint/correctness/noUnusedVariables: <explanation> 108 + message, 109 + }: { 110 + monitor: Monitor; 111 + notification: Notification; 112 + statusCode?: number; 113 + message?: string; 114 + incidentId?: string; 115 + cronTimestamp: number; 116 + latency?: number; 117 + region?: Region; 118 + }) => { 119 + const notificationData = whatsappDataSchema.parse( 120 + JSON.parse(notification.data), 121 + ); 122 + const contentVariables = JSON.stringify({ url: monitor.url }); 123 + 124 + const body = new FormData(); 125 + body.set("To", `whatsapp:${notificationData.whatsapp}`); 126 + body.set("From", "whatsapp:+14807252613"); 127 + body.set("ContentSid", "HX35589f2e7ac8b8be63f4bd62a60e435f"); 128 + body.set("ContentVariables", contentVariables); 129 + 130 + try { 131 + await fetch( 132 + `https://api.twilio.com/2010-04-01/Accounts/${env.TWILLIO_ACCOUNT_ID}/Messages.json`, 133 + { 134 + method: "post", 135 + body, 136 + headers: { 137 + Authorization: `Basic ${btoa( 138 + `${env.TWILLIO_ACCOUNT_ID}:${env.TWILLIO_AUTH_TOKEN}`, 139 + )}`, 140 + }, 141 + }, 142 + ); 143 + } catch (err) { 144 + console.log(err); 145 + // Do something 146 + } 147 + }; 148 + 149 + export const sendTest = async ({ phoneNumber }: { phoneNumber: string }) => { 150 + const contentVariables = JSON.stringify({ url: "https://openstat.us" }); 151 + const body = new FormData(); 152 + body.set("To", `whatsapp:${phoneNumber}`); 153 + body.set("ContentSid", "HX36ac9074ebda4376c7d6ddd1690b5291"); 154 + body.set("From", "whatsapp:+14807252613"); 155 + body.set("ContentVariables", contentVariables); 156 + 157 + console.log("send data"); 158 + try { 159 + await fetch( 160 + `https://api.twilio.com/2010-04-01/Accounts/${env.TWILLIO_ACCOUNT_ID}/Messages.json`, 161 + { 162 + method: "post", 163 + body, 164 + headers: { 165 + Authorization: `Basic ${btoa( 166 + `${env.TWILLIO_ACCOUNT_ID}:${env.TWILLIO_AUTH_TOKEN}`, 167 + )}`, 168 + }, 169 + }, 170 + ); 171 + } catch (err) { 172 + console.log(err); 173 + // Do something 174 + } 175 + };
+7
packages/notifications/twillio-whatsapp/tsconfig.json
··· 1 + { 2 + "extends": "@openstatus/tsconfig/nextjs.json", 3 + "include": ["src", "*.ts"], 4 + "compilerOptions": { 5 + "types": ["bun-types"] 6 + } 7 + }
+111 -73
pnpm-lock.yaml
··· 110 110 '@openstatus/notification-telegram': 111 111 specifier: workspace:* 112 112 version: link:../../packages/notifications/telegram 113 + '@openstatus/notification-twillio-whatsapp': 114 + specifier: workspace:* 115 + version: link:../../packages/notifications/twillio-whatsapp 113 116 '@openstatus/notification-webhook': 114 117 specifier: workspace:* 115 118 version: link:../../packages/notifications/webhook ··· 1091 1094 '@openstatus/notification-twillio-sms': 1092 1095 specifier: workspace:* 1093 1096 version: link:../../packages/notifications/twillio-sms 1097 + '@openstatus/notification-twillio-whatsapp': 1098 + specifier: workspace:* 1099 + version: link:../../packages/notifications/twillio-whatsapp 1094 1100 '@openstatus/notification-webhook': 1095 1101 specifier: workspace:* 1096 1102 version: link:../../packages/notifications/webhook ··· 1174 1180 '@openstatus/notification-telegram': 1175 1181 specifier: workspace:* 1176 1182 version: link:../notifications/telegram 1183 + '@openstatus/notification-twillio-whatsapp': 1184 + specifier: workspace:* 1185 + version: link:../notifications/twillio-whatsapp 1177 1186 '@openstatus/regions': 1178 1187 specifier: workspace:* 1179 1188 version: link:../regions ··· 1642 1651 specifier: 5.7.2 1643 1652 version: 5.7.2 1644 1653 1654 + packages/notifications/twillio-whatsapp: 1655 + dependencies: 1656 + '@openstatus/db': 1657 + specifier: workspace:* 1658 + version: link:../../db 1659 + '@t3-oss/env-core': 1660 + specifier: 0.7.1 1661 + version: 0.7.1(typescript@5.7.2)(zod@3.25.76) 1662 + validator: 1663 + specifier: 13.12.0 1664 + version: 13.12.0 1665 + zod: 1666 + specifier: 3.25.76 1667 + version: 3.25.76 1668 + devDependencies: 1669 + '@openstatus/tsconfig': 1670 + specifier: workspace:* 1671 + version: link:../../tsconfig 1672 + '@types/node': 1673 + specifier: 22.10.2 1674 + version: 22.10.2 1675 + '@types/validator': 1676 + specifier: 13.12.0 1677 + version: 13.12.0 1678 + bun-types: 1679 + specifier: 1.3.1 1680 + version: 1.3.1(@types/react@19.2.2) 1681 + typescript: 1682 + specifier: 5.7.2 1683 + version: 5.7.2 1684 + 1645 1685 packages/notifications/webhook: 1646 1686 dependencies: 1647 1687 '@openstatus/db': ··· 2704 2744 '@ericcornelissen/bash-parser@0.5.2': 2705 2745 resolution: {integrity: sha512-4pIMTa1nEFfMXitv7oaNEWOdM+zpOZavesa5GaiWTgda6Zk32CFGxjUp/iIaN0PwgUW1yTq/fztSjbpE8SLGZQ==} 2706 2746 engines: {node: '>=4'} 2707 - deprecated: Support for this package will stop 2025-12-31 2708 2747 2709 2748 '@esbuild-kit/core-utils@3.3.2': 2710 2749 resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} ··· 6832 6871 '@upstash/kafka@1.3.3': 6833 6872 resolution: {integrity: sha512-CIr657FZuK+IMuwcxkj3oCB6xKO+LMlHd4BL4J/Lwbpj6+5YHO+5ZcpdMIQhbcemthJcRtE0gDUfZEnrfb3Rjg==} 6834 6873 engines: {node: '>=10'} 6835 - deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. 6836 6874 6837 6875 '@upstash/qstash@2.6.2': 6838 6876 resolution: {integrity: sha512-aB/1yqMJTRyOt7Go2Db1ZIVnmTPpsc2KGY5jpLVcegNtjksaPTJF6fmITxos5HVvsQhS8IB3gvF/+gQfRQlPLQ==} ··· 13385 13423 '@floating-ui/core': 1.7.3 13386 13424 '@floating-ui/utils': 0.2.10 13387 13425 13388 - '@floating-ui/react-dom@2.1.6(react-dom@19.0.0(react@19.2.2))(react@19.0.0)': 13426 + '@floating-ui/react-dom@2.1.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 13389 13427 dependencies: 13390 13428 '@floating-ui/dom': 1.7.4 13391 13429 react: 19.0.0 ··· 14540 14578 '@types/react': 19.2.2 14541 14579 '@types/react-dom': 19.1.9(@types/react@19.2.2) 14542 14580 14543 - '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0)': 14581 + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 14544 14582 dependencies: 14545 - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 14583 + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 14546 14584 react: 19.0.0 14547 14585 react-dom: 19.0.0(react@19.0.0) 14548 14586 optionalDependencies: ··· 14631 14669 '@types/react': 19.2.2 14632 14670 '@types/react-dom': 19.2.2(@types/react@19.2.2) 14633 14671 14634 - '@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0)': 14672 + '@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 14635 14673 dependencies: 14636 14674 '@radix-ui/primitive': 1.1.3 14637 14675 '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.10)(react@19.0.0) 14638 14676 '@radix-ui/react-context': 1.1.2(@types/react@19.0.10)(react@19.0.0) 14639 14677 '@radix-ui/react-id': 1.1.1(@types/react@19.0.10)(react@19.0.0) 14640 - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 14641 - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 14678 + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 14679 + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 14642 14680 '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.0.10)(react@19.0.0) 14643 14681 '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.0.10)(react@19.0.0) 14644 14682 react: 19.0.0 ··· 14675 14713 '@types/react': 19.2.2 14676 14714 '@types/react-dom': 19.1.9(@types/react@19.2.2) 14677 14715 14678 - '@radix-ui/react-collection@1.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0)': 14716 + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 14679 14717 dependencies: 14680 14718 '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.10)(react@19.0.0) 14681 14719 '@radix-ui/react-context': 1.1.2(@types/react@19.0.10)(react@19.0.0) 14682 - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 14720 + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 14683 14721 '@radix-ui/react-slot': 1.2.3(@types/react@19.0.10)(react@19.0.0) 14684 14722 react: 19.0.0 14685 14723 react-dom: 19.0.0(react@19.0.0) ··· 14859 14897 '@types/react': 19.2.2 14860 14898 '@types/react-dom': 19.2.2(@types/react@19.2.2) 14861 14899 14862 - '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0)': 14900 + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 14863 14901 dependencies: 14864 14902 '@radix-ui/primitive': 1.1.3 14865 14903 '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.10)(react@19.0.0) 14866 - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 14904 + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 14867 14905 '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.0.10)(react@19.0.0) 14868 14906 '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.0.10)(react@19.0.0) 14869 14907 react: 19.0.0 ··· 14900 14938 '@types/react': 19.2.2 14901 14939 '@types/react-dom': 19.2.2(@types/react@19.2.2) 14902 14940 14903 - '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0)': 14941 + '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 14904 14942 dependencies: 14905 14943 '@radix-ui/primitive': 1.1.3 14906 14944 '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.10)(react@19.0.0) 14907 14945 '@radix-ui/react-context': 1.1.2(@types/react@19.0.10)(react@19.0.0) 14908 14946 '@radix-ui/react-id': 1.1.1(@types/react@19.0.10)(react@19.0.0) 14909 - '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 14910 - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 14947 + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 14948 + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 14911 14949 '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.0.10)(react@19.0.0) 14912 14950 react: 19.0.0 14913 14951 react-dom: 19.0.0(react@19.0.0) ··· 14959 14997 '@types/react': 19.2.2 14960 14998 '@types/react-dom': 19.1.9(@types/react@19.2.2) 14961 14999 14962 - '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0)': 15000 + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 14963 15001 dependencies: 14964 15002 '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.10)(react@19.0.0) 14965 - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 15003 + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 14966 15004 '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.0.10)(react@19.0.0) 14967 15005 react: 19.0.0 14968 15006 react-dom: 19.0.0(react@19.0.0) ··· 15091 15129 '@types/react': 19.2.2 15092 15130 '@types/react-dom': 19.2.2(@types/react@19.2.2) 15093 15131 15094 - '@radix-ui/react-menu@2.1.16(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0)': 15132 + '@radix-ui/react-menu@2.1.16(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 15095 15133 dependencies: 15096 15134 '@radix-ui/primitive': 1.1.3 15097 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 15135 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 15098 15136 '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.10)(react@19.0.0) 15099 15137 '@radix-ui/react-context': 1.1.2(@types/react@19.0.10)(react@19.0.0) 15100 15138 '@radix-ui/react-direction': 1.1.1(@types/react@19.0.10)(react@19.0.0) 15101 - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 15139 + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 15102 15140 '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.0.10)(react@19.0.0) 15103 - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 15141 + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 15104 15142 '@radix-ui/react-id': 1.1.1(@types/react@19.0.10)(react@19.0.0) 15105 - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 15106 - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 15107 - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 15108 - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 15109 - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 15143 + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 15144 + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 15145 + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 15146 + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 15147 + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 15110 15148 '@radix-ui/react-slot': 1.2.3(@types/react@19.0.10)(react@19.0.0) 15111 15149 '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.0.10)(react@19.0.0) 15112 15150 aria-hidden: 1.2.6 ··· 15188 15226 '@types/react': 19.2.2 15189 15227 '@types/react-dom': 19.2.2(@types/react@19.2.2) 15190 15228 15191 - '@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0)': 15229 + '@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 15192 15230 dependencies: 15193 15231 '@radix-ui/primitive': 1.1.3 15194 15232 '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.10)(react@19.0.0) 15195 15233 '@radix-ui/react-context': 1.1.2(@types/react@19.0.10)(react@19.0.0) 15196 - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 15234 + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 15197 15235 '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.0.10)(react@19.0.0) 15198 - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 15236 + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 15199 15237 '@radix-ui/react-id': 1.1.1(@types/react@19.0.10)(react@19.0.0) 15200 - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 15201 - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 15202 - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 15203 - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 15238 + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 15239 + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 15240 + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 15241 + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 15204 15242 '@radix-ui/react-slot': 1.2.3(@types/react@19.0.10)(react@19.0.0) 15205 15243 '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.0.10)(react@19.0.0) 15206 15244 aria-hidden: 1.2.6 ··· 15270 15308 '@types/react': 19.2.2 15271 15309 '@types/react-dom': 19.2.2(@types/react@19.2.2) 15272 15310 15273 - '@radix-ui/react-popper@1.2.8(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0)': 15311 + '@radix-ui/react-popper@1.2.8(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 15274 15312 dependencies: 15275 - '@floating-ui/react-dom': 2.1.6(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 15276 - '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 15313 + '@floating-ui/react-dom': 2.1.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 15314 + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 15277 15315 '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.10)(react@19.0.0) 15278 15316 '@radix-ui/react-context': 1.1.2(@types/react@19.0.10)(react@19.0.0) 15279 - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 15317 + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 15280 15318 '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.0.10)(react@19.0.0) 15281 15319 '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.0.10)(react@19.0.0) 15282 15320 '@radix-ui/react-use-rect': 1.1.1(@types/react@19.0.10)(react@19.0.0) ··· 15298 15336 '@types/react': 19.2.2 15299 15337 '@types/react-dom': 19.1.9(@types/react@19.2.2) 15300 15338 15301 - '@radix-ui/react-portal@1.1.9(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0)': 15339 + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 15302 15340 dependencies: 15303 - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 15341 + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 15304 15342 '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.0.10)(react@19.0.0) 15305 15343 react: 19.0.0 15306 15344 react-dom: 19.0.0(react@19.0.0) ··· 15358 15396 '@types/react': 19.2.2 15359 15397 '@types/react-dom': 19.2.2(@types/react@19.2.2) 15360 15398 15361 - '@radix-ui/react-presence@1.1.5(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0)': 15399 + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 15362 15400 dependencies: 15363 15401 '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.10)(react@19.0.0) 15364 15402 '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.0.10)(react@19.0.0) ··· 15377 15415 '@types/react': 19.2.2 15378 15416 '@types/react-dom': 19.1.9(@types/react@19.2.2) 15379 15417 15380 - '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0)': 15418 + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 15381 15419 dependencies: 15382 15420 '@radix-ui/react-slot': 1.2.3(@types/react@19.0.10)(react@19.0.0) 15383 15421 react: 19.0.0 ··· 15512 15550 '@types/react': 19.2.2 15513 15551 '@types/react-dom': 19.2.2(@types/react@19.2.2) 15514 15552 15515 - '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0)': 15553 + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 15516 15554 dependencies: 15517 15555 '@radix-ui/primitive': 1.1.3 15518 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 15556 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 15519 15557 '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.10)(react@19.0.0) 15520 15558 '@radix-ui/react-context': 1.1.2(@types/react@19.0.10)(react@19.0.0) 15521 15559 '@radix-ui/react-direction': 1.1.1(@types/react@19.0.10)(react@19.0.0) 15522 15560 '@radix-ui/react-id': 1.1.1(@types/react@19.0.10)(react@19.0.0) 15523 - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 15561 + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 15524 15562 '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.0.10)(react@19.0.0) 15525 15563 '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.0.10)(react@19.0.0) 15526 15564 react: 19.0.0 ··· 15717 15755 '@types/react': 19.2.2 15718 15756 '@types/react-dom': 19.2.2(@types/react@19.2.2) 15719 15757 15720 - '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0)': 15758 + '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 15721 15759 dependencies: 15722 15760 '@radix-ui/primitive': 1.1.3 15723 15761 '@radix-ui/react-context': 1.1.2(@types/react@19.0.10)(react@19.0.0) 15724 15762 '@radix-ui/react-direction': 1.1.1(@types/react@19.0.10)(react@19.0.0) 15725 15763 '@radix-ui/react-id': 1.1.1(@types/react@19.0.10)(react@19.0.0) 15726 - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 15727 - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 15728 - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 15764 + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 15765 + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 15766 + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 15729 15767 '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.0.10)(react@19.0.0) 15730 15768 react: 19.0.0 15731 15769 react-dom: 19.0.0(react@19.0.0) ··· 15749 15787 '@types/react': 19.2.2 15750 15788 '@types/react-dom': 19.1.9(@types/react@19.2.2) 15751 15789 15752 - '@radix-ui/react-toggle-group@1.1.11(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0)': 15790 + '@radix-ui/react-toggle-group@1.1.11(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 15753 15791 dependencies: 15754 15792 '@radix-ui/primitive': 1.1.3 15755 15793 '@radix-ui/react-context': 1.1.2(@types/react@19.0.10)(react@19.0.0) 15756 15794 '@radix-ui/react-direction': 1.1.1(@types/react@19.0.10)(react@19.0.0) 15757 - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 15758 - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 15759 - '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 15795 + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 15796 + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 15797 + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 15760 15798 '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.0.10)(react@19.0.0) 15761 15799 react: 19.0.0 15762 15800 react-dom: 19.0.0(react@19.0.0) ··· 15775 15813 '@types/react': 19.2.2 15776 15814 '@types/react-dom': 19.1.9(@types/react@19.2.2) 15777 15815 15778 - '@radix-ui/react-toggle@1.1.10(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0)': 15816 + '@radix-ui/react-toggle@1.1.10(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 15779 15817 dependencies: 15780 15818 '@radix-ui/primitive': 1.1.3 15781 - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 15819 + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 15782 15820 '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.0.10)(react@19.0.0) 15783 15821 react: 19.0.0 15784 15822 react-dom: 19.0.0(react@19.0.0) ··· 15826 15864 '@types/react': 19.2.2 15827 15865 '@types/react-dom': 19.2.2(@types/react@19.2.2) 15828 15866 15829 - '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0)': 15867 + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 15830 15868 dependencies: 15831 15869 '@radix-ui/primitive': 1.1.3 15832 15870 '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.10)(react@19.0.0) 15833 15871 '@radix-ui/react-context': 1.1.2(@types/react@19.0.10)(react@19.0.0) 15834 - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 15872 + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 15835 15873 '@radix-ui/react-id': 1.1.1(@types/react@19.0.10)(react@19.0.0) 15836 - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 15837 - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 15838 - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 15839 - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 15874 + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 15875 + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 15876 + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 15877 + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 15840 15878 '@radix-ui/react-slot': 1.2.3(@types/react@19.0.10)(react@19.0.0) 15841 15879 '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.0.10)(react@19.0.0) 15842 - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 15880 + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 15843 15881 react: 19.0.0 15844 15882 react-dom: 19.0.0(react@19.0.0) 15845 15883 optionalDependencies: ··· 16010 16048 '@types/react': 19.2.2 16011 16049 '@types/react-dom': 19.1.9(@types/react@19.2.2) 16012 16050 16013 - '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0)': 16051 + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 16014 16052 dependencies: 16015 - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 16053 + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 16016 16054 react: 19.0.0 16017 16055 react-dom: 19.0.0(react@19.0.0) 16018 16056 optionalDependencies: ··· 16153 16191 '@babel/traverse': 7.27.0 16154 16192 '@lottiefiles/dotlottie-react': 0.13.3(react@19.0.0) 16155 16193 '@radix-ui/colors': 3.0.0 16156 - '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 16157 - '@radix-ui/react-dropdown-menu': 2.1.16(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 16158 - '@radix-ui/react-popover': 1.1.15(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 16194 + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 16195 + '@radix-ui/react-dropdown-menu': 2.1.16(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 16196 + '@radix-ui/react-popover': 1.1.15(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 16159 16197 '@radix-ui/react-slot': 1.2.3(@types/react@19.0.10)(react@19.0.0) 16160 - '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 16161 - '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 16162 - '@radix-ui/react-tooltip': 1.2.8(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 16198 + '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 16199 + '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 16200 + '@radix-ui/react-tooltip': 1.2.8(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 16163 16201 '@types/node': 22.14.1 16164 16202 '@types/normalize-path': 3.0.2 16165 16203 '@types/react': 19.0.10 ··· 16168 16206 autoprefixer: 10.4.21(postcss@8.4.38) 16169 16207 clsx: 2.1.1 16170 16208 esbuild: 0.25.10 16171 - framer-motion: 12.23.22(@emotion/is-prop-valid@1.4.0)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 16209 + framer-motion: 12.23.22(@emotion/is-prop-valid@1.4.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 16172 16210 json5: 2.2.3 16173 16211 log-symbols: 4.1.0 16174 16212 module-punycode: punycode@2.3.1 ··· 16181 16219 react-dom: 19.0.0(react@19.0.0) 16182 16220 sharp: 0.34.4 16183 16221 socket.io-client: 4.8.1 16184 - sonner: 2.0.3(react-dom@19.0.0(react@19.2.2))(react@19.0.0) 16222 + sonner: 2.0.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 16185 16223 source-map-js: 1.2.1 16186 16224 spamc: 0.0.5 16187 16225 stacktrace-parser: 0.1.11 ··· 19175 19213 19176 19214 fraction.js@4.3.7: {} 19177 19215 19178 - framer-motion@12.23.22(@emotion/is-prop-valid@1.4.0)(react-dom@19.0.0(react@19.2.2))(react@19.0.0): 19216 + framer-motion@12.23.22(@emotion/is-prop-valid@1.4.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): 19179 19217 dependencies: 19180 19218 motion-dom: 12.23.23 19181 19219 motion-utils: 12.23.6 ··· 22433 22471 react: 19.2.2 22434 22472 react-dom: 19.2.2(react@19.2.2) 22435 22473 22436 - sonner@2.0.3(react-dom@19.0.0(react@19.2.2))(react@19.0.0): 22474 + sonner@2.0.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0): 22437 22475 dependencies: 22438 22476 react: 19.0.0 22439 22477 react-dom: 19.0.0(react@19.0.0)