diff --git a/package.json b/package.json index 09fe2cff..5c193ca4 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "@electron-toolkit/tsconfig": "^1.0.1", "@swc/core": "^1.4.16", "@types/auto-launch": "^5.0.5", + "@types/color": "^3.0.6", "@types/jsdom": "^21.1.6", "@types/lodash-es": "^4.17.12", "@types/node": "^20.12.7", diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 72b2b4cf..ffc468fe 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -19,7 +19,8 @@ "filter": "Filter library", "home": "Home", "queued": "{{title}} (Queued)", - "game_has_no_executable": "Game has no executable selected" + "game_has_no_executable": "Game has no executable selected", + "sign_in": "Sign in" }, "header": { "search": "Search games", @@ -232,7 +233,6 @@ "amount_hours": "{{amount}} hours", "amount_minutes": "{{amount}} minutes", "last_time_played": "Last played {{period}}", - "sign_out": "Sign out", "activity": "Recent activity", "library": "Library", "total_play_time": "Total playtime: {{amount}}", @@ -244,9 +244,11 @@ "edit_profile": "Edit Profile", "saved_successfully": "Saved successfully", "try_again": "Please, try again", - "signout_modal_title": "Are you sure?", + "sign_out_modal_title": "Are you sure?", "cancel": "Cancel", - "signout": "Sign Out", - "successfully_signed_out": "Successfully signed out" + "successfully_signed_out": "Successfully signed out", + "sign_out": "Sign out", + "playing_for": "Playing for {{amount}}", + "sign_out_modal_text": "Your library is linked with your current account. When signing out, your library will not be visible anymore, and any progress will not be saved. Continue with sign out?" } } diff --git a/src/locales/pt/translation.json b/src/locales/pt/translation.json index e658e656..f51f89f9 100644 --- a/src/locales/pt/translation.json +++ b/src/locales/pt/translation.json @@ -19,7 +19,8 @@ "filter": "Filtrar biblioteca", "home": "Início", "queued": "{{title}} (Na fila)", - "game_has_no_executable": "Jogo não possui executável selecionado" + "game_has_no_executable": "Jogo não possui executável selecionado", + "sign_in": "Login" }, "header": { "search": "Buscar jogos", @@ -232,7 +233,6 @@ "amount_hours": "{{amount}} horas", "amount_minutes": "{{amount}} minutos", "last_time_played": "Jogou {{period}}", - "sign_out": "Sair da conta", "activity": "Atividade recente", "library": "Biblioteca", "total_play_time": "Tempo total de jogo: {{amount}}", @@ -245,8 +245,10 @@ "saved_successfully": "Salvo com sucesso", "try_again": "Por favor, tente novamente", "cancel": "Cancelar", - "signout": "Sair da conta", - "signout_modal_title": "Tem certeza?", - "successfully_signed_out": "Deslogado com sucesso" + "successfully_signed_out": "Deslogado com sucesso", + "sign_out": "Sair da conta", + "sign_out_modal_title": "Tem certeza?", + "playing_for": "Jogando por {{amount}}", + "sign_out_modal_text": "Sua biblioteca de jogos está associada com a sua conta atual. Ao sair, sua biblioteca não aparecerá mais no Hydra e qualquer progresso não será salvo. Deseja continuar?" } } diff --git a/src/main/events/auth/signout.ts b/src/main/events/auth/sign-out.ts similarity index 66% rename from src/main/events/auth/signout.ts rename to src/main/events/auth/sign-out.ts index 4525fdb2..dafce7ad 100644 --- a/src/main/events/auth/signout.ts +++ b/src/main/events/auth/sign-out.ts @@ -1,11 +1,12 @@ -import { userAuthRepository } from "@main/repository"; +import { gameRepository, userAuthRepository } from "@main/repository"; import { registerEvent } from "../register-event"; import { HydraApi } from "@main/services/hydra-api"; const signOut = async (_event: Electron.IpcMainInvokeEvent): Promise => { await Promise.all([ userAuthRepository.delete({ id: 1 }), - HydraApi.post("/auth/logout"), + gameRepository.delete({}), + HydraApi.post("/auth/logout").catch(), ]); }; diff --git a/src/main/events/index.ts b/src/main/events/index.ts index 32c9243c..01e73636 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -40,7 +40,7 @@ import "./download-sources/validate-download-source"; import "./download-sources/add-download-source"; import "./download-sources/remove-download-source"; import "./download-sources/sync-download-sources"; -import "./auth/signout"; +import "./auth/sign-out"; import "./auth/open-auth-window"; import "./user/get-user"; import "./profile/get-me"; diff --git a/src/main/services/process-watcher.ts b/src/main/services/process-watcher.ts index de3af727..fc97ff40 100644 --- a/src/main/services/process-watcher.ts +++ b/src/main/services/process-watcher.ts @@ -5,6 +5,7 @@ import { gameRepository } from "@main/repository"; import { getProcesses } from "@main/helpers"; import { WindowManager } from "./window-manager"; import { createGame, updateGamePlaytime } from "./library-sync"; +import { GameRunning } from "@types"; const gamesPlaytime = new Map< number, @@ -46,10 +47,6 @@ export const watchProcesses = async () => { const zero = gamePlaytime.lastTick; const delta = performance.now() - zero; - if (WindowManager.mainWindow) { - WindowManager.mainWindow.webContents.send("on-playtime", game.id); - } - await gameRepository.update(game.id, { playTimeInMilliseconds: game.playTimeInMilliseconds + delta, lastTimePlayed: new Date(), @@ -92,10 +89,20 @@ export const watchProcesses = async () => { gameRepository.update({ objectID: game.objectID }, { remoteId }); }); } - - if (WindowManager.mainWindow) { - WindowManager.mainWindow.webContents.send("on-game-close", game.id); - } } } + + if (WindowManager.mainWindow) { + const gamesRunning = Array.from(gamesPlaytime.entries()).map((entry) => { + return { + id: entry[0], + sessionDurationInMillis: performance.now() - entry[1].firstTick, + }; + }); + + WindowManager.mainWindow.webContents.send( + "on-games-running", + gamesRunning as Pick[] + ); + } }; diff --git a/src/preload/index.ts b/src/preload/index.ts index 0cacafe3..493a3795 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -8,6 +8,7 @@ import type { UserPreferences, AppUpdaterEvent, StartGameDownloadPayload, + GameRunning, } from "@types"; contextBridge.exposeInMainWorld("electron", { @@ -84,17 +85,15 @@ contextBridge.exposeInMainWorld("electron", { ipcRenderer.invoke("deleteGameFolder", gameId), getGameByObjectID: (objectID: string) => ipcRenderer.invoke("getGameByObjectID", objectID), - onPlaytime: (cb: (gameId: number) => void) => { - const listener = (_event: Electron.IpcRendererEvent, gameId: number) => - cb(gameId); - ipcRenderer.on("on-playtime", listener); - return () => ipcRenderer.removeListener("on-playtime", listener); - }, - onGameClose: (cb: (gameId: number) => void) => { - const listener = (_event: Electron.IpcRendererEvent, gameId: number) => - cb(gameId); - ipcRenderer.on("on-game-close", listener); - return () => ipcRenderer.removeListener("on-game-close", listener); + onGamesRunning: ( + cb: ( + gamesRunning: Pick[] + ) => void + ) => { + const listener = (_event: Electron.IpcRendererEvent, gamesRunning) => + cb(gamesRunning); + ipcRenderer.on("on-games-running", listener); + return () => ipcRenderer.removeListener("on-games-running", listener); }, onLibraryBatchComplete: (cb: () => void) => { const listener = (_event: Electron.IpcRendererEvent) => cb(); diff --git a/src/renderer/src/app.tsx b/src/renderer/src/app.tsx index 45a0cb43..d98f1e1f 100644 --- a/src/renderer/src/app.tsx +++ b/src/renderer/src/app.tsx @@ -22,6 +22,7 @@ import { closeToast, setUserDetails, setProfileBackground, + setGameRunning, } from "@renderer/features"; import { useTranslation } from "react-i18next"; @@ -31,7 +32,7 @@ export interface AppProps { export function App() { const contentRef = useRef(null); - const { updateLibrary } = useLibrary(); + const { updateLibrary, library } = useLibrary(); const { t } = useTranslation("app"); @@ -110,6 +111,32 @@ export function App() { }); }, [fetchUserDetails, t, showSuccessToast, updateUserDetails]); + useEffect(() => { + const unsubscribe = window.electron.onGamesRunning((gamesRunning) => { + if (gamesRunning.length) { + const lastGame = gamesRunning[gamesRunning.length - 1]; + const libraryGame = library.find( + (library) => library.id === lastGame.id + ); + + if (libraryGame) { + dispatch( + setGameRunning({ + ...libraryGame, + sessionDurationInMillis: lastGame.sessionDurationInMillis, + }) + ); + return; + } + } + dispatch(setGameRunning(null)); + }); + + return () => { + unsubscribe(); + }; + }, [dispatch, library]); + useEffect(() => { const listeners = [ window.electron.onSignIn(onSignIn), diff --git a/src/renderer/src/components/sidebar/sidebar-profile.css.ts b/src/renderer/src/components/sidebar/sidebar-profile.css.ts index 9681c866..d01b07f1 100644 --- a/src/renderer/src/components/sidebar/sidebar-profile.css.ts +++ b/src/renderer/src/components/sidebar/sidebar-profile.css.ts @@ -9,7 +9,7 @@ export const profileButton = style({ padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 2}px`, color: vars.color.muted, borderBottom: `solid 1px ${vars.color.border}`, - boxShadow: "0px 0px 15px 0px #000000", + boxShadow: "0px 0px 15px 0px rgb(0 0 0 / 70%)", ":hover": { backgroundColor: "rgba(255, 255, 255, 0.15)", }, @@ -20,6 +20,7 @@ export const profileButtonContent = style({ alignItems: "center", gap: `${SPACING_UNIT + SPACING_UNIT / 2}px`, height: "40px", + width: "100%", }); export const profileAvatar = style({ @@ -39,6 +40,8 @@ export const profileButtonInformation = style({ display: "flex", flexDirection: "column", alignItems: "flex-start", + flex: "1", + minWidth: 0, }); export const statusBadge = style({ @@ -55,4 +58,9 @@ export const statusBadge = style({ export const profileButtonTitle = style({ fontWeight: "bold", fontSize: vars.size.body, + width: "100%", + textAlign: "left", + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", }); diff --git a/src/renderer/src/components/sidebar/sidebar-profile.tsx b/src/renderer/src/components/sidebar/sidebar-profile.tsx index c86aecb7..914481b0 100644 --- a/src/renderer/src/components/sidebar/sidebar-profile.tsx +++ b/src/renderer/src/components/sidebar/sidebar-profile.tsx @@ -2,14 +2,19 @@ import { useNavigate } from "react-router-dom"; import { PersonIcon } from "@primer/octicons-react"; import * as styles from "./sidebar-profile.css"; -import { useUserDetails } from "@renderer/hooks"; +import { useAppSelector, useUserDetails } from "@renderer/hooks"; import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; export function SidebarProfile() { const navigate = useNavigate(); + const { t } = useTranslation("sidebar"); + const { userDetails, profileBackground } = useUserDetails(); + const { gameRunning } = useAppSelector((state) => state.gameRunning); + const handleButtonClick = () => { if (userDetails === null) { window.electron.openAuthWindow(); @@ -46,9 +51,24 @@ export function SidebarProfile() {

- {userDetails ? userDetails.displayName : "Sign in"} + {userDetails ? userDetails.displayName : t("sign_in")}

+ + {userDetails && gameRunning && ( +
+ {gameRunning.title} +
+ )}
+ + {userDetails && gameRunning && ( + {gameRunning.title} + )} ); diff --git a/src/renderer/src/context/game-details/game-details.context.tsx b/src/renderer/src/context/game-details/game-details.context.tsx index ad32f987..19d2cc72 100644 --- a/src/renderer/src/context/game-details/game-details.context.tsx +++ b/src/renderer/src/context/game-details/game-details.context.tsx @@ -109,20 +109,19 @@ export function GameDetailsContextProvider({ }, [objectID, gameTitle, dispatch]); useEffect(() => { - const listeners = [ - window.electron.onGameClose(() => { - if (isGameRunning) setisGameRunning(false); - }), - window.electron.onPlaytime((gameId) => { - if (gameId === game?.id) { - if (!isGameRunning) setisGameRunning(true); - updateGame(); - } - }), - ]; + const unsubscribe = window.electron.onGamesRunning((gamesIds) => { + const updatedIsGameRunning = + !!game?.id && + !!gamesIds.find((gameRunning) => gameRunning.id == game.id); + if (isGameRunning != updatedIsGameRunning) { + updateGame(); + } + + setisGameRunning(updatedIsGameRunning); + }); return () => { - listeners.forEach((unsubscribe) => unsubscribe()); + unsubscribe(); }; }, [game?.id, isGameRunning, updateGame]); diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index b7707d6f..64f8d0a3 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -71,8 +71,11 @@ declare global { removeGame: (gameId: number) => Promise; deleteGameFolder: (gameId: number) => Promise; getGameByObjectID: (objectID: string) => Promise; - onPlaytime: (cb: (gameId: number) => void) => () => Electron.IpcRenderer; - onGameClose: (cb: (gameId: number) => void) => () => Electron.IpcRenderer; + onGamesRunning: ( + cb: ( + gamesRunning: Pick[] + ) => void + ) => () => Electron.IpcRenderer; onLibraryBatchComplete: (cb: () => void) => () => Electron.IpcRenderer; /* User preferences */ diff --git a/src/renderer/src/features/index.ts b/src/renderer/src/features/index.ts index f3132520..fdc23e68 100644 --- a/src/renderer/src/features/index.ts +++ b/src/renderer/src/features/index.ts @@ -5,3 +5,4 @@ export * from "./download-slice"; export * from "./window-slice"; export * from "./toast-slice"; export * from "./user-details-slice"; +export * from "./running-game-slice"; diff --git a/src/renderer/src/features/running-game-slice.ts b/src/renderer/src/features/running-game-slice.ts new file mode 100644 index 00000000..b3fb0a9d --- /dev/null +++ b/src/renderer/src/features/running-game-slice.ts @@ -0,0 +1,22 @@ +import { PayloadAction, createSlice } from "@reduxjs/toolkit"; +import { GameRunning } from "@types"; + +export interface GameRunningState { + gameRunning: GameRunning | null; +} + +const initialState: GameRunningState = { + gameRunning: null, +}; + +export const gameRunningSlice = createSlice({ + name: "running-game", + initialState, + reducers: { + setGameRunning: (state, action: PayloadAction) => { + state.gameRunning = action.payload; + }, + }, +}); + +export const { setGameRunning } = gameRunningSlice.actions; diff --git a/src/renderer/src/helpers.ts b/src/renderer/src/helpers.ts index 19d1969c..d37612d4 100644 --- a/src/renderer/src/helpers.ts +++ b/src/renderer/src/helpers.ts @@ -43,5 +43,5 @@ export const buildGameDetailsPath = ( return `/game/${game.shop}/${game.objectID}?${searchParams.toString()}`; }; -export const darkenColor = (color: string, amount: number) => - new Color(color).darken(amount).toString(); +export const darkenColor = (color: string, amount: number, alpha: number = 1) => + new Color(color).darken(amount).alpha(alpha).toString(); diff --git a/src/renderer/src/hooks/use-date.ts b/src/renderer/src/hooks/use-date.ts index f5e8204e..01f55610 100644 --- a/src/renderer/src/hooks/use-date.ts +++ b/src/renderer/src/hooks/use-date.ts @@ -1,4 +1,4 @@ -import { formatDistance } from "date-fns"; +import { formatDistance, subMilliseconds } from "date-fns"; import type { FormatDistanceOptions } from "date-fns"; import { ptBR, @@ -52,5 +52,20 @@ export function useDate() { return ""; } }, + + formatDiffInMillis: ( + millis: number, + baseDate: string | number | Date, + options?: FormatDistanceOptions + ) => { + try { + return formatDistance(subMilliseconds(new Date(), millis), baseDate, { + ...options, + locale: getDateLocale(), + }); + } catch (err) { + return ""; + } + }, }; } diff --git a/src/renderer/src/hooks/use-user-details.ts b/src/renderer/src/hooks/use-user-details.ts index 75de473f..1d8257f4 100644 --- a/src/renderer/src/hooks/use-user-details.ts +++ b/src/renderer/src/hooks/use-user-details.ts @@ -36,8 +36,7 @@ export function useUserDetails() { format: "hex", }); - const profileBackground = `linear-gradient(135deg, ${darkenColor(output as string, 0.6)}, ${darkenColor(output as string, 0.8)})`; - + const profileBackground = `linear-gradient(135deg, ${darkenColor(output as string, 0.6)}, ${darkenColor(output as string, 0.8, 0.7)})`; dispatch(setProfileBackground(profileBackground)); window.localStorage.setItem( @@ -45,9 +44,13 @@ export function useUserDetails() { JSON.stringify({ ...userDetails, profileBackground }) ); } else { - dispatch(setProfileBackground(null)); + const profileBackground = `#151515B3`; + dispatch(setProfileBackground(profileBackground)); - window.localStorage.setItem("userDetails", JSON.stringify(userDetails)); + window.localStorage.setItem( + "userDetails", + JSON.stringify({ ...userDetails, profileBackground }) + ); } }, [dispatch] diff --git a/src/renderer/src/pages/game-details/game-details.css.ts b/src/renderer/src/pages/game-details/game-details.css.ts index fd8a48be..8dc5d4cb 100644 --- a/src/renderer/src/pages/game-details/game-details.css.ts +++ b/src/renderer/src/pages/game-details/game-details.css.ts @@ -138,8 +138,9 @@ export const randomizerButton = style({ bottom: `${26 + SPACING_UNIT * 2}px`, /* Scroll bar + spacing */ right: `${9 + SPACING_UNIT * 2}px`, - boxShadow: "rgba(255, 255, 255, 0.1) 0px 0px 10px 3px", + boxShadow: "rgba(255, 255, 255, 0.1) 0px 0px 10px 1px", border: `solid 2px ${vars.color.border}`, + zIndex: "1", backgroundColor: vars.color.background, ":hover": { backgroundColor: vars.color.background, diff --git a/src/renderer/src/pages/user/user-content.tsx b/src/renderer/src/pages/user/user-content.tsx index 0c32f989..ad620868 100644 --- a/src/renderer/src/pages/user/user-content.tsx +++ b/src/renderer/src/pages/user/user-content.tsx @@ -6,9 +6,14 @@ import { SPACING_UNIT, vars } from "@renderer/theme.css"; import { useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import SteamLogo from "@renderer/assets/steam-logo.svg?react"; -import { useDate, useToast, useUserDetails } from "@renderer/hooks"; +import { + useAppSelector, + useDate, + useToast, + useUserDetails, +} from "@renderer/hooks"; import { useNavigate } from "react-router-dom"; -import { buildGameDetailsPath } from "@renderer/helpers"; +import { buildGameDetailsPath, steamUrlBuilder } from "@renderer/helpers"; import { PersonIcon, TelescopeIcon } from "@primer/octicons-react"; import { Button } from "@renderer/components"; import { UserEditProfileModal } from "./user-edit-modal"; @@ -33,6 +38,8 @@ export function UserContent({ const [showEditProfileModal, setShowEditProfileModal] = useState(false); const [showSignOutModal, setShowSignOutModal] = useState(false); + const { gameRunning } = useAppSelector((state) => state.gameRunning); + const navigate = useNavigate(); const numberFormatter = useMemo(() => { @@ -41,7 +48,7 @@ export function UserContent({ }); }, [i18n.language]); - const { formatDistance } = useDate(); + const { formatDistance, formatDiffInMillis } = useDate(); const formatPlayTime = () => { const seconds = userProfile.libraryGames.reduce( @@ -102,10 +109,32 @@ export function UserContent({
+ {gameRunning && isMe && ( +
+ )} + +
+
{userProfile.profileImageUrl ? (

{userProfile.displayName}

+ {isMe && gameRunning && ( +
+
+

{gameRunning.title}

+
+ + {t("playing_for", { + amount: formatDiffInMillis( + gameRunning.sessionDurationInMillis, + new Date() + ), + })} + +
+ )}
{isMe && ( -
+
<> +
+

{t("sign_out_modal_text")}

+
+ - + +
diff --git a/src/renderer/src/pages/user/user.css.ts b/src/renderer/src/pages/user/user.css.ts index 3a9b4c86..63871c26 100644 --- a/src/renderer/src/pages/user/user.css.ts +++ b/src/renderer/src/pages/user/user.css.ts @@ -4,7 +4,6 @@ import { style } from "@vanilla-extract/css"; export const wrapper = style({ padding: "24px", width: "100%", - height: "100%", display: "flex", flexDirection: "column", gap: `${SPACING_UNIT * 3}px`, @@ -33,6 +32,7 @@ export const profileAvatarContainer = style({ overflow: "hidden", border: `solid 1px ${vars.color.border}`, boxShadow: "0px 0px 5px 0px rgba(0, 0, 0, 0.7)", + zIndex: 1, }); export const profileAvatarEditContainer = style({ @@ -56,7 +56,6 @@ export const profileAvatar = style({ borderRadius: "50%", overflow: "hidden", objectFit: "cover", - animationPlayState: "paused", }); export const profileAvatarEditOverlay = style({ @@ -72,8 +71,10 @@ export const profileAvatarEditOverlay = style({ export const profileInformation = style({ display: "flex", flexDirection: "column", + gap: `${SPACING_UNIT}px`, alignItems: "flex-start", color: "#c0c1c7", + zIndex: 1, }); export const profileContent = style({ @@ -189,10 +190,18 @@ export const noDownloads = style({ gap: `${SPACING_UNIT}px`, }); +export const signOutModalContent = style({ + display: "flex", + width: "100%", + flexDirection: "column", + gap: `${SPACING_UNIT}px`, +}); + export const signOutModalButtonsContainer = style({ display: "flex", width: "100%", justifyContent: "end", alignItems: "center", gap: `${SPACING_UNIT}px`, + paddingTop: `${SPACING_UNIT}px`, }); diff --git a/src/renderer/src/store.ts b/src/renderer/src/store.ts index 9bc0c950..0f2bee9f 100644 --- a/src/renderer/src/store.ts +++ b/src/renderer/src/store.ts @@ -7,6 +7,7 @@ import { userPreferencesSlice, toastSlice, userDetailsSlice, + gameRunningSlice, } from "@renderer/features"; export const store = configureStore({ @@ -18,6 +19,7 @@ export const store = configureStore({ download: downloadSlice.reducer, toast: toastSlice.reducer, userDetails: userDetailsSlice.reducer, + gameRunning: gameRunningSlice.reducer, }, }); diff --git a/src/types/index.ts b/src/types/index.ts index 153fdc9e..c1da0d08 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -127,6 +127,15 @@ export interface Game { export type LibraryGame = Omit; +export interface GameRunning { + id: number; + title: string; + iconUrl: string; + objectID: string; + shop: GameShop; + sessionDurationInMillis: number; +} + export interface DownloadProgress { downloadSpeed: number; timeRemaining: number; diff --git a/yarn.lock b/yarn.lock index b822ab1d..1e03e6ee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1251,6 +1251,25 @@ "@types/node" "*" "@types/responselike" "^1.0.0" +"@types/color-convert@*": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@types/color-convert/-/color-convert-2.0.3.tgz#e93f5c991eda87a945058b47044f5f0008b0dce9" + integrity sha512-2Q6wzrNiuEvYxVQqhh7sXM2mhIhvZR/Paq4FdsQkOMgWsCIkKvSGj8Le1/XalulrmgOzPMqNa0ix+ePY4hTrfg== + dependencies: + "@types/color-name" "*" + +"@types/color-name@*": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.4.tgz#e002611ff627347818d440a05e81650e9a4053b8" + integrity sha512-hulKeREDdLFesGQjl96+4aoJSHY5b2GRjagzzcqCfIrWhe5vkCqIvrLbqzBaI1q94Vg8DNJZZqTR5ocdWmWclg== + +"@types/color@^3.0.6": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/color/-/color-3.0.6.tgz#29c27a99d4de2975e1676712679a0bd7f646a3fb" + integrity sha512-NMiNcZFRUAiUUCCf7zkAelY8eV3aKqfbzyFQlXpPIEeoNDbsEHGpb854V3gzTsGKYj830I5zPuOwU/TP5/cW6A== + dependencies: + "@types/color-convert" "*" + "@types/conventional-commits-parser@^5.0.0": version "5.0.0" resolved "https://registry.yarnpkg.com/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz#8c9d23e0b415b24b91626d07017303755d542dc8"