diff --git a/apps/website/app/[locale]/page.tsx b/apps/website/app/[locale]/page.tsx index c3b5ce1..3faac8b 100644 --- a/apps/website/app/[locale]/page.tsx +++ b/apps/website/app/[locale]/page.tsx @@ -6,6 +6,7 @@ import { Testimonials } from "@/components/Testimonials"; import { FeaturesSectionDemo } from "@/components/features"; import { Pricing } from "@/components/pricing"; import { RippleDemo } from "@/components/sponsors"; +import { StatsSection } from "@/components/stats"; export default function Home() { return ( @@ -14,12 +15,12 @@ export default function Home() { +
- diff --git a/apps/website/components/CallToAction.tsx b/apps/website/components/CallToAction.tsx index 8c1d797..4f95f63 100644 --- a/apps/website/components/CallToAction.tsx +++ b/apps/website/components/CallToAction.tsx @@ -9,7 +9,7 @@ export function CallToAction() { return (
{t("navigation.docs")} + +
+ {t("navigation.dashboard")} + +
+ + + @@ -152,7 +166,7 @@ export function Header() { -
+
*/} -
*/} +
+
+
+ +
+ + 🎉
{" "} + -
-
-
- -
+ + + + {t("hero.deploy")}{" "} + + + {t("hero.anywhere")} + {" "} + {t("hero.with")} + + + {t("hero.des")} + + +
+
+ + curl -sSL https://dokploy.com/install.sh | sh + + {isCopied ? ( + + ) : ( + + )} + + +
+
+ + +
+
+
+ +
+
- -
- -
-
+
- + + {/* */} +
); } diff --git a/apps/website/components/sponsors.tsx b/apps/website/components/sponsors.tsx index 2d83582..8f2e7aa 100644 --- a/apps/website/components/sponsors.tsx +++ b/apps/website/components/sponsors.tsx @@ -1,12 +1,20 @@ +"use client"; import { PlusCircleIcon } from "lucide-react"; import { useTranslations } from "next-intl"; -import { Button } from "./ui/button"; +import Link from "next/link"; +import { buttonVariants } from "./ui/button"; import Ripple from "./ui/ripple"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "./ui/tooltip"; export const RippleDemo = () => { const t = useTranslations("HomePage"); return ( -
+

{t("hero.sponsors.title")} @@ -15,13 +23,27 @@ export const RippleDemo = () => { {t("hero.sponsors.description")}

-
-

- - {/* */} -

+
+ + + + + + + + + Become a sponsor 🤑 + + +
diff --git a/apps/website/components/stats.tsx b/apps/website/components/stats.tsx new file mode 100644 index 0000000..e3a6f2f --- /dev/null +++ b/apps/website/components/stats.tsx @@ -0,0 +1,172 @@ +import { GithubIcon, HandCoins, Users } from "lucide-react"; +import React from "react"; +import { useId } from "react"; +import NumberTicker from "./ui/number-ticker"; + +export function StatsSection() { + return ( +
+
+

+ Stats You Didn’t Ask For (But Secretly Love to See) +

+

+ Just a few numbers to show we’re not *completely* making this up. + Turns out, Dokploy has actually helped a few people—who knew? +

+
+
+ {grid.map((feature, index) => ( +
+ + +

+ {feature.title} + {feature.icon} +

+

+ {feature.description} +

+ {feature.component} +
+ ))} +
+
+ ); +} + +const grid = [ + { + title: "GitHub Stars", + description: + "With over 8.3k stars on GitHub, Dokploy is trusted by developers worldwide. Explore our repositories and join our community!", + icon: ( + + ), + component: ( +

+ + +

+ ), + }, + { + title: "DockerHub Downloads", + description: + "Downloaded over 700,000 times, Dokploy has become a go-to solution for seamless deployments. Discover our presence on DockerHub.", + icon: ( + + + + ), + component: ( +

+ + +

+ ), + }, + { + title: "Community Contributors", + description: + "Thanks to a growing base of over 65 contributors, Dokploy continues to thrive with valuable contributions from developers around the world.", + icon: , + component: ( +

+ + +

+ ), + }, + { + title: "Sponsors", + description: + "More than 35 companies/individuals have sponsored Dokploy, ensuring a steady flow of support and resources. Join our community!", + icon: , + component: ( +

+ + +

+ ), + }, +]; + +export const Grid = ({ + pattern, + size, +}: { + pattern?: number[][]; + size?: number; +}) => { + const p = pattern ?? [ + [Math.floor(Math.random() * 4) + 7, Math.floor(Math.random() * 6) + 1], + [Math.floor(Math.random() * 4) + 7, Math.floor(Math.random() * 6) + 1], + [Math.floor(Math.random() * 4) + 7, Math.floor(Math.random() * 6) + 1], + [Math.floor(Math.random() * 4) + 7, Math.floor(Math.random() * 6) + 1], + [Math.floor(Math.random() * 4) + 7, Math.floor(Math.random() * 6) + 1], + ]; + return ( +
+
+ +
+
+ ); +}; + +export function GridPattern({ width, height, x, y, squares, ...props }: any) { + const patternId = useId(); + + return ( + + ); +} diff --git a/apps/website/components/ui/animated-grid-pattern.tsx b/apps/website/components/ui/animated-grid-pattern.tsx new file mode 100644 index 0000000..c31c9fa --- /dev/null +++ b/apps/website/components/ui/animated-grid-pattern.tsx @@ -0,0 +1,150 @@ +"use client"; + +import { motion } from "framer-motion"; +import { useEffect, useId, useRef, useState } from "react"; + +import { cn } from "@/lib/utils"; + +interface GridPatternProps { + width?: number; + height?: number; + x?: number; + y?: number; + strokeDasharray?: any; + numSquares?: number; + className?: string; + maxOpacity?: number; + duration?: number; + repeatDelay?: number; +} + +export function GridPattern({ + width = 40, + height = 40, + x = -1, + y = -1, + strokeDasharray = 0, + numSquares = 50, + className, + maxOpacity = 0.5, + duration = 4, + repeatDelay = 0.5, + ...props +}: GridPatternProps) { + const id = useId(); + const containerRef = useRef(null); + const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); + const [squares, setSquares] = useState(() => generateSquares(numSquares)); + + function getPos() { + return [ + Math.floor((Math.random() * dimensions.width) / width), + Math.floor((Math.random() * dimensions.height) / height), + ]; + } + + // Adjust the generateSquares function to return objects with an id, x, and y + function generateSquares(count: number) { + return Array.from({ length: count }, (_, i) => ({ + id: i, + pos: getPos(), + })); + } + + // Function to update a single square's position + const updateSquarePosition = (id: number) => { + setSquares((currentSquares) => + currentSquares.map((sq) => + sq.id === id + ? { + ...sq, + pos: getPos(), + } + : sq, + ), + ); + }; + + // Update squares to animate in + useEffect(() => { + if (dimensions.width && dimensions.height) { + setSquares(generateSquares(numSquares)); + } + }, [dimensions, numSquares]); + + // Resize observer to update container dimensions + useEffect(() => { + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + setDimensions({ + width: entry.contentRect.width, + height: entry.contentRect.height, + }); + } + }); + + if (containerRef.current) { + resizeObserver.observe(containerRef.current); + } + + return () => { + if (containerRef.current) { + resizeObserver.unobserve(containerRef.current); + } + }; + }, [containerRef]); + + return ( + + ); +} + +export default GridPattern; diff --git a/apps/website/components/ui/number-ticker.tsx b/apps/website/components/ui/number-ticker.tsx new file mode 100644 index 0000000..118d30f --- /dev/null +++ b/apps/website/components/ui/number-ticker.tsx @@ -0,0 +1,58 @@ +"use client"; + +import { useInView, useMotionValue, useSpring } from "framer-motion"; +import { useEffect, useRef } from "react"; + +import { cn } from "@/lib/utils"; + +export default function NumberTicker({ + value, + direction = "up", + delay = 0, + className, + decimalPlaces = 0, +}: { + value: number; + direction?: "up" | "down"; + className?: string; + delay?: number; // delay in s + decimalPlaces?: number; +}) { + const ref = useRef(null); + const motionValue = useMotionValue(direction === "down" ? value : 0); + const springValue = useSpring(motionValue, { + damping: 60, + stiffness: 100, + }); + const isInView = useInView(ref, { once: true, margin: "0px" }); + + useEffect(() => { + isInView && + setTimeout(() => { + motionValue.set(direction === "down" ? 0 : value); + }, delay * 1000); + }, [motionValue, isInView, delay, value, direction]); + + useEffect( + () => + springValue.on("change", (latest) => { + if (ref.current) { + ref.current.textContent = Intl.NumberFormat("en-US", { + minimumFractionDigits: decimalPlaces, + maximumFractionDigits: decimalPlaces, + }).format(Number(latest.toFixed(decimalPlaces))); + } + }), + [springValue, decimalPlaces], + ); + + return ( + + ); +} diff --git a/apps/website/components/ui/ripple.tsx b/apps/website/components/ui/ripple.tsx index 7898b2a..0e69681 100644 --- a/apps/website/components/ui/ripple.tsx +++ b/apps/website/components/ui/ripple.tsx @@ -1,8 +1,15 @@ "use client"; -import React, { type CSSProperties } from "react"; +import React from "react"; import { cn } from "@/lib/utils"; +import Link from "next/link"; import { Avatar, AvatarFallback, AvatarImage } from "./avatar"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "./tooltip"; interface RippleProps { mainCircleSize?: number; @@ -11,37 +18,79 @@ interface RippleProps { className?: string; } +type AvatarItem = { + name: string; + image: string; + link: string; + type: "hero" | "premium" | "elite" | "supporting" | "community"; +}; + const Ripple = React.memo(function Ripple({ mainCircleSize = 210, mainCircleOpacity = 0.24, numCircles = 8, className, }: RippleProps) { - const avatarsFirstRing = [ - "https://github.com/shadcn.png", // URLs de los avatares del primer aro - "https://github.com/shadcn.png", - "https://github.com/shadcn.png", - "https://github.com/shadcn.png", - "https://github.com/shadcn.png", + const heroSponsors: AvatarItem[] = [ + { + name: "Hostinger", + image: "https://avatars.githubusercontent.com/u/2630767?s=200&v=4", + link: "https://www.hostinger.com/vps-hosting?ref=dokploy", + type: "hero", + }, + { + name: "Lxaer", + image: + "https://raw.githubusercontent.com/Dokploy/dokploy/canary/.github/sponsors/lxaer.png", + link: "https://www.lxaer.com?ref=dokploy", + type: "hero", + }, ]; - const avatarsSecondRing = [ - "https://github.com/shadcn.png", // URLs de los avatares del segundo aro - "https://github.com/shadcn.png", - "https://github.com/shadcn.png", - "https://github.com/shadcn.png", - "https://github.com/shadcn.png", - "https://github.com/shadcn.png", + const premiumSponsors = [ + { + name: "Supafort", + image: "supafort.png", + link: "https://supafort.com/?ref=dokploy", + type: "premium", + }, ]; - const avatarsThirdRing = [ - "https://github.com/shadcn.png", // URLs de los avatares del tercer aro - "https://github.com/shadcn.png", - "https://github.com/shadcn.png", - "https://github.com/shadcn.png", - "https://github.com/shadcn.png", - "https://github.com/shadcn.png", - "https://github.com/shadcn.png", - "https://github.com/shadcn.png", + const eliteSponsors = []; + + const supportingSponsors = [ + { + name: "Lightspeed Run", + image: "https://github.com/lightspeedrun.png", + link: "https://lightspeed.run/?ref=dokploy", + type: "supporting", + }, + { + name: "Cloudblast", + image: "https://cloudblast.io/img/logo-icon.193cf13e.svg", + link: "https://cloudblast.io/?ref=dokploy", + type: "supporting", + }, + ]; + + const communitySponsors = [ + { + name: "Steamsets", + image: "https://avatars.githubusercontent.com/u/111978405?s=200&v=4", + link: "https://steamsets.com/?ref=dokploy", + type: "premium", + }, + { + name: "Rivo GG", + image: "https://avatars.githubusercontent.com/u/126797452?s=200&v=4", + link: "https://rivo.gg/?ref=dokploy", + type: "premium", + }, + { + name: "Photoquest", + image: "https://photoquest.wedding/favicon/android-chrome-512x512.png", + link: "https://photoquest.wedding/?ref=dokploy", + type: "premium", + }, ]; return ( @@ -75,14 +124,40 @@ const Ripple = React.memo(function Ripple({ left: "50%", transform: "translate(-50%, -50%) scale(1)", }} + /> + ); + })} + {Array.from({ length: numCircles }, (_, i) => { + const size = mainCircleSize + i * 70; + const opacity = mainCircleOpacity - i * 0.03; + const animationDelay = `${i * 0.06}s`; + const borderStyle = i === numCircles - 1 ? "dashed" : "solid"; + const borderOpacity = 5 + i * 5; + + return ( +
{i === 0 && (
- {avatarsFirstRing.map((src, index) => { - const angle = (360 / avatarsFirstRing.length) * index; + {heroSponsors.map((item, index) => { + const angle = (360 / heroSponsors.length) * index; const radius = mainCircleSize / 2; const x = radius * Math.cos((angle * Math.PI) / 180); const y = radius * Math.sin((angle * Math.PI) / 180); + const initials = item.name + .split(" ") + .map((n) => n[0]) + .join(""); return (
- - - CN - + + + + + + + {initials} + + + + +

+ {item.name} +

+
+
+
); })} @@ -106,12 +197,15 @@ const Ripple = React.memo(function Ripple({ {i === 1 && (
- {avatarsSecondRing.map((src, index) => { - const angle = (360 / avatarsSecondRing.length) * index; + {premiumSponsors.map((item, index) => { + const angle = (360 / premiumSponsors.length) * index; const radius = mainCircleSize / 2 + 70; // Radio mayor para el segundo aro const x = radius * Math.cos((angle * Math.PI) / 180); const y = radius * Math.sin((angle * Math.PI) / 180); - + const initials = item.name + .split(" ") + .map((n) => n[0]) + .join(""); return (
- - - CN - + + + + + + + {initials} + + + + +

+ {item.name} +

+
+
+
); })} @@ -134,12 +244,15 @@ const Ripple = React.memo(function Ripple({ {i === 3 && (
- {avatarsThirdRing.map((src, index) => { - const angle = (360 / avatarsThirdRing.length) * index; - const radius = mainCircleSize / 2 + 140; // Radio mayor para el tercer aro + {supportingSponsors.map((item, index) => { + const angle = (360 / supportingSponsors.length) * index; + const radius = mainCircleSize / 2 + 140; const x = radius * Math.cos((angle * Math.PI) / 180); const y = radius * Math.sin((angle * Math.PI) / 180); - + const initials = item.name + .split(" ") + .map((n) => n[0]) + .join(""); return (
- - - CN - + + + + + + + {initials} + + + + +

+ {item.name} +

+
+
+
+
+ ); + })} +
+ )} + + {i === 4 && ( +
+ {communitySponsors.map((item, index) => { + const angle = (360 / communitySponsors.length) * index; + const radius = mainCircleSize / 2 + 180; + const x = radius * Math.cos((angle * Math.PI) / 180); + const y = radius * Math.sin((angle * Math.PI) / 180); + const initials = item.name + .split(" ") + .map((n) => n[0]) + .join(""); + return ( +
+ + + + + + + {initials} + + + + +

+ {item.name} +

+
+
+
); })} diff --git a/apps/website/components/ui/tooltip.tsx b/apps/website/components/ui/tooltip.tsx new file mode 100644 index 0000000..3fe980f --- /dev/null +++ b/apps/website/components/ui/tooltip.tsx @@ -0,0 +1,30 @@ +"use client"; + +import * as TooltipPrimitive from "@radix-ui/react-tooltip"; +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const TooltipProvider = TooltipPrimitive.Provider; + +const Tooltip = TooltipPrimitive.Root; + +const TooltipTrigger = TooltipPrimitive.Trigger; + +const TooltipContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + +)); +TooltipContent.displayName = TooltipPrimitive.Content.displayName; + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; diff --git a/apps/website/package.json b/apps/website/package.json index d5d5f0f..9fe1c58 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -21,6 +21,7 @@ "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tabs": "1.1.1", + "@radix-ui/react-tooltip": "^1.1.3", "@tabler/icons-react": "3.21.0", "@types/node": "20.4.6", "autoprefixer": "^10.4.12", diff --git a/apps/website/public/supafort.png b/apps/website/public/supafort.png new file mode 100644 index 0000000..bc28a29 Binary files /dev/null and b/apps/website/public/supafort.png differ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 83d6124..81800e8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -109,6 +109,9 @@ importers: '@radix-ui/react-tabs': specifier: 1.1.1 version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-tooltip': + specifier: ^1.1.3 + version: 1.1.3(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@tabler/icons-react': specifier: 3.21.0 version: 3.21.0(react@18.2.0) @@ -782,6 +785,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-dismissable-layer@1.1.1': + resolution: {integrity: sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==} + peerDependencies: + '@types/react': 18.3.5 + '@types/react-dom': 18.3.0 + 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-focus-guards@1.0.1': resolution: {integrity: sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==} peerDependencies: @@ -896,6 +912,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-portal@1.1.2': + resolution: {integrity: sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==} + peerDependencies: + '@types/react': 18.3.5 + '@types/react-dom': 18.3.0 + 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.0.1': resolution: {integrity: sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==} peerDependencies: @@ -1070,6 +1099,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-tooltip@1.1.3': + resolution: {integrity: sha512-Z4w1FIS0BqVFI2c1jZvb/uDVJijJjJ2ZMuPV81oVgTZ7g3BZxobplnMVvXtFWgtozdvYJ+MFWtwkM5S2HnAong==} + peerDependencies: + '@types/react': 18.3.5 + '@types/react-dom': 18.3.0 + 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.0.1': resolution: {integrity: sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==} peerDependencies: @@ -3702,6 +3744,19 @@ snapshots: '@types/react': 18.3.5 '@types/react-dom': 18.3.0 + '@radix-ui/react-dismissable-layer@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.5)(react@18.2.0) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(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.3.5)(react@18.2.0) + '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@18.3.5)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.3.5 + '@types/react-dom': 18.3.0 + '@radix-ui/react-focus-guards@1.0.1(@types/react@18.3.5)(react@18.3.1)': dependencies: '@babel/runtime': 7.25.0 @@ -3866,6 +3921,16 @@ snapshots: '@types/react': 18.3.5 '@types/react-dom': 18.3.0 + '@radix-ui/react-portal@1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.5)(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.3.0)(@types/react@18.3.5)(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.3.5)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.3.5 + '@types/react-dom': 18.3.0 + '@radix-ui/react-presence@1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.25.0 @@ -4091,6 +4156,26 @@ snapshots: '@types/react': 18.3.5 '@types/react-dom': 18.3.0 + '@radix-ui/react-tooltip@1.1.3(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.5)(react@18.2.0) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.5)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.5)(react@18.2.0) + '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-portal': 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.5)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.5)(react@18.2.0) + '@radix-ui/react-visually-hidden': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(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.3.5 + '@types/react-dom': 18.3.0 + '@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.3.5)(react@18.3.1)': dependencies: '@babel/runtime': 7.25.0