diff --git a/package.json b/package.json index 11627c2..be15300 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,13 @@ "@tanstack/react-router": "^1.136.18", "@tanstack/react-router-devtools": "^1.136.18", "clsx": "^2.1.1", + "konva": "^10.0.11", "react": "^19.2.0", "react-dom": "^19.2.0", - "react-use-websocket": "3.0.0" + "react-konva": "^19.2.0", + "react-tabs": "^6.1.0", + "react-use-websocket": "3.0.0", + "sonner": "^2.0.7" }, "devDependencies": { "@eslint/js": "^9.39.1", diff --git a/src/features/cameras/components/CameraGrid.tsx b/src/features/cameras/components/CameraGrid.tsx new file mode 100644 index 0000000..27a5b0c --- /dev/null +++ b/src/features/cameras/components/CameraGrid.tsx @@ -0,0 +1,40 @@ +import { useState } from "react"; + +import VideoFeedGridPainter from "./Video/VideoFeedGridPainter"; +import CameraSettings from "./CameraSettings/CameraSettings"; +import type { Region } from "../../../types/types"; +import PlatePatch from "./PlatePatch/PlatePatch"; + +const CameraGrid = () => { + const [regions, setRegions] = useState([ + { name: "Region 1", brushColour: "#ff0000" }, + { name: "Region 2", brushColour: "#00ff00" }, + { name: "Region 3", brushColour: "#0400ff" }, + ]); + const [selectedRegionIndex, setSelectedRegionIndex] = useState(0); + const [mode, setMode] = useState(""); + const [tabIndex, setTabIndex] = useState(0); + console.log(tabIndex); + const updateRegionColour = (index: number, newColour: string) => { + setRegions((prev) => prev.map((r, i) => (i === index ? { ...r, brushColour: newColour } : r))); + }; + console.log(mode); + return ( +
+ + + +
+ ); +}; + +export default CameraGrid; diff --git a/src/features/cameras/components/CameraSettings/CameraSettings.tsx b/src/features/cameras/components/CameraSettings/CameraSettings.tsx new file mode 100644 index 0000000..b0ad443 --- /dev/null +++ b/src/features/cameras/components/CameraSettings/CameraSettings.tsx @@ -0,0 +1,65 @@ +import Card from "../../../../ui/Card"; +import { Tab, Tabs, TabList, TabPanel } from "react-tabs"; +import "react-tabs/style/react-tabs.css"; +import RegionSelector from "./RegionSelector"; +import type { Region } from "../../../../types/types"; + +type CameraSettingsProps = { + regions: Region[]; + selectedRegionIndex: number; + onSelectRegion: (index: number) => void; + onChangeRegionColour: (index: number, colour: string) => void; + mode: string; + onSelectMode: (mode: string) => void; + setTabIndex: (tabIndex: number) => void; + tabIndex: number; +}; + +const CameraSettings = ({ + regions, + selectedRegionIndex, + onSelectRegion, + onChangeRegionColour, + mode, + onSelectMode, + tabIndex, + setTabIndex, +}: CameraSettingsProps) => { + return ( + + setTabIndex(index)} + > + + Target Detection + Camera 1 + Camera 2 + Camera 3 + + + + + +
Camera details {tabIndex}
+
+ +
Camera details {tabIndex}
+
+ +
Camera details {tabIndex}
+
+
+
+ ); +}; + +export default CameraSettings; diff --git a/src/features/cameras/components/CameraSettings/ColourPicker.tsx b/src/features/cameras/components/CameraSettings/ColourPicker.tsx new file mode 100644 index 0000000..f097cc4 --- /dev/null +++ b/src/features/cameras/components/CameraSettings/ColourPicker.tsx @@ -0,0 +1,10 @@ +type ColourPickerProps = { + colour: string; + setColour: (colour: string) => void; +}; + +const ColourPicker = ({ colour, setColour }: ColourPickerProps) => { + return setColour(e.target.value)} />; +}; + +export default ColourPicker; diff --git a/src/features/cameras/components/CameraSettings/RegionSelector.tsx b/src/features/cameras/components/CameraSettings/RegionSelector.tsx new file mode 100644 index 0000000..f03d132 --- /dev/null +++ b/src/features/cameras/components/CameraSettings/RegionSelector.tsx @@ -0,0 +1,69 @@ +import ColourPicker from "./ColourPicker"; +import type { Region } from "../../../../types/types"; + +type RegionSelectorProps = { + regions: Region[]; + selectedRegionIndex: number; + onSelectRegion: (index: number) => void; + onChangeRegionColour: (index: number, colour: string) => void; + mode: string; + onSelectMode: (mode: string) => void; +}; + +const RegionSelector = ({ + regions, + selectedRegionIndex, + onSelectRegion, + onChangeRegionColour, + mode, + onSelectMode, +}: RegionSelectorProps) => { + const handleChange = (e: { target: { value: string } }) => { + onSelectMode(e.target.value); + }; + + return ( +
+
+

Region Select

+
+
+ {regions.map((region, idx) => ( +
+ + onChangeRegionColour(idx, c)} /> +
+ ))} +
+

Tools

+
+
+ + + +
+
+
+ ); +}; + +export default RegionSelector; diff --git a/src/features/cameras/components/PlatePatch/PlatePatch.tsx b/src/features/cameras/components/PlatePatch/PlatePatch.tsx new file mode 100644 index 0000000..6a652f2 --- /dev/null +++ b/src/features/cameras/components/PlatePatch/PlatePatch.tsx @@ -0,0 +1,7 @@ +import Card from "../../../../ui/Card"; + +const PlatePatch = () => { + return PlatePatch; +}; + +export default PlatePatch; diff --git a/src/features/cameras/components/Video/VideoFeedGridPainter.tsx b/src/features/cameras/components/Video/VideoFeedGridPainter.tsx new file mode 100644 index 0000000..4ddebdb --- /dev/null +++ b/src/features/cameras/components/Video/VideoFeedGridPainter.tsx @@ -0,0 +1,153 @@ +import { useEffect, useRef, useState, type RefObject } from "react"; +import { Stage, Layer, Image, Shape } from "react-konva"; +import type { KonvaEventObject } from "konva/lib/Node"; +import { useCreateVideoSnapshot } from "../../hooks/useGetvideoSnapshots"; +import type { Region } from "../../../../types/types"; +import Card from "../../../../ui/Card"; + +const rows = 40; +const cols = 40; +const size = 20; +const gap = 0; + +type VideoFeedGridPainterProps = { + regions: Region[]; + selectedRegionIndex: number; + mode: string; +}; + +type PaintedCell = { + colour: string; +}; + +const VideoFeedGridPainter = ({ regions, selectedRegionIndex, mode }: VideoFeedGridPainterProps) => { + const { latestBitmapRef, isloading } = useCreateVideoSnapshot(); + const [stageSize, setStageSize] = useState({ width: 740, height: 460 }); + const isDrawingRef = useRef(false); + const paintedCellsRef = useRef>(new Map()); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const paintLayerRef = useRef(null); + + const draw = (bmp: RefObject): ImageBitmap | null => { + if (!bmp || !bmp.current) { + return null; + } + const image = bmp.current; + return image; + }; + + const image = draw(latestBitmapRef); + + const paintCell = (x: number, y: number) => { + const col = Math.floor(x / (size + gap)); + const row = Math.floor(y / (size + gap)); + + if (row < 0 || row >= rows || col < 0 || col >= cols) return; + + const activeRegion = regions[selectedRegionIndex]; + if (!activeRegion) return; + + const key = `${row}-${col}`; + const currentColour = regions[selectedRegionIndex].brushColour; + + const map = paintedCellsRef.current; + const existing = map.get(key); + + if (mode === "eraser") { + if (map.has(key)) { + map.delete(key); + paintLayerRef.current?.batchDraw(); + } + return; + } + + if (existing && existing.colour === currentColour) return; + + map.set(key, { colour: currentColour }); + + paintLayerRef.current?.batchDraw(); + }; + + const handleStageMouseDown = (e: KonvaEventObject) => { + if (!regions[selectedRegionIndex]) return; + isDrawingRef.current = true; + const pos = e.target.getStage()?.getPointerPosition(); + if (pos) paintCell(pos.x, pos.y); + }; + + const handleStageMouseMove = (e: KonvaEventObject) => { + if (!isDrawingRef.current) return; + if (!regions[selectedRegionIndex]) return; + const pos = e.target.getStage()?.getPointerPosition(); + if (pos) paintCell(pos.x, pos.y); + }; + + const handleStageMouseUp = () => { + isDrawingRef.current = false; + }; + + useEffect(() => { + const handleResize = () => { + const width = window.innerWidth; + + const aspectRatio = 740 / 460; + const newWidth = width * 0.36; + const newHeight = newWidth / aspectRatio; + setStageSize({ width: newWidth, height: newHeight }); + }; + handleResize(); + window.addEventListener("resize", handleResize); + + return () => window.removeEventListener("resize", handleResize); + }, []); + + if (image === null || isloading) + return ( + + Loading Video feed… + + ); + return ( +
+ + + + + + + { + const cells = paintedCellsRef.current; + cells.forEach((cell, key) => { + const [rowStr, colStr] = key.split("-"); + const row = Number(rowStr); + const col = Number(colStr); + + const x = col * (size + gap); + const y = row * (size + gap); + + ctx.beginPath(); + ctx.rect(x, y, size, size); + ctx.fillStyle = cell.colour; + ctx.fill(); + }); + + ctx.fillStrokeShape(shape); + }} + width={stageSize.width} + height={stageSize.height} + /> + + +
+ ); +}; + +export default VideoFeedGridPainter; diff --git a/src/features/cameras/hooks/useGetVideoFeed.ts b/src/features/cameras/hooks/useGetVideoFeed.ts new file mode 100644 index 0000000..bedbaeb --- /dev/null +++ b/src/features/cameras/hooks/useGetVideoFeed.ts @@ -0,0 +1,22 @@ +import { useQuery } from "@tanstack/react-query"; + +const getfeed = async () => { + const response = await fetch(`http://100.115.148.59/TargetDetectionColour-preview`, { + signal: AbortSignal.timeout(300000), + cache: "no-store", + }); + if (!response.ok) { + throw new Error(`Cannot reach endpoint (${response.status})`); + } + return response.blob(); +}; + +export const useGetVideoFeed = () => { + const videoQuery = useQuery({ + queryKey: ["getfeed"], + queryFn: getfeed, + refetchInterval: 500, + }); + + return { videoQuery }; +}; diff --git a/src/features/cameras/hooks/useGetvideoSnapshots.ts b/src/features/cameras/hooks/useGetvideoSnapshots.ts new file mode 100644 index 0000000..4c9679c --- /dev/null +++ b/src/features/cameras/hooks/useGetvideoSnapshots.ts @@ -0,0 +1,27 @@ +import { useEffect, useRef } from "react"; +import { useGetVideoFeed } from "./useGetVideoFeed"; + +export const useCreateVideoSnapshot = () => { + const latestBitmapRef = useRef(null); + const { videoQuery } = useGetVideoFeed(); + + const snapShot = videoQuery?.data; + const isloading = videoQuery.isPending; + + useEffect(() => { + async function createBitmap() { + if (!snapShot) return; + + try { + const bitmap = await createImageBitmap(snapShot); + if (!bitmap) return; + latestBitmapRef.current = bitmap; + } catch (error) { + console.log(error); + } + } + createBitmap(); + }, [snapShot]); + + return { latestBitmapRef, isloading }; +}; diff --git a/src/routes/baywatch.tsx b/src/routes/baywatch.tsx index 8ec01dd..f7401ab 100644 --- a/src/routes/baywatch.tsx +++ b/src/routes/baywatch.tsx @@ -1,9 +1,17 @@ -import { createFileRoute } from '@tanstack/react-router' +import { createFileRoute } from "@tanstack/react-router"; +import CameraGrid from "../features/cameras/components/CameraGrid"; +import { Toaster } from "sonner"; -export const Route = createFileRoute('/baywatch')({ +export const Route = createFileRoute("/baywatch")({ component: RouteComponent, -}) +}); function RouteComponent() { - return
Hello "/baywatch"!
+ return ( +
+

Cameras

+ + +
+ ); } diff --git a/src/types/types.ts b/src/types/types.ts index 3c59478..127fad1 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -12,3 +12,7 @@ export type InfoBarData = { }; export type StatusIndicator = "neutral-quaternary" | "dark" | "info" | "success" | "warning" | "danger"; +export type Region = { + name: string; + brushColour: string; +}; diff --git a/src/ui/Header.tsx b/src/ui/Header.tsx index c1934dd..31d2d2a 100644 --- a/src/ui/Header.tsx +++ b/src/ui/Header.tsx @@ -18,7 +18,7 @@ const Header = () => { - Baywatch + Cameras Output diff --git a/yarn.lock b/yarn.lock index 1101e46..b567169 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1011,6 +1011,16 @@ resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.2.3.tgz#c1e305d15a52a3e508d54dca770d202cb63abf2c" integrity sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ== +"@types/react-reconciler@^0.28.9": + version "0.28.9" + resolved "https://registry.yarnpkg.com/@types/react-reconciler/-/react-reconciler-0.28.9.tgz#d24b4864c384e770c83275b3fe73fba00269c83b" + integrity sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg== + +"@types/react-reconciler@^0.32.2": + version "0.32.3" + resolved "https://registry.yarnpkg.com/@types/react-reconciler/-/react-reconciler-0.32.3.tgz#eb4b346f367f29f07628032934d30a4f3f9eaba7" + integrity sha512-cMi5ZrLG7UtbL7LTK6hq9w/EZIRk4Mf1Z5qHoI+qBh7/WkYkFXQ7gOto2yfUvPzF5ERMAhaXS5eTQ2SAnHjLzA== + "@types/react@^19.2.5": version "19.2.6" resolved "https://registry.yarnpkg.com/@types/react/-/react-19.2.6.tgz#d27db1ff45012d53980f5589fda925278e1249ca" @@ -1283,7 +1293,7 @@ chokidar@^3.6.0: optionalDependencies: fsevents "~2.3.2" -clsx@^2.1.1: +clsx@^2.0.0, clsx@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== @@ -1726,12 +1736,19 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== +its-fine@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/its-fine/-/its-fine-2.0.0.tgz#a90b18a3ee4c211a1fb6faac2abcc2b682ce1f21" + integrity sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng== + dependencies: + "@types/react-reconciler" "^0.28.9" + jiti@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.6.1.tgz#178ef2fc9a1a594248c20627cd820187a4d78d92" integrity sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ== -js-tokens@^4.0.0: +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== @@ -1775,6 +1792,11 @@ keyv@^4.5.4: dependencies: json-buffer "3.0.1" +konva@^10.0.11: + version "10.0.11" + resolved "https://registry.yarnpkg.com/konva/-/konva-10.0.11.tgz#f63d3422625d13513094b646a2f09359d644542a" + integrity sha512-h0O6YNrwdgfb76kzkiMIqkyufUxE6GPcNwJZhYalnZ5qnYBEuxSRk62fQwtJqygGP5hdZKzJs2ea1ivVP+zetw== + levn@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" @@ -1869,6 +1891,13 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== +loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -1940,6 +1969,11 @@ normalize-range@^0.1.2: resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== +object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + optionator@^0.9.3: version "0.9.4" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" @@ -2027,6 +2061,15 @@ prettier@^3.5.0: resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.6.2.tgz#ccda02a1003ebbb2bfda6f83a074978f608b9393" integrity sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ== +prop-types@^15.5.0: + version "15.8.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + punycode@^2.1.0: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" @@ -2044,11 +2087,41 @@ react-dom@^19.2.0: dependencies: scheduler "^0.27.0" +react-is@^16.13.1: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +react-konva@^19.2.0: + version "19.2.0" + resolved "https://registry.yarnpkg.com/react-konva/-/react-konva-19.2.0.tgz#b4cc5d73cd6d642569e4df36a0139996c3dcf8e6" + integrity sha512-Ofifq/rdNvff50+Lj8x86WSfoeQDvdysOlsXMMrpD2uWmDxUPrEYSRLt27iCfdovQZL6xinKRpX9VaL9xDwXDQ== + dependencies: + "@types/react-reconciler" "^0.32.2" + its-fine "^2.0.0" + react-reconciler "0.33.0" + scheduler "0.27.0" + +react-reconciler@0.33.0: + version "0.33.0" + resolved "https://registry.yarnpkg.com/react-reconciler/-/react-reconciler-0.33.0.tgz#9dd20208d45baa5b0b4701781f858236657f15e1" + integrity sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA== + dependencies: + scheduler "^0.27.0" + react-refresh@^0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.18.0.tgz#2dce97f4fe932a4d8142fa1630e475c1729c8062" integrity sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw== +react-tabs@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/react-tabs/-/react-tabs-6.1.0.tgz#a1fc9d9b8db4c6e7bb327a1b6783bc51a1c457a1" + integrity sha512-6QtbTRDKM+jA/MZTTefvigNxo0zz+gnBTVFw2CFVvq+f2BuH0nF0vDLNClL045nuTAdOoK/IL1vTP0ZLX0DAyQ== + dependencies: + clsx "^2.0.0" + prop-types "^15.5.0" + react-use-websocket@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/react-use-websocket/-/react-use-websocket-3.0.0.tgz#754cb8eea76f55d31c5676d4abe3e573bc2cea04" @@ -2130,7 +2203,7 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -scheduler@^0.27.0: +scheduler@0.27.0, scheduler@^0.27.0: version "0.27.0" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.27.0.tgz#0c4ef82d67d1e5c1e359e8fc76d3a87f045fe5bd" integrity sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q== @@ -2167,6 +2240,11 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== +sonner@^2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/sonner/-/sonner-2.0.7.tgz#810c1487a67ec3370126e0f400dfb9edddc3e4f6" + integrity sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w== + source-map-js@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"