Merge pull request #10 from jhalitaksoy/new-ui

New UI
This commit is contained in:
Halit Aksoy 2022-06-04 22:43:17 +03:00 committed by GitHub
commit eb1a51c41f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 447 additions and 124 deletions

View File

@ -1,6 +1,6 @@
{
"name": "mancala-frontend",
"version": "0.1.3-beta.7",
"version": "0.1.3-beta.8",
"description": "",
"main": "index.js",
"scripts": {
@ -14,6 +14,8 @@
"author": "",
"license": "ISC",
"dependencies": {
"@szhsin/react-menu": "^3.0.2",
"@types/": "szhsin/react-menu",
"@types/uuid": "^8.3.4",
"mancala.js": "^0.0.2-beta.2",
"react": "^17.0.2",

View File

@ -18,6 +18,10 @@ import { GameMove } from "./models/GameMove";
import PitAnimator from "./animation/PitAnimator";
import BoardViewModel from "./viewmodel/BoardViewModel";
import { v4 } from "uuid";
import { Menu, MenuButton, MenuItem } from "@szhsin/react-menu";
import "@szhsin/react-menu/dist/index.css";
import "@szhsin/react-menu/dist/transitions/slide.css";
import { getColorByLuminance } from "./util/ColorUtil";
type ConnectionState = "connecting" | "error" | "connected" | "reconnecting";
@ -116,6 +120,12 @@ const Home: FunctionComponent<{ initial?: number }> = ({ initial = 0 }) => {
};
}, []);
React.useEffect(() => {
context.themeManager.onThemeChange = () => {
updateBoardViewModel(pitAnimator.getBoardViewModelFromGame(game));
};
}, [boardViewModel]);
const resetGameState = () => {
setGame(undefined);
setCrashMessage(undefined);
@ -166,8 +176,9 @@ const Home: FunctionComponent<{ initial?: number }> = ({ initial = 0 }) => {
const renderNewGameButton = () => {
const newGame = (
<Button
context={context}
text={context.texts.NewGame}
color="#005f73"
color={context.themeManager.theme.primary}
onClick={newGameClick}
/>
);
@ -184,14 +195,18 @@ const Home: FunctionComponent<{ initial?: number }> = ({ initial = 0 }) => {
}
return <></>;
};
const menuTextColor = getColorByLuminance(
context.themeManager.theme.appBarBgColor,
context.themeManager.theme.primary,
context.themeManager.theme.primaryLight
);
return (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
background: "#EEEEEE",
background: context.themeManager.theme.background,
flex: "1",
}}
>
@ -205,8 +220,8 @@ const Home: FunctionComponent<{ initial?: number }> = ({ initial = 0 }) => {
borderTopRightRadius: "1vw",
minWidth: "10vw",
minHeight: "1vw",
background: "#2F2504",
color: "white",
background: context.themeManager.theme.primary,
color: context.themeManager.theme.primaryLight,
}}
>
{connectionStateText()}
@ -214,8 +229,8 @@ const Home: FunctionComponent<{ initial?: number }> = ({ initial = 0 }) => {
)}
<div
style={{
padding: "0px 50px",
background: "rgb(228, 228, 228)",
padding: "0px 4vw",
background: context.themeManager.theme.appBarBgColor,
display: "flex",
flexDirection: "row",
alignItems: "center",
@ -223,15 +238,77 @@ const Home: FunctionComponent<{ initial?: number }> = ({ initial = 0 }) => {
alignSelf: "stretch",
}}
>
<h1 style={{ margin: "10px 0px" }}>{context.texts.Mancala}</h1>
<div>
<h1 style={{ color: menuTextColor, margin: "10px 0px" }}>
{context.texts.Mancala}
</h1>
<div
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
}}
>
<div
style={{
marginRight: "1vw",
display: "flex",
alignItems: "center",
}}
>
<Menu
menuStyle={{
background: context.themeManager.theme.appBarBgColor,
}}
menuButton={
<span
style={{ color: menuTextColor }}
class="material-symbols-outlined"
>
light_mode
</span>
}
transition
align="end"
>
{context.themeManager.themes.map((theme) => {
return (
<MenuItem
style={{
color: menuTextColor,
}}
onMouseOver={(event) =>
(event.target.style.background =
context.themeManager.theme.background)
}
onMouseOut={(event) =>
(event.target.style.background = "transparent")
}
onClick={() => (context.themeManager.theme = theme)}
>
<div
style={{
borderRadius: "5vw",
background: theme.boardColor,
width: "1vw",
height: "1vw",
marginRight: "1vw",
}}
></div>
{theme.name}
</MenuItem>
);
})}
</Menu>
</div>
{renderNewGameButton()}
{game &&
!userKeyWhoLeave &&
!crashMessage &&
(game?.state === "playing" || game?.state === "initial") && (
<Button
color="#005f73"
context={context}
color={context.themeManager.theme.primary}
text={context.texts.Leave}
onClick={leaveGame}
/>
@ -239,6 +316,7 @@ const Home: FunctionComponent<{ initial?: number }> = ({ initial = 0 }) => {
</div>
</div>
<InfoPanel
context={context}
game={game}
crashMessage={crashMessage}
userKey={userKey}

View File

@ -12,6 +12,7 @@ import { v4 } from "uuid";
import { Context } from "../context";
import BoardViewModelFactory from "../factory/BoardViewModelFactory";
import { PitViewModelFactory } from "../factory/PitViewModelFactory";
import { getColorByLuminance } from "../util/ColorUtil";
import BoardViewModel from "../viewmodel/BoardViewModel";
const animationUpdateInterval = 300;
@ -130,6 +131,11 @@ export default class PitAnimator {
pitViewModel.pitColor = theme.pitGetRivalStonePitAnimateColor;
pitViewModel.stoneCount = 0;
}
pitViewModel.stoneColor = getColorByLuminance(
pitViewModel.pitColor,
theme.stoneColor,
theme.stoneLightColor
);
}
startAnimationUpdateCyle() {
@ -156,7 +162,7 @@ export default class PitAnimator {
}
}
private getBoardViewModelFromGame(game: MancalaGame): BoardViewModel {
public getBoardViewModelFromGame(game: MancalaGame): BoardViewModel {
const pitViewModels = this.createPitViewModelsFromGame(game);
return BoardViewModelFactory.create(v4(), pitViewModels);
}
@ -165,7 +171,7 @@ export default class PitAnimator {
return game.board.pits.map((pit) => {
const theme = this.context.themeManager.theme;
const stoneCount = pit.stoneCount;
const stoneColor = theme.ballColor;
const stoneColor = theme.stoneColor;
const pitColor = theme.holeColor;
const id = pit.index.toString();
return PitViewModelFactory.create({

View File

@ -2,6 +2,7 @@ import { Bank, MancalaGame, Pit } from "mancala.js";
import * as React from "react";
import { FunctionComponent, useState } from "react";
import { Context } from "../context";
import { getColorByLuminance } from "../util/ColorUtil";
import BoardViewModel from "../viewmodel/BoardViewModel";
import PitViewModel from "../viewmodel/PitViewModel";
@ -67,6 +68,11 @@ const StoreView: FunctionComponent<{
const balls = [...range(pitViewModel.stoneCount)].map((i) => (
<BallView color={pitViewModel.stoneColor} />
));
const textColor = getColorByLuminance(
pitViewModel.pitColor,
context.themeManager.theme.primary,
context.themeManager.theme.primaryLight
);
return (
<div
style={{
@ -91,7 +97,7 @@ const StoreView: FunctionComponent<{
fontFamily: "monospace",
fontWeight: "bold",
fontSize: "2vw",
color: context.themeManager.theme.ballColor,
color: textColor,
}}
>
{balls.length}
@ -150,16 +156,14 @@ const BoardView: FunctionComponent<{
return (
<div
style={{
margin: "10px",
padding: "20px",
margin: "1vw",
padding: "2vw",
display: "grid",
gridTemplateColumns: "repeat(8, 11vw)",
gridTemplateRows: "repeat(2, 11vw)",
borderRadius: "3vw",
transition: "background-color 0.5s",
background: isUserTurn
? theme.boardColor
: theme.boardColorWhenPlayerTurn,
background: theme.boardColor,
}}
>
{userKey === game.player2Id ? (

View File

@ -1,22 +1,34 @@
import * as React from 'react';
import { FunctionComponent } from "react"
import * as React from "react";
import { FunctionComponent } from "react";
import { Context } from "../context";
import { getColorByLuminance } from "../util/ColorUtil";
const Button: FunctionComponent<{ text: String,onClick: () => void, color: string }> = ({ text, color, onClick }) => {
const Button: FunctionComponent<{
context: Context;
text: String;
onClick: () => void;
color: string;
}> = ({ context, text, color, onClick }) => {
const textColor = getColorByLuminance(
color,
context.themeManager.theme.primary,
context.themeManager.theme.primaryLight
);
return (
<button
onClick={onClick}
style={{
background: color,
color: textColor,
margin: "5px",
padding: "10px",
border: "none",
borderRadius: "4vw",
}}
>
{text}
</button>
);
};
return (
<button
onClick={onClick}
style={{
background: color,
color : "white",
margin: "5px",
padding: "10px",
border : "none",
borderRadius: "3vw",
}} >
{text}
</button>
)
}
export default Button
export default Button;

View File

@ -1,66 +1,109 @@
import { MancalaGame } from 'mancala.js';
import * as React from 'react';
import { FunctionComponent } from "react"
import { context } from '../context';
import { MancalaGame } from "mancala.js";
import * as React from "react";
import { FunctionComponent } from "react";
import { Context } from "../context";
import { getColorByLuminance } from "../util/ColorUtil";
function getInfoPanelTextByGameState(params: {
context: Context;
game: MancalaGame;
crashMessage: string;
userKey: string;
userKeyWhoLeave: string;
searchingOpponent: boolean;
}): string {
const {
context,
game,
crashMessage,
userKey,
userKeyWhoLeave,
searchingOpponent,
} = params;
if (searchingOpponent) {
return context.texts.SearchingOpponent + " " + context.texts.PleaseWait;
} else if (crashMessage) {
return context.texts.GameCrashed + " " + crashMessage;
} else if (userKeyWhoLeave) {
let message = context.texts.OpponentLeavesTheGame;
if (userKeyWhoLeave == userKey) {
message = context.texts.YouLeftTheGame;
}
return message;
} else if (game?.state == "ended") {
const wonPlayer = game.getWonPlayerId();
let whoWon =
game.getWonPlayerId() === userKey
? context.texts.YouWon
: context.texts.YouLost;
if (!wonPlayer) {
whoWon = context.texts.GameDraw;
}
return context.texts.GameEnded + " " + whoWon;
} else {
return game?.checkIsPlayerTurn(userKey)
? context.texts.YourTurn
: context.texts.OpponentTurn;
}
return undefined;
}
const InfoPanelContainer: FunctionComponent<{
context: Context;
color: string;
}> = (props) => {
return (
<div
style={{
background: props.color,
padding: "1vw 2vw",
marginTop: "1vw",
borderRadius: "10vw",
}}
>
{props.children}
</div>
);
};
const InfoPanel: FunctionComponent<{
game: MancalaGame,
crashMessage: string,
userKey: string,
userKeyWhoLeave: string,
searchingOpponent: boolean
context: Context;
game: MancalaGame;
crashMessage: string;
userKey: string;
userKeyWhoLeave: string;
searchingOpponent: boolean;
}> = ({
game, crashMessage, userKey, userKeyWhoLeave, searchingOpponent }) => {
if (searchingOpponent) {
return (
<h4>{
context.texts.SearchingOpponent + " " + context.texts.PleaseWait
}</h4>
)
}
context,
game,
crashMessage,
userKey,
userKeyWhoLeave,
searchingOpponent,
}) => {
const isUserTurn = game?.checkIsPlayerTurn(userKey);
const containerColor = isUserTurn
? context.themeManager.theme.playerTurnColor
: context.themeManager.theme.holeColor;
const textColor = getColorByLuminance(
containerColor,
context.themeManager.theme.primary,
context.themeManager.theme.primaryLight
);
return (
<InfoPanelContainer context={context} color={containerColor}>
<h4 style={{ margin: "0", color: textColor }}>
{getInfoPanelTextByGameState({
context,
game,
crashMessage,
userKey,
userKeyWhoLeave,
searchingOpponent,
})}
</h4>
</InfoPanelContainer>
);
};
if (crashMessage) {
return (
<h4>{
context.texts.GameCrashed + " " + crashMessage
}</h4>
)
}
if (userKeyWhoLeave) {
let message = context.texts.OpponentLeavesTheGame
if (userKeyWhoLeave == userKey) {
message = context.texts.YouLeftTheGame
}
return (
<h4>
{message}
</h4>
)
}
if (game) {
if (game.state == "ended") {
const wonPlayer = game.getWonPlayerId();
let whoWon = game.getWonPlayerId() === userKey ? context.texts.YouWon : context.texts.YouLost
if(!wonPlayer){
whoWon = context.texts.GameDraw
}
return (
<h4>{
context.texts.GameEnded + " " + whoWon
}</h4>
)
} else {
return (
<h4>{game.checkIsPlayerTurn(userKey) ? context.texts.YourTurn : context.texts.OpponentTurn}</h4>
)
}
}
return <h4></h4>
}
export default InfoPanel
export default InfoPanel;

View File

@ -2,7 +2,6 @@ import { Texts, TrTr } from "./const/texts";
import { RTMT } from "./rtmt/rtmt";
import { RTMTWS } from "./rtmt/rtmt_websocket";
import { UserKeyStore, UserKeyStoreImpl } from "./store/key_store";
import defaultTheme from "./theme/DefaultTheme";
import ThemeManager from "./theme/ThemeManager";
export type Context = {
@ -16,7 +15,7 @@ export const initContext = () => {
const rtmt = new RTMTWS();
const userKeyStore = new UserKeyStoreImpl();
const texts = TrTr;
const themeManager = new ThemeManager(defaultTheme);
const themeManager = new ThemeManager();
return {
rtmt: rtmt,
userKeyStore: userKeyStore,

View File

@ -11,6 +11,8 @@
<script src="./App.tsx"></script>
</body>
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@48,400,0,0" />
<style>
html{

32
src/theme/DarkTheme.ts Normal file
View File

@ -0,0 +1,32 @@
import { Theme } from "./Theme";
// from https://colorhunt.co/palette/0f0e0e5412128b9a46eeeeee
const colors = {
primary: "#541212",
secondary: "#0F0E0E",
tertiary: "#8B9A46",
quaternary: "#EEEEEE",
};
const colorSpecial = "#990000";
const darkTheme: Theme = {
id: "2",
name: "Dark Theme",
background: colors.primary,
appBarBgColor: colors.secondary,
primary: colors.primary,
primaryLight: colors.quaternary,
playerTurnColor: colors.secondary,
boardColor: colors.secondary,
holeColor: colors.tertiary,
pitSelectedColor: colors.tertiary,
stoneColor: colors.primary,
stoneLightColor: colors.tertiary,
pitGameMoveAnimateColor: colors.quaternary,
pitEmptyPitAnimateColor: colorSpecial,
pitLastStoneInBankPitAnimateColor: colorSpecial,
pitGetRivalStonePitAnimateColor: colorSpecial,
};
export default darkTheme;

View File

@ -1,19 +1,32 @@
import { Theme } from "./Theme";
// https://colorhunt.co/palette/7d5a50b4846ce5b299fcdec0
const colors = {
primary: "#7D5A50",
secondary: "#B4846C",
tertiary: "#E5B299",
quaternary: "#FCDEC0",
};
const colorSpecial = "#F6A9A9";
const defaultTheme: Theme = {
id: "1",
name: "Default Light Theme",
background: "#EEEEEE",
boardColor: "#4D606E",
boardColorWhenPlayerTurn: "#84b8a6",
storeColor: "#3FBAC2",
storeColorWhenPlayerTurn: "#6cab94",
holeColor: "#D3D4D8",
pitSelectedColor: "#8837fa",
ballColor: "#393E46",
ballLightColor: "#393E46",
pitGameMoveAnimateColor: "#c9b43c",
pitEmptyPitAnimateColor: "#5d7322",
pitLastStoneInBankPitAnimateColor: "#9463f7",
pitGetRivalStonePitAnimateColor: "#ff3d44",
appBarBgColor: colors.quaternary,
primary: colors.primary,
primaryLight: colors.quaternary,
playerTurnColor: colors.secondary,
boardColor: colors.secondary,
holeColor: colors.quaternary,
pitSelectedColor: colors.tertiary,
stoneColor: colors.primary,
stoneLightColor: colors.tertiary,
pitGameMoveAnimateColor: colors.tertiary,
pitEmptyPitAnimateColor: colorSpecial,
pitLastStoneInBankPitAnimateColor: colorSpecial,
pitGetRivalStonePitAnimateColor: colorSpecial,
};
export default defaultTheme;

22
src/theme/OldTheme.ts Normal file
View File

@ -0,0 +1,22 @@
import { Theme } from "./Theme";
const oldTheme: Theme = {
id: "0",
name: "Old Theme",
background: "#EEEEEE",
appBarBgColor: "#e4e4e4",
primary: "#4D606E",
primaryLight: "#EEEEEE",
playerTurnColor: "#84b8a6",
boardColor: "#4D606E",
holeColor: "#D3D4D8",
pitSelectedColor: "#8837fa",
stoneColor: "#393E46",
stoneLightColor: "#EEEEEE",
pitGameMoveAnimateColor: "#c9b43c",
pitEmptyPitAnimateColor: "#5d7322",
pitLastStoneInBankPitAnimateColor: "#9463f7",
pitGetRivalStonePitAnimateColor: "#ff3d44",
};
export default oldTheme;

View File

@ -1,13 +1,16 @@
export type Theme = {
id: string;
name: string;
primary: string;
primaryLight: string;
background: string;
appBarBgColor: string;
playerTurnColor: string;
boardColor: string;
boardColorWhenPlayerTurn: string;
storeColor: string;
storeColorWhenPlayerTurn: string;
holeColor: string;
pitSelectedColor: string;
ballColor: string;
ballLightColor: string;
stoneColor: string;
stoneLightColor: string;
pitGameMoveAnimateColor: string;
pitEmptyPitAnimateColor: string;
pitLastStoneInBankPitAnimateColor: string;

View File

@ -1,18 +1,40 @@
import darkTheme from "./DarkTheme";
import defaultTheme from "./DefaultTheme";
import oldTheme from "./OldTheme";
import { Theme } from "./Theme";
export const themes = [defaultTheme, darkTheme, oldTheme];
const THEME_ID = "theme_id";
export default class ThemeManager {
_theme: Theme;
onThemeChange: (theme: Theme) => void;
constructor(theme: Theme) {
this._theme = theme;
constructor() {
this._theme = this.readFromLocalStorage() || defaultTheme;
}
public get theme() {
return this._theme;
}
public set theme(value) {
public set theme(value: Theme) {
this._theme = value;
this.onThemeChange?.(value);
this.writetToLocalStorage(value);
}
private writetToLocalStorage(value: Theme) {
localStorage.setItem(THEME_ID, value.id);
}
private readFromLocalStorage(): Theme {
const themeID = localStorage.getItem(THEME_ID);
const theme = themes.find((eachTheme: Theme) => themeID === eachTheme.id);
return theme;
}
public get themes(): Theme[] {
return themes;
}
}

51
src/util/ColorUtil.ts Normal file
View File

@ -0,0 +1,51 @@
//from this gist https://gist.github.com/jfsiii/5641126
// from http://www.w3.org/TR/WCAG20/#relativeluminancedef
export function relativeLuminanceW3C(
R8bit: number,
G8bit: number,
B8bit: number
) {
const RsRGB = R8bit / 255;
const GsRGB = G8bit / 255;
const BsRGB = B8bit / 255;
const R =
RsRGB <= 0.03928 ? RsRGB / 12.92 : Math.pow((RsRGB + 0.055) / 1.055, 2.4);
const G =
GsRGB <= 0.03928 ? GsRGB / 12.92 : Math.pow((GsRGB + 0.055) / 1.055, 2.4);
const B =
BsRGB <= 0.03928 ? BsRGB / 12.92 : Math.pow((BsRGB + 0.055) / 1.055, 2.4);
// For the sRGB colorspace, the relative luminance of a color is defined as:
const L = 0.2126 * R + 0.7152 * G + 0.0722 * B;
return L;
}
export function relativeLuminanceW3CHexColor(hexColor: string): number {
const [r, g, b] = hexToRgb(hexColor);
return relativeLuminanceW3C(r, g, b);
}
// from https://www.codegrepper.com/code-examples/javascript/javascript+convert+color+string+to+rgb
export function rgbToHex(r: number, g: number, b: number): string {
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
}
// from https://www.codegrepper.com/code-examples/javascript/javascript+convert+color+string+to+rgb
export function hexToRgb(
hex: string,
result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
): number[] {
return result ? result.map((i) => parseInt(i, 16)).slice(1) : null;
//returns [23, 14, 45] -> reformat if needed
}
export function getColorByLuminance(
color: string,
lightColor: string,
darkColor: string
): string {
return relativeLuminanceW3CHexColor(color) < 0.5 ? darkColor : lightColor;
}

View File

@ -1011,6 +1011,21 @@
"@parcel/utils" "^1.11.0"
physical-cpu-count "^2.0.0"
"@szhsin/react-menu@^3.0.2":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@szhsin/react-menu/-/react-menu-3.0.2.tgz#d22971c53d56e6d404c9d3c98f533907cd8f03dc"
integrity sha512-m9Ly+cT+CxQx3xhq90CVaOLQWU7f7UKeMxfDt1gPYV23tDwEe8Zo6PO547qPlAEGEwwb9MdA38U8OyueXKJc2g==
dependencies:
prop-types "^15.7.2"
react-transition-state "^1.1.4"
"@types/@szhsin/react-menu":
version "3.0.2"
resolved "https://codeload.github.com/szhsin/react-menu/tar.gz/28284b2183801fb4f6a95e9270ce580441c5da70"
dependencies:
prop-types "^15.7.2"
react-transition-state "^1.1.4"
"@types/prop-types@*":
version "15.7.5"
resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz"
@ -3364,7 +3379,7 @@ log-symbols@^2.2.0:
dependencies:
chalk "^2.0.1"
loose-envify@^1.1.0:
loose-envify@^1.1.0, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
@ -4333,6 +4348,15 @@ process@^0.11.10:
resolved "https://registry.npmjs.org/process/-/process-0.11.10.tgz"
integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI=
prop-types@^15.7.2:
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"
psl@^1.1.28:
version "1.8.0"
resolved "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz"
@ -4433,6 +4457,16 @@ react-dom@^17.0.2:
object-assign "^4.1.1"
scheduler "^0.20.2"
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-transition-state@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/react-transition-state/-/react-transition-state-1.1.4.tgz#113224eaa27e0ff81661305e44d5e0348cdf61ac"
integrity sha512-6nQLWWx95gYazCm6OdtD1zGbRiirvVXPrDtHAGsYb4xs9spMM7bA8Vx77KCpjL8PJ8qz1lXFGz2PTboCSvt7iw==
react@^17.0.2:
version "17.0.2"
resolved "https://registry.npmjs.org/react/-/react-17.0.2.tgz"