feat(map): add pitch and bearing constants for map initialization

feat(district-popup): enhance badge hover styles for crime levels

feat(map): implement year timeline control with smooth animation

feat(ui): create a reusable slider component using Radix UI

chore(package): update package.json and package-lock.json to include @radix-ui/react-slider
This commit is contained in:
vergiLgood1 2025-05-03 22:58:14 +07:00
parent fdc0403b81
commit e488bad7c1
9 changed files with 1123 additions and 248 deletions

View File

@ -0,0 +1,215 @@
import { useState, useEffect, useRef } from "react"
import { Pause, Play } from "lucide-react"
import { Button } from "@/app/_components/ui/button"
import { cn } from "@/app/_lib/utils"
import { Slider } from "@/app/_components/ui/slider"
import { getMonthName } from "@/app/_utils/common"
interface SmoothYearTimelineProps {
startYear: number
endYear: number
onChange?: (year: number, month: number, progress: number) => void
className?: string
autoPlay?: boolean
autoPlaySpeed?: number // Time to progress through one month in ms
}
export function SmoothYearTimeline({
startYear = 2020,
endYear = 2024,
onChange,
className,
autoPlay = true,
autoPlaySpeed = 1000, // Speed of month progress
}: SmoothYearTimelineProps) {
const [currentYear, setCurrentYear] = useState<number>(startYear)
const [currentMonth, setCurrentMonth] = useState<number>(1) // Start at January (1)
const [progress, setProgress] = useState<number>(0) // Progress within the current month
const [isPlaying, setIsPlaying] = useState<boolean>(autoPlay)
const [isDragging, setIsDragging] = useState<boolean>(false)
const animationRef = useRef<number | null>(null)
const lastUpdateTimeRef = useRef<number>(0)
// Calculate total months from start to end year
const totalMonths = ((endYear - startYear) * 12) + 12 // +12 to include all months of end year
const calculateOverallProgress = (): number => {
const yearDiff = currentYear - startYear
const monthProgress = (yearDiff * 12) + (currentMonth - 1)
return ((monthProgress + progress) / (totalMonths - 1)) * 100
}
const calculateTimeFromProgress = (overallProgress: number): { year: number; month: number; progress: number } => {
const totalProgress = (overallProgress * (totalMonths - 1)) / 100
const monthsFromStart = Math.floor(totalProgress)
const year = startYear + Math.floor(monthsFromStart / 12)
const month = (monthsFromStart % 12) + 1 // 1-12 for months
const monthProgress = totalProgress - Math.floor(totalProgress)
return {
year: Math.min(year, endYear),
month: Math.min(month, 12),
progress: monthProgress
}
}
// Calculate the current position for the active marker
const calculateMarkerPosition = (): string => {
const overallProgress = calculateOverallProgress()
return `${overallProgress}%`
}
const animate = (timestamp: number) => {
if (!lastUpdateTimeRef.current) {
lastUpdateTimeRef.current = timestamp
}
if (!isDragging) {
const elapsed = timestamp - lastUpdateTimeRef.current
const progressIncrement = elapsed / autoPlaySpeed
let newProgress = progress + progressIncrement
let newMonth = currentMonth
let newYear = currentYear
if (newProgress >= 1) {
newProgress = 0
newMonth = currentMonth + 1
if (newMonth > 12) {
newMonth = 1
newYear = currentYear + 1
if (newYear > endYear) {
newYear = startYear
newMonth = 1
}
}
setCurrentMonth(newMonth)
setCurrentYear(newYear)
}
setProgress(newProgress)
if (onChange) {
onChange(newYear, newMonth, newProgress)
}
lastUpdateTimeRef.current = timestamp
}
if (isPlaying) {
animationRef.current = requestAnimationFrame(animate)
}
}
useEffect(() => {
if (isPlaying) {
lastUpdateTimeRef.current = 0
animationRef.current = requestAnimationFrame(animate)
} else if (animationRef.current) {
cancelAnimationFrame(animationRef.current)
}
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current)
}
}
}, [isPlaying, currentYear, currentMonth, progress, isDragging])
const handlePlayPause = () => {
setIsPlaying(!isPlaying)
}
const handleSliderChange = (value: number[]) => {
const overallProgress = value[0]
const { year, month, progress } = calculateTimeFromProgress(overallProgress)
setCurrentYear(year)
setCurrentMonth(month)
setProgress(progress)
if (onChange) {
onChange(year, month, progress)
}
}
const handleSliderDragStart = () => {
setIsDragging(true)
}
const handleSliderDragEnd = () => {
setIsDragging(false)
}
// Create year markers
const yearMarkers = []
for (let year = startYear; year <= endYear; year++) {
yearMarkers.push(year)
}
return (
<div className={cn("w-full bg-transparent text-emerald-500", className)}>
<div className="relative">
{/* Current month/year marker that moves with the slider */}
<div
className="absolute bottom-full mb-2 transform -translate-x-1/2 bg-emerald-500 text-background px-3 py-1 rounded-full text-xs font-bold z-20"
style={{ left: calculateMarkerPosition() }}
>
{getMonthName(currentMonth)} {currentYear}
</div>
{/* Wrap button and slider in their container */}
<div className="px-2 flex gap-x-2">
{/* Play/Pause button */}
<Button
variant="ghost"
size="icon"
onClick={handlePlayPause}
className="text-background bg-emerald-500 rounded-full hover:text-background hover:bg-emerald-500/50 h-10 w-10 z-10"
>
{isPlaying ? <Pause className="h-5 w-5" /> : <Play className="h-5 w-5" />}
</Button>
{/* Slider */}
<Slider
value={[calculateOverallProgress()]}
min={0}
max={100}
step={0.01}
onValueChange={handleSliderChange}
onValueCommit={handleSliderDragEnd}
onPointerDown={handleSliderDragStart}
className="w-full [&>span:first-child]:h-1.5 [&>span:first-child]:bg-white/30 [&_[role=slider]]:bg-emerald-500 [&_[role=slider]]:w-3 [&_[role=slider]]:h-3 [&_[role=slider]]:border-0 [&>span:first-child_span]:bg-emerald-500 [&_[role=slider]:focus-visible]:ring-0 [&_[role=slider]:focus-visible]:ring-offset-0 [&_[role=slider]:focus-visible]:scale-105 [&_[role=slider]:focus-visible]:transition-transform"
/>
</div>
{/* Year markers */}
<div className="flex items-center relative h-10">
<div className="absolute inset-0 h-full flex">
{yearMarkers.map((year, index) => (
<div
key={year}
className={cn(
"flex-1 h-full flex items-center justify-center relative",
index < yearMarkers.length - 1 && ""
)}
>
<div
className={cn(
"text-sm transition-colors font-medium",
year === currentYear ? "text-emerald-500 font-bold text-lg" : "text-white/50"
)}
>
{year}
</div>
</div>
))}
</div>
</div>
</div>
</div>
)
}

