From 704015b53f64a5b05ef7fa10b4e54be15554c3ba Mon Sep 17 00:00:00 2001 From: Eugene Fox Date: Sun, 18 Feb 2024 18:52:05 +0000 Subject: [PATCH] Static preview done --- .eslintrc.cjs | 10 +- package.json | 2 + src/App.styles.ts | 10 +- src/App.tsx | 76 ++++++++++++-- src/Components/CartesianGrid.tsx | 63 ++++++++++++ src/Components/ChartSkeleton.styles.ts | 45 +++++++++ src/Components/ChartSkeleton.tsx | 29 ++++++ src/Components/TrackChart.tsx | 133 +++++++++++++++++++++++++ src/Components/TrackLinePlot.tsx | 71 +++++++++++++ src/Components/TrackSurfacePlot.tsx | 47 +++++++++ src/Data/IChartPoint.ts | 21 ++++ src/Data/IPoint.ts | 12 +++ src/Data/ITrack.ts | 21 ++++ src/Data/LoadMockData.ts | 51 ++++++++++ src/Data/MaxSpeed.ts | 9 ++ src/Data/Surface.ts | 9 ++ src/Data/TrackChartDataProps.ts | 92 +++++++++++++++++ yarn.lock | 14 ++- 18 files changed, 701 insertions(+), 14 deletions(-) create mode 100644 src/Components/CartesianGrid.tsx create mode 100644 src/Components/ChartSkeleton.styles.ts create mode 100644 src/Components/ChartSkeleton.tsx create mode 100644 src/Components/TrackChart.tsx create mode 100644 src/Components/TrackLinePlot.tsx create mode 100644 src/Components/TrackSurfacePlot.tsx create mode 100644 src/Data/IChartPoint.ts create mode 100644 src/Data/IPoint.ts create mode 100644 src/Data/ITrack.ts create mode 100644 src/Data/LoadMockData.ts create mode 100644 src/Data/MaxSpeed.ts create mode 100644 src/Data/Surface.ts create mode 100644 src/Data/TrackChartDataProps.ts diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 0dc0071..6f3226f 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -22,9 +22,17 @@ module.exports = { "warn", { allowConstantExport: true }, ], - "@typescript-eslint/no-non-null-assertion": "warn", + "@typescript-eslint/no-non-null-assertion": "off", "@typescript-eslint/no-confusing-void-expression": "off", "@typescript-eslint/no-unused-vars": "warn", + "@typescript-eslint/prefer-for-of": "warn", + "no-empty": "warn", + "@typescript-eslint/no-inferrable-types": "off", + "@typescript-eslint/consistent-indexed-object-style": "warn", + "@typescript-eslint/no-unused-vars": "warn", + "@typescript-eslint/no-unnecessary-type-assertion": "warn", + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-unused-vars": "warn", }, parserOptions: { ecmaVersion: "latest", diff --git a/package.json b/package.json index 0a97133..1426f20 100644 --- a/package.json +++ b/package.json @@ -22,10 +22,12 @@ "@mui/system": "^5.15.9", "@mui/x-charts": "^6.19.4", "@mui/x-data-grid": "^6.19.4", + "d3-scale": "^4.0.2", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { + "@types/d3-scale": "^4.0.8", "@types/react": "^18.2.55", "@types/react-dom": "^18.2.19", "@typescript-eslint/eslint-plugin": "^6.21.0", diff --git a/src/App.styles.ts b/src/App.styles.ts index d67b769..e473b6d 100644 --- a/src/App.styles.ts +++ b/src/App.styles.ts @@ -9,12 +9,14 @@ const AppStyles = makeStyles(theme => ({ display: "flex", flexFlow: "column", height: "100%", - alignItems: "center", - justifyContent: "center" + userSelect: "none", }, - caption: + controls: { - marginTop: theme.spacing(2), + marginBottom: theme.spacing(4), + display: "flex", + alignItems: "center", + gap: theme.spacing(4), }, })); diff --git a/src/App.tsx b/src/App.tsx index f713faf..25c4114 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,12 @@ -import { Button, Container, GlobalStyles, Theme, ThemeProvider, Typography, createTheme } from "@mui/material"; -import { useState } from "react"; +import RefreshIcon from "@mui/icons-material/Refresh"; +import { Box, Button, Container, GlobalStyles, Slider, Theme, ThemeProvider, createTheme } from "@mui/material"; +import { useEffect, useState } from "react"; import useStyles from "./App.styles"; +import ChartSkeleton from "./Components/ChartSkeleton"; +import TrackChart from "./Components/TrackChart"; +import IPoint from "./Data/IPoint"; +import ITrack from "./Data/ITrack"; +import LoadMockData from "./Data/LoadMockData"; const theme: Theme = createTheme(); @@ -9,18 +15,72 @@ const theme: Theme = createTheme(); */ function App(): JSX.Element { + const [data, setData] = useState<{ tracks: ITrack[], points: IPoint[]; }>({ tracks: [], points: [] }); + const [isLoading, setLoading] = useState(true); + const [zoom, setZoom] = useState([0, 1]); const sx = useStyles(); - const [clicks, setClicks] = useState(0); + + const loadData = () => + { + setLoading(true); + console.log("Loading data..."); + const newData = LoadMockData(true); + setData(newData); + setZoom([0, Math.min(newData.tracks.length + 1, 20)]); + + new Promise(resolve => setTimeout(resolve, 1000)) + .then(() => setLoading(false)) + .catch(console.error); + }; + + useEffect(() => + { + loadData(); + }, []); + + const handleZoomChange = (_: unknown, newValue: number | number[]) => + { + const value: number[] = newValue as number[]; + + if ((value[0] === zoom[0] && value[1] === zoom[1]) || value[1] - value[0] < 2) + return; + + if (value[1] - value[0] > 50) + setLoading(true); + + setZoom(value); + }; return ( - - Hello World! - - Times clicked: { clicks } - + + setLoading(false) } /> + + + { !isLoading && + + } + + + + + + { isLoading && + + } + ); } diff --git a/src/Components/CartesianGrid.tsx b/src/Components/CartesianGrid.tsx new file mode 100644 index 0000000..c54fc7b --- /dev/null +++ b/src/Components/CartesianGrid.tsx @@ -0,0 +1,63 @@ +import { grey } from "@mui/material/colors"; +import { styled } from "@mui/system"; +import { useDrawingArea, useXScale, useYScale } from "@mui/x-charts"; +import { ScaleLinear } from "d3-scale"; +import React, { useEffect } from "react"; + +interface IProps +{ + /** The maximum Y value. */ + maxY: number; + /** The X ticks. */ + xTicks: number[]; +} + +/** The Cartesian grid. */ +function CartesianGrid(props: IProps): JSX.Element +{ + const [yTicks, setYTicks] = React.useState([]); + const xTicks = props.xTicks; + + // Get the drawing area bounding box + const { left, top, width, height } = useDrawingArea(); + + // Get the two scale + const yAxisScale = useYScale() as ScaleLinear; + const xAxisScale = useXScale() as ScaleLinear; + + + useEffect(() => + { + const ticks: number[] = []; + + for (let i = 1; i <= Math.ceil(props.maxY / 20) + 1; i++) + ticks.push(i * 20); + + setYTicks(ticks); + }, [props.maxY]); + + return ( + + + { yTicks.map((value) => + + ) } + + { xTicks.map((value) => + + ) } + + + ); +} + +const StyledPath = styled("path")( + () => ({ + fill: "none", + stroke: grey[500], + strokeWidth: 1, + pointerEvents: "none", + }), +); + +export default CartesianGrid; diff --git a/src/Components/ChartSkeleton.styles.ts b/src/Components/ChartSkeleton.styles.ts new file mode 100644 index 0000000..7ca75a0 --- /dev/null +++ b/src/Components/ChartSkeleton.styles.ts @@ -0,0 +1,45 @@ +import { makeStyles } from "../Utils/makeStyles"; + +export const useStyles = makeStyles(theme => ({ + root: + { + position: "fixed", + top: 0, + left: 0, + width: "100%", + height: "100%", + display: "grid", + gridTemplateRows: "1fr auto", + gap: theme.spacing(4), + backgroundColor: theme.palette.background.default, + }, + chart: + { + paddingLeft: theme.spacing(2), + paddingRight: theme.spacing(6), + paddingTop: theme.spacing(6), + display: "grid", + gridTemplateColumns: "auto 1fr", + gridTemplateRows: "1fr auto", + gap: theme.spacing(2), + }, + xAxis: + { + gridColumn: 2, + gridRow: 2, + }, + grid: + { + display: "grid", + gridTemplateColumns: "repeat(10, 1fr)", + gridTemplateRows: "repeat(10, 1fr)", + gap: theme.spacing(1), + }, + controls: + { + display: "flex", + alignItems: "center", + gap: theme.spacing(4), + marginBottom: theme.spacing(4), + }, +})); diff --git a/src/Components/ChartSkeleton.tsx b/src/Components/ChartSkeleton.tsx new file mode 100644 index 0000000..5f0a381 --- /dev/null +++ b/src/Components/ChartSkeleton.tsx @@ -0,0 +1,29 @@ +import { Box, Container, Skeleton } from "@mui/material"; +import { useStyles } from "./ChartSkeleton.styles"; + +function ChartSkeleton(): JSX.Element +{ + const sx = useStyles(); + + return ( + + + + + + + { Array.from({ length: 100 }).map((_, i) => + + ) } + + + + + + + + + ); +} + +export default ChartSkeleton; diff --git a/src/Components/TrackChart.tsx b/src/Components/TrackChart.tsx new file mode 100644 index 0000000..19c0536 --- /dev/null +++ b/src/Components/TrackChart.tsx @@ -0,0 +1,133 @@ +import { SxProps } from "@mui/system"; +import * as xc from "@mui/x-charts"; +import { useEffect, useState } from "react"; +import IChartPoint from "../Data/IChartPoint"; +import IPoint from "../Data/IPoint"; +import ITrack from "../Data/ITrack"; +import MaxSpeed from "../Data/MaxSpeed"; +import { TooltipProps, TracklineSeries } from "../Data/TrackChartDataProps"; +import CartesianGrid from "./CartesianGrid"; +import TrackLinePlot from "./TrackLinePlot"; +import TrackSurfacePlot from "./TrackSurfacePlot"; + +interface IProps +{ + /** The tracks data. */ + tracks: ITrack[]; + /** The points data. */ + points: IPoint[]; + /** The zoom levels (start, end). */ + zoom: number[]; + /** A callback for when the processing is complete. */ + onProcessingComplete?: () => void; +} + +/** + * A chart of the track. + */ +function TrackChart({ tracks, points, zoom, ...props }: IProps): JSX.Element +{ + const [dataset, setDataset] = useState([]); + const [xTicks, setXTicks] = useState([]); + const [zoomedDataset, setZoomedDataset] = useState([]); + const [zoomedXTicks, setZoomedXTicks] = useState([]); + const [maskStyles, setMasks] = useState({}); + const maxPointHeight: number = Math.max(...points.map(i => i.height)); + + useEffect(() => + { + for (let i = 0; i < dataset.length; i++) + { + const selector: string = `.MuiMarkElement-series-${MaxSpeed[dataset[i].maxSpeed]}`; + const element: SVGPathElement | undefined = document.querySelectorAll(selector)[i]; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + element?.style.setProperty("display", "inline"); + } + }); + + useEffect(() => + { + if (tracks.length < 1 || points.length < 1) + return; + + const data: IChartPoint[] = []; + let currentDistance: number = 0; + + for (const track of tracks) + { + const firstPoint = points.find(p => p.id === track.firstId)!; + + data.push({ + distance: currentDistance, + length: track.distance, + surface: track.surface, + maxSpeed: track.maxSpeed, + name: `${firstPoint.name} (ID#${firstPoint.id})`, + height: firstPoint.height, + }); + + currentDistance += track.distance; + } + + const lastPoint = points.find(p => p.id === tracks[tracks.length - 1].secondId)!; + + data.push({ + distance: currentDistance, + length: -1, + surface: tracks[tracks.length - 1].surface, + maxSpeed: tracks[tracks.length - 1].maxSpeed, + name: `${lastPoint.name} (ID#${lastPoint.id})`, + height: lastPoint.height + }); + + setDataset(data); + setXTicks(data.map(i => i.distance)); + + console.warn("Reflow!"); + + setTimeout(() => props.onProcessingComplete?.(), 500); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [tracks, points, props.onProcessingComplete]); + + useEffect(() => + { + setZoomedDataset(dataset.slice(zoom[0], zoom[1])); + setZoomedXTicks(xTicks.slice(zoom[0], zoom[1])); + }, [dataset, xTicks, zoom]); + + const getSx = (): SxProps => ({ + "& .MuiMarkElement-root": + { + display: "none", + }, + ...maskStyles, + }); + + return ( + + + + + setMasks(styles) } strokeWidth={ 4 } /> + + + + + + + + + + + ); +} + +export default TrackChart; diff --git a/src/Components/TrackLinePlot.tsx b/src/Components/TrackLinePlot.tsx new file mode 100644 index 0000000..5618aa6 --- /dev/null +++ b/src/Components/TrackLinePlot.tsx @@ -0,0 +1,71 @@ +import { SxProps } from "@mui/system"; +import { LinePlot, useDrawingArea, useXScale } from "@mui/x-charts"; +import { ScaleLinear } from "d3-scale"; +import { useEffect } from "react"; +import MaxSpeed from "../Data/MaxSpeed"; +import IChartPoint from "../Data/IChartPoint"; + +// Remarks: +// Even though this component adds nothing to the chart and only calculates masks, +// it is still required since a) I can't use `useDrawingArea` inside `TrackChart` +// and b) I can't pass calculates styles here + +interface IProps +{ + /** Data to plot. */ + dataset: IChartPoint[]; + /** Callback to update the styles. */ + onStylesUpdated: (styles: SxProps) => void; + /** The width of the line. */ + strokeWidth?: number; +} + +/** A plot of the track line. */ +function TrackLinePlot({ dataset, onStylesUpdated, strokeWidth }: IProps): JSX.Element +{ + const area = useDrawingArea(); + const xAxisScale = useXScale() as ScaleLinear; + + useEffect(() => + { + const masks: string[] = []; + + for (let i = 0; i < Object.keys(MaxSpeed).length / 2; i++) + masks.push("M 0 0 "); + + for (const point of dataset) + { + if (point.length < 1) + continue; + + const xStart: number = xAxisScale(point.distance) - area.left; + const xEnd: number = xAxisScale(point.distance + point.length) - area.left; + + masks[point.maxSpeed] += `H ${xStart} V ${area.height} H ${isNaN(xEnd) ? area.width : xEnd} V 0 `; + } + + let sx: SxProps = {}; + + for (let i = 0; i < masks.length; i++) + sx = { + ...sx, + [`& .MuiLineElement-series-${MaxSpeed[i]}`]: + { + clipPath: `path("${masks[i]} Z")`, + strokeWidth: strokeWidth ?? 2, + } + }; + + onStylesUpdated(sx); + + // Suppressing warning, since adding useXScale to the dependency array would cause an infinite component reflow + // `area` dependency is sufficient. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [area, dataset, strokeWidth]); + + return ( + + ); +} + +export default TrackLinePlot; diff --git a/src/Components/TrackSurfacePlot.tsx b/src/Components/TrackSurfacePlot.tsx new file mode 100644 index 0000000..c109122 --- /dev/null +++ b/src/Components/TrackSurfacePlot.tsx @@ -0,0 +1,47 @@ +import { styled } from "@mui/material"; +import { useDrawingArea, useXScale } from "@mui/x-charts"; +import { ScaleLinear } from "d3-scale"; +import React from "react"; +import IChartPoint from "../Data/IChartPoint"; +import { SurfaceColors } from "../Data/TrackChartDataProps"; + +interface IProps +{ + /** Data to plot. */ + dataset: IChartPoint[]; +} + +/** A plot of the track surface. */ +function TrackSurfacePlot({ dataset }: IProps): JSX.Element +{ + // Get the drawing area bounding box + const { top, height } = useDrawingArea(); + const xAxisScale = useXScale() as ScaleLinear; + + // Calculate the width of each track + const getWidth = (item: IChartPoint): number => + { + const width = xAxisScale(item.distance + item.length) - xAxisScale(item.distance); + return isNaN(width) ? 0 : width; + } + + return ( + + { dataset.map((i, index) => i.length > 0 && + + ) } + + ); +} + +const StyledRect = styled("rect")<{ color: string; }>( + ({ color }) => ({ + fill: color, + pointerEvents: "none" + }) +); + +export default TrackSurfacePlot; diff --git a/src/Data/IChartPoint.ts b/src/Data/IChartPoint.ts new file mode 100644 index 0000000..f7c86b1 --- /dev/null +++ b/src/Data/IChartPoint.ts @@ -0,0 +1,21 @@ +import MaxSpeed from "./MaxSpeed"; +import Surface from "./Surface"; + +/** Represents an aggregated point on the chart. */ +export default interface IChartPoint +{ + /** The distance from the start of the track. */ + distance: number; + /** The distance until the next point. */ + length: number; + /** The type of track surface */ + surface: Surface; + /** The maximum speed of the point. */ + maxSpeed: MaxSpeed; + /** The name of the point. */ + name: string; + /** The height of the point. */ + height: number; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any; +} diff --git a/src/Data/IPoint.ts b/src/Data/IPoint.ts new file mode 100644 index 0000000..f0a747f --- /dev/null +++ b/src/Data/IPoint.ts @@ -0,0 +1,12 @@ +/** Represents a point with an ID, name, and height. */ +export default interface IPoint +{ + /** The unique identifier. */ + id: number; + + /** The name of the point. */ + name: string; + + /** The height of the point. */ + height: number; +} diff --git a/src/Data/ITrack.ts b/src/Data/ITrack.ts new file mode 100644 index 0000000..e3482c4 --- /dev/null +++ b/src/Data/ITrack.ts @@ -0,0 +1,21 @@ +import MaxSpeed from "./MaxSpeed"; +import Surface from "./Surface"; + +/** Represents a track segment */ +export default interface ITrack +{ + /** First point ID */ + firstId: number; + + /** Second (last) point ID */ + secondId: number; + + /** Distance between points */ + distance: number; + + /** Surface type */ + surface: Surface; + + /** Maximum speed */ + maxSpeed: MaxSpeed; +} diff --git a/src/Data/LoadMockData.ts b/src/Data/LoadMockData.ts new file mode 100644 index 0000000..fb8190c --- /dev/null +++ b/src/Data/LoadMockData.ts @@ -0,0 +1,51 @@ +import IPoint from "./IPoint"; +import ITrack from "./ITrack"; + +/** + * Returns a random number between min and max + * @param min The minimum value (inclusive) + * @param max The maximum value (exclusive) + * @returns A random number between min and max + */ +const getRandom = (min: number, max: number): number => + Math.floor(Math.random() * (max - min)) + min; + +const pointNames: string[] = + [ + "Alpha", "Bravo", "Charlie", "Delta", "Echo", "Foxtrot", "Golf", "Hotel", "India", + "Juliett", "Kilo", "Lima", "Mike", "November", "Oscar", "Papa", "Quebec", "Romeo", + "Sierra", "Tango", "Uniform", "Victor", "Whiskey", "X-ray", "Yankee", "Zulu" + ]; + +function LoadMockData(largeCount?: boolean): { tracks: ITrack[], points: IPoint[]; } +{ + const count: number = getRandom(10, 20) * (largeCount ? 10 : 1); + const points: IPoint[] = []; + const tracks: ITrack[] = []; + + // Generate random data + + for (let i = 0; i < count; i++) + { + points.push({ + id: i, + name: `${pointNames[getRandom(0, pointNames.length)]}-${i}`, + height: getRandom(50, 200) + }); + } + + for (let i = 0; i < count - 1; i++) + { + tracks.push({ + firstId: points[i].id, + secondId: points[i + 1].id, + distance: getRandom(1000, 2000), + surface: getRandom(0, 3), + maxSpeed: getRandom(0, 3) + }); + } + + return { tracks, points }; +} + +export default LoadMockData; diff --git a/src/Data/MaxSpeed.ts b/src/Data/MaxSpeed.ts new file mode 100644 index 0000000..abec4a6 --- /dev/null +++ b/src/Data/MaxSpeed.ts @@ -0,0 +1,9 @@ +/** Represents the maximum speed options. */ +enum MaxSpeed +{ + FAST, + NORMAL, + SLOW +} + +export default MaxSpeed; diff --git a/src/Data/Surface.ts b/src/Data/Surface.ts new file mode 100644 index 0000000..5e368cc --- /dev/null +++ b/src/Data/Surface.ts @@ -0,0 +1,9 @@ +/** Represents the type of surface. */ +enum Surface +{ + SAND, + ASPHALT, + GROUND +} + +export default Surface; diff --git a/src/Data/TrackChartDataProps.ts b/src/Data/TrackChartDataProps.ts new file mode 100644 index 0000000..71184a1 --- /dev/null +++ b/src/Data/TrackChartDataProps.ts @@ -0,0 +1,92 @@ +import { green, grey, orange, red, yellow } from "@mui/material/colors"; +import { AllSeriesType, ChartsAxisContentProps } from "@mui/x-charts"; +import IChartPoint from "./IChartPoint"; +import MaxSpeed from "./MaxSpeed"; +import Surface from "./Surface"; + +/** Props for rendering trackline segments. */ +export const TracklineSeries: AllSeriesType[] = [ + // There're three of them, because there are. + // Instead of trying to split signle line into different segments, + // I instead apply a mask to each line, which is a bit more efficient. + { + type: "line", + dataKey: "height", + curve: "catmullRom", + id: MaxSpeed[0], + color: green[500], + }, + { + type: "line", + dataKey: "height", + curve: "catmullRom", + id: MaxSpeed[1], + color: orange[500], + }, + { + type: "line", + dataKey: "height", + curve: "catmullRom", + id: MaxSpeed[2], + color: red[500], + }, +]; + +/** Props for the chart tooltip. */ +export const TooltipProps = (dataset: IChartPoint[]): Partial => ({ + // @ts-expect-error - The type definition is incorrect + axis: + { + valueFormatter: (value: number): string => `${value} mi` + }, + series: + [ + { + type: "line", + data: dataset.map((_, index) => index), + label: "Point name", + id: "name", + valueFormatter: (value: number) => dataset[value].name, + color: green[500], + }, + { + type: "line", + data: dataset.map(i => i.height), + label: "Height ASL", + id: "height", + valueFormatter: (value: number) => `${value} ft`, + color: green[500], + }, + { + type: "line", + data: dataset.map(i => i.surface), + label: "Segment type", + id: "surface", + valueFormatter: (value: number) => Surface[value], + color: green[700], + }, + { + type: "line", + data: dataset.map(i => i.length), + id: "length", + label: "Next point", + valueFormatter: (value: number) => value < 1 ? "FINISH" : `${value} mi`, + color: yellow[200], + }, + { + type: "line", + data: dataset.map(i => i.maxSpeed), + id: "maxSpeed", + label: "Speed caution", + valueFormatter: (value: number) => MaxSpeed[value], + color: red[500], + }, + ] +}); + +/** Colors for each surface type. */ +export const SurfaceColors: Record = { + [Surface.ASPHALT]: grey[400], + [Surface.SAND]: yellow[100], + [Surface.GROUND]: green[100], +}; diff --git a/yarn.lock b/yarn.lock index 2db807b..3ff2b82 100644 --- a/yarn.lock +++ b/yarn.lock @@ -837,6 +837,18 @@ dependencies: "@babel/types" "^7.20.7" +"@types/d3-scale@^4.0.8": + version "4.0.8" + resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.8.tgz#d409b5f9dcf63074464bf8ddfb8ee5a1f95945bb" + integrity sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ== + dependencies: + "@types/d3-time" "*" + +"@types/d3-time@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.3.tgz#3c186bbd9d12b9d84253b6be6487ca56b54f88be" + integrity sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw== + "@types/estree@1.0.5": version "1.0.5" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" @@ -2528,7 +2540,7 @@ react-transition-group@^4.4.5: loose-envify "^1.4.0" prop-types "^15.6.2" -react@^18.0.0, react@^18.2.0: +react@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==