View File

@ -20,6 +20,7 @@ import SidebarToggle from "./sidebar/sidebar-toggle"
import { cn } from "@/app/_lib/utils"
import CrimePopup from "./pop-up/crime-popup"
import { $Enums, crime_categories, crime_incidents, crimes, demographics, districts, geographics, locations } from "@prisma/client"
import { SmoothYearTimeline } from "./controls/year-timeline"
// Updated CrimeIncident type to match the structure in crime_incidents
interface CrimeIncident {
@ -44,6 +45,7 @@ export default function CrimeMap() {
const [selectedYear, setSelectedYear] = useState<number>(2024)
const [selectedMonth, setSelectedMonth] = useState<number | "all">("all")
const [activeControl, setActiveControl] = useState<ITopTooltipsMapId>("incidents")
const [yearProgress, setYearProgress] = useState(0)
const mapContainerRef = useRef<HTMLDivElement>(null)
@ -174,6 +176,13 @@ export default function CrimeMap() {
setSelectedDistrict(feature);
}
// Handle year-month timeline change
const handleTimelineChange = useCallback((year: number, month: number, progress: number) => {
setSelectedYear(year)
setSelectedMonth(month)
setYearProgress(progress)
}, [])
// Reset filters
const resetFilters = useCallback(() => {
setSelectedYear(2024)
@ -295,8 +304,21 @@ export default function CrimeMap() {
/>
<MapLegend position="bottom-right" />
</>
)}
{isFullscreen && (
<div className="absolute flex w-full bottom-0">
<SmoothYearTimeline
startYear={2020}
endYear={2024}
autoPlay={false}
autoPlaySpeed={1000}
onChange={handleTimelineChange}
/>
</div>
)}
</MapView>
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@ import type React from "react"
import { useState, useCallback, useRef } from "react"
import { type ViewState, Map, type MapRef, NavigationControl, GeolocateControl } from "react-map-gl/mapbox"
import { FullscreenControl } from "react-map-gl/mapbox"
import { BASE_LATITUDE, BASE_LONGITUDE, BASE_ZOOM, MAPBOX_STYLES, type MapboxStyle } from "@/app/_utils/const/map"
import { BASE_BEARING, BASE_LATITUDE, BASE_LONGITUDE, BASE_PITCH, BASE_ZOOM, MAPBOX_STYLES, type MapboxStyle } from "@/app/_utils/const/map"
import "mapbox-gl/dist/mapbox-gl.css"
import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder';
import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css';
@ -39,8 +39,8 @@ export default function MapView({
longitude: BASE_LONGITUDE,
latitude: BASE_LATITUDE,
zoom: BASE_ZOOM,
bearing: 0,
pitch: 0,
bearing: BASE_BEARING,
pitch: BASE_PITCH,
...initialViewState,
}

View File

@ -76,13 +76,13 @@ export default function DistrictPopup({
const getCrimeRateBadge = (level?: string) => {
switch (level) {
case "low":
return <Badge className="bg-emerald-600 text-white">Low</Badge>
return <Badge className="bg-emerald-600 text-white hover:bg-emerald-600">Low</Badge>
case "medium":
return <Badge className="bg-amber-500 text-white">Medium</Badge>
return <Badge className="bg-amber-500 text-white hover:bg-amber-500">Medium</Badge>
case "high":
return <Badge className="bg-rose-600 text-white">High</Badge>
return <Badge className="bg-rose-600 text-white hover:bg-rose-600">High</Badge>
case "critical":
return <Badge className="bg-red-700 text-white">Critical</Badge>
return <Badge className="bg-red-700 text-white hover:bg-red-700">Critical</Badge>
default:
return <Badge className="bg-slate-600">Unknown</Badge>
}

View File

@ -0,0 +1,27 @@
"use client"
import * as React from "react"
import * as SliderPrimitive from "@radix-ui/react-slider"
import { cn } from "@/app/_lib/utils"
const Slider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
className={cn(
"relative flex w-full touch-none select-none items-center",
className
)}
{...props}
>
<SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-primary/20">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-4 w-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
))
Slider.displayName = SliderPrimitive.Root.displayName
export { Slider }

View File

@ -1,6 +1,6 @@
export const MAP_STYLE = 'mapbox://styles/mapbox/dark-v11';
export const BASE_ZOOM = 9.5; // Default zoom level for the map
export const BASE_PITCH = 0; // Default pitch for the map
export const BASE_BEARING = 0; // Default bearing for the map
export const BASE_LATITUDE = -8.17; // Default latitude for the map center (Jember region)
export const BASE_LONGITUDE = 113.65; // Default longitude for the map center (Jember region)
export const MAPBOX_ACCESS_TOKEN = process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN;

View File

@ -23,6 +23,7 @@
"@radix-ui/react-scroll-area": "^1.2.3",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slider": "^1.3.2",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-switch": "^1.1.3",
"@radix-ui/react-tabs": "^1.1.3",
@ -3442,6 +3443,230 @@
}
}
},
"node_modules/@radix-ui/react-slider": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.2.tgz",
"integrity": "sha512-oQnqfgSiYkxZ1MrF6672jw2/zZvpB+PJsrIc3Zm1zof1JHf/kj7WhmROw7JahLfOwYQ5/+Ip0rFORgF1tjSiaQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/number": "1.1.1",
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-collection": "1.1.4",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-primitive": "2.1.0",
"@radix-ui/react-use-controllable-state": "1.2.2",
"@radix-ui/react-use-layout-effect": "1.1.1",
"@radix-ui/react-use-previous": "1.1.1",
"@radix-ui/react-use-size": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/number": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
"integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
"license": "MIT"
},
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/primitive": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
"license": "MIT"
},
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-collection": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.4.tgz",
"integrity": "sha512-cv4vSf7HttqXilDnAnvINd53OTl1/bjUYVZrkFnA7nwmY9Ob2POUy0WY0sfqBAe1s5FyKsyceQlqiEGPYNTadg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-primitive": "2.1.0",
"@radix-ui/react-slot": "1.2.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-context": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-direction": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
"integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-primitive": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.0.tgz",
"integrity": "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.2.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-slot": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz",
"integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-effect-event": "0.0.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-use-previous": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz",
"integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-use-size": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
"integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slot": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
@ -3586,6 +3811,39 @@
}
}
},
"node_modules/@radix-ui/react-use-effect-event": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
"integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-effect-event/node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-escape-keydown": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz",

View File

@ -29,6 +29,7 @@
"@radix-ui/react-scroll-area": "^1.2.3",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slider": "^1.3.2",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-switch": "^1.1.3",
"@radix-ui/react-tabs": "^1.1.3",