From d6b80c9c7be18acd52d9b0e18b8bb8ff04745271 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Fri, 20 Dec 2024 10:39:06 -0500 Subject: [PATCH] FE: Update User menu to use react select 5 (#24281) --- .../components/AvatarTopNav/AvatarTopNav.tsx | 1 + frontend/components/AvatarTopNav/_styles.scss | 1 - .../top_nav/SiteTopNav/SiteTopNav.tests.tsx | 148 ++++++++--- .../top_nav/SiteTopNav/SiteTopNav.tsx | 2 +- .../components/top_nav/UserMenu/UserMenu.tsx | 235 ++++++++++++++++-- .../components/top_nav/UserMenu/_styles.scss | 60 ----- frontend/interfaces/dropdownOption.ts | 1 + frontend/layouts/CoreLayout/CoreLayout.tsx | 12 +- frontend/styles/var/colors.ts | 2 +- 9 files changed, 328 insertions(+), 134 deletions(-) delete mode 100644 frontend/components/top_nav/UserMenu/_styles.scss diff --git a/frontend/components/AvatarTopNav/AvatarTopNav.tsx b/frontend/components/AvatarTopNav/AvatarTopNav.tsx index bcbfa3ff3a47..47b3a2a10f81 100644 --- a/frontend/components/AvatarTopNav/AvatarTopNav.tsx +++ b/frontend/components/AvatarTopNav/AvatarTopNav.tsx @@ -39,6 +39,7 @@ const Avatar = ({ className, size, user }: IAvatarInterface): JSX.Element => { src={gravatar_url_dark || DEFAULT_GRAVATAR_LINK_DARK} onError={onError} onLoad={onLoad} + data-testid="user-avatar" /> ); diff --git a/frontend/components/AvatarTopNav/_styles.scss b/frontend/components/AvatarTopNav/_styles.scss index b4cbb6e6bb38..04d71e5cc29d 100644 --- a/frontend/components/AvatarTopNav/_styles.scss +++ b/frontend/components/AvatarTopNav/_styles.scss @@ -1,5 +1,4 @@ .avatar-wrapper-top-nav { - margin-right: 8px; svg { display: block; } diff --git a/frontend/components/top_nav/SiteTopNav/SiteTopNav.tests.tsx b/frontend/components/top_nav/SiteTopNav/SiteTopNav.tests.tsx index 5ef627eea72b..d1308b0aa878 100644 --- a/frontend/components/top_nav/SiteTopNav/SiteTopNav.tests.tsx +++ b/frontend/components/top_nav/SiteTopNav/SiteTopNav.tests.tsx @@ -37,18 +37,29 @@ describe("SiteTopNav - component", () => { /> ); - await user.click(screen.getByTestId("user-menu")); + await user.click(screen.getByTestId("user-avatar")); expect(screen.getByText(/hosts/i)).toBeInTheDocument(); expect(screen.getByText(/controls/i)).toBeInTheDocument(); expect(screen.getByText(/software/i)).toBeInTheDocument(); expect(screen.getByText(/queries/i)).toBeInTheDocument(); expect(screen.getByText(/policies/i)).toBeInTheDocument(); - expect(screen.getByText(/settings/i)).toBeInTheDocument(); - expect(screen.getByText(/manage users/i)).toBeInTheDocument(); - expect(screen.getByText(/my account/i)).toBeInTheDocument(); - expect(screen.getByText(/documentation/i)).toBeInTheDocument(); - expect(screen.getByText(/sign out/i)).toBeInTheDocument(); + expect( + screen.getByRole("menuitem", { name: /settings/i }) + ).toBeInTheDocument(); + + expect( + screen.getByRole("menuitem", { name: /manage users/i }) + ).toBeInTheDocument(); + expect( + screen.getByRole("menuitem", { name: /my account/i }) + ).toBeInTheDocument(); + expect( + screen.getByRole("menuitem", { name: /documentation/i }) + ).toBeInTheDocument(); + expect( + screen.getByRole("menuitem", { name: /sign out/i }) + ).toBeInTheDocument(); }); it("renders correct navigation for free global maintainer", async () => { const render = createCustomRenderer({ @@ -73,16 +84,22 @@ describe("SiteTopNav - component", () => { /> ); - await user.click(screen.getByTestId("user-menu")); + await user.click(screen.getByTestId("user-avatar")); expect(screen.getByText(/hosts/i)).toBeInTheDocument(); expect(screen.getByText(/controls/i)).toBeInTheDocument(); expect(screen.getByText(/software/i)).toBeInTheDocument(); expect(screen.getByText(/queries/i)).toBeInTheDocument(); expect(screen.getByText(/policies/i)).toBeInTheDocument(); - expect(screen.getByText(/my account/i)).toBeInTheDocument(); - expect(screen.getByText(/documentation/i)).toBeInTheDocument(); - expect(screen.getByText(/sign out/i)).toBeInTheDocument(); + expect( + screen.getByRole("menuitem", { name: /my account/i }) + ).toBeInTheDocument(); + expect( + screen.getByRole("menuitem", { name: /documentation/i }) + ).toBeInTheDocument(); + expect( + screen.getByRole("menuitem", { name: /sign out/i }) + ).toBeInTheDocument(); expect(screen.queryByText(/settings/i)).not.toBeInTheDocument(); expect(screen.queryByText(/manage users/i)).not.toBeInTheDocument(); @@ -109,15 +126,21 @@ describe("SiteTopNav - component", () => { /> ); - await user.click(screen.getByTestId("user-menu")); + await user.click(screen.getByTestId("user-avatar")); expect(screen.getByText(/hosts/i)).toBeInTheDocument(); expect(screen.getByText(/software/i)).toBeInTheDocument(); expect(screen.getByText(/queries/i)).toBeInTheDocument(); expect(screen.getByText(/policies/i)).toBeInTheDocument(); - expect(screen.getByText(/my account/i)).toBeInTheDocument(); - expect(screen.getByText(/documentation/i)).toBeInTheDocument(); - expect(screen.getByText(/sign out/i)).toBeInTheDocument(); + expect( + screen.getByRole("menuitem", { name: /my account/i }) + ).toBeInTheDocument(); + expect( + screen.getByRole("menuitem", { name: /documentation/i }) + ).toBeInTheDocument(); + expect( + screen.getByRole("menuitem", { name: /sign out/i }) + ).toBeInTheDocument(); expect(screen.queryByText(/controls/i)).not.toBeInTheDocument(); expect(screen.queryByText(/settings/i)).not.toBeInTheDocument(); @@ -143,18 +166,28 @@ describe("SiteTopNav - component", () => { /> ); - await user.click(screen.getByTestId("user-menu")); + await user.click(screen.getByTestId("user-avatar")); expect(screen.getByText(/hosts/i)).toBeInTheDocument(); expect(screen.getByText(/controls/i)).toBeInTheDocument(); expect(screen.getByText(/software/i)).toBeInTheDocument(); expect(screen.getByText(/queries/i)).toBeInTheDocument(); expect(screen.getByText(/policies/i)).toBeInTheDocument(); - expect(screen.getByText(/settings/i)).toBeInTheDocument(); - expect(screen.getByText(/manage users/i)).toBeInTheDocument(); - expect(screen.getByText(/my account/i)).toBeInTheDocument(); - expect(screen.getByText(/documentation/i)).toBeInTheDocument(); - expect(screen.getByText(/sign out/i)).toBeInTheDocument(); + expect( + screen.getByRole("menuitem", { name: /settings/i }) + ).toBeInTheDocument(); + expect( + screen.getByRole("menuitem", { name: /manage users/i }) + ).toBeInTheDocument(); + expect( + screen.getByRole("menuitem", { name: /my account/i }) + ).toBeInTheDocument(); + expect( + screen.getByRole("menuitem", { name: /documentation/i }) + ).toBeInTheDocument(); + expect( + screen.getByRole("menuitem", { name: /sign out/i }) + ).toBeInTheDocument(); }); it("renders correct navigation for premium global maintainer", async () => { const render = createCustomRenderer({ @@ -179,16 +212,22 @@ describe("SiteTopNav - component", () => { /> ); - await user.click(screen.getByTestId("user-menu")); + await user.click(screen.getByTestId("user-avatar")); expect(screen.getByText(/hosts/i)).toBeInTheDocument(); expect(screen.getByText(/controls/i)).toBeInTheDocument(); expect(screen.getByText(/software/i)).toBeInTheDocument(); expect(screen.getByText(/queries/i)).toBeInTheDocument(); expect(screen.getByText(/policies/i)).toBeInTheDocument(); - expect(screen.getByText(/my account/i)).toBeInTheDocument(); - expect(screen.getByText(/documentation/i)).toBeInTheDocument(); - expect(screen.getByText(/sign out/i)).toBeInTheDocument(); + expect( + screen.getByRole("menuitem", { name: /my account/i }) + ).toBeInTheDocument(); + expect( + screen.getByRole("menuitem", { name: /documentation/i }) + ).toBeInTheDocument(); + expect( + screen.getByRole("menuitem", { name: /sign out/i }) + ).toBeInTheDocument(); expect(screen.queryByText(/settings/i)).not.toBeInTheDocument(); expect(screen.queryByText(/manage users/i)).not.toBeInTheDocument(); @@ -215,15 +254,21 @@ describe("SiteTopNav - component", () => { /> ); - await user.click(screen.getByTestId("user-menu")); + await user.click(screen.getByTestId("user-avatar")); expect(screen.getByText(/hosts/i)).toBeInTheDocument(); expect(screen.getByText(/software/i)).toBeInTheDocument(); expect(screen.getByText(/queries/i)).toBeInTheDocument(); expect(screen.getByText(/policies/i)).toBeInTheDocument(); - expect(screen.getByText(/my account/i)).toBeInTheDocument(); - expect(screen.getByText(/documentation/i)).toBeInTheDocument(); - expect(screen.getByText(/sign out/i)).toBeInTheDocument(); + expect( + screen.getByRole("menuitem", { name: /my account/i }) + ).toBeInTheDocument(); + expect( + screen.getByRole("menuitem", { name: /documentation/i }) + ).toBeInTheDocument(); + expect( + screen.getByRole("menuitem", { name: /sign out/i }) + ).toBeInTheDocument(); expect(screen.queryByText(/controls/i)).not.toBeInTheDocument(); expect(screen.queryByText(/settings/i)).not.toBeInTheDocument(); @@ -252,17 +297,25 @@ describe("SiteTopNav - component", () => { /> ); - await user.click(screen.getByTestId("user-menu")); + await user.click(screen.getByTestId("user-avatar")); expect(screen.getByText(/hosts/i)).toBeInTheDocument(); expect(screen.getByText(/controls/i)).toBeInTheDocument(); expect(screen.getByText(/software/i)).toBeInTheDocument(); expect(screen.getByText(/queries/i)).toBeInTheDocument(); expect(screen.getByText(/policies/i)).toBeInTheDocument(); - expect(screen.getByText(/settings/i)).toBeInTheDocument(); - expect(screen.getByText(/my account/i)).toBeInTheDocument(); - expect(screen.getByText(/documentation/i)).toBeInTheDocument(); - expect(screen.getByText(/sign out/i)).toBeInTheDocument(); + expect( + screen.getByRole("menuitem", { name: /settings/i }) + ).toBeInTheDocument(); + expect( + screen.getByRole("menuitem", { name: /my account/i }) + ).toBeInTheDocument(); + expect( + screen.getByRole("menuitem", { name: /documentation/i }) + ).toBeInTheDocument(); + expect( + screen.getByRole("menuitem", { name: /sign out/i }) + ).toBeInTheDocument(); expect(screen.queryByText(/manage users/i)).not.toBeInTheDocument(); }); @@ -289,16 +342,22 @@ describe("SiteTopNav - component", () => { /> ); - await user.click(screen.getByTestId("user-menu")); + await user.click(screen.getByTestId("user-avatar")); expect(screen.getByText(/hosts/i)).toBeInTheDocument(); expect(screen.getByText(/controls/i)).toBeInTheDocument(); expect(screen.getByText(/software/i)).toBeInTheDocument(); expect(screen.getByText(/queries/i)).toBeInTheDocument(); expect(screen.getByText(/policies/i)).toBeInTheDocument(); - expect(screen.getByText(/my account/i)).toBeInTheDocument(); - expect(screen.getByText(/documentation/i)).toBeInTheDocument(); - expect(screen.getByText(/sign out/i)).toBeInTheDocument(); + expect( + screen.getByRole("menuitem", { name: /my account/i }) + ).toBeInTheDocument(); + expect( + screen.getByRole("menuitem", { name: /documentation/i }) + ).toBeInTheDocument(); + expect( + screen.getByRole("menuitem", { name: /sign out/i }) + ).toBeInTheDocument(); expect(screen.queryByText(/settings/i)).not.toBeInTheDocument(); expect(screen.queryByText(/manage users/i)).not.toBeInTheDocument(); @@ -325,15 +384,22 @@ describe("SiteTopNav - component", () => { /> ); - await user.click(screen.getByTestId("user-menu")); + await user.click(screen.getByTestId("user-avatar")); expect(screen.getByText(/hosts/i)).toBeInTheDocument(); expect(screen.getByText(/software/i)).toBeInTheDocument(); expect(screen.getByText(/queries/i)).toBeInTheDocument(); expect(screen.getByText(/policies/i)).toBeInTheDocument(); - expect(screen.getByText(/my account/i)).toBeInTheDocument(); - expect(screen.getByText(/documentation/i)).toBeInTheDocument(); - expect(screen.getByText(/sign out/i)).toBeInTheDocument(); + + expect( + screen.getByRole("menuitem", { name: /my account/i }) + ).toBeInTheDocument(); + expect( + screen.getByRole("menuitem", { name: /documentation/i }) + ).toBeInTheDocument(); + expect( + screen.getByRole("menuitem", { name: /sign out/i }) + ).toBeInTheDocument(); expect(screen.queryByText(/controls/i)).not.toBeInTheDocument(); expect(screen.queryByText(/settings/i)).not.toBeInTheDocument(); diff --git a/frontend/components/top_nav/SiteTopNav/SiteTopNav.tsx b/frontend/components/top_nav/SiteTopNav/SiteTopNav.tsx index 2f0f0d3b1fcd..c1fc399914d9 100644 --- a/frontend/components/top_nav/SiteTopNav/SiteTopNav.tsx +++ b/frontend/components/top_nav/SiteTopNav/SiteTopNav.tsx @@ -9,10 +9,10 @@ import { IUser } from "interfaces/user"; import { QueryParams } from "utilities/url"; import LinkWithContext from "components/LinkWithContext"; -import UserMenu from "components/top_nav/UserMenu"; // @ts-ignore import OrgLogoIcon from "components/icons/OrgLogoIcon"; +import UserMenu from "../UserMenu"; import getNavItems, { INavItem } from "./navItems"; interface ISiteTopNavProps { diff --git a/frontend/components/top_nav/UserMenu/UserMenu.tsx b/frontend/components/top_nav/UserMenu/UserMenu.tsx index b1228abc25ce..4a07f1b48214 100644 --- a/frontend/components/top_nav/UserMenu/UserMenu.tsx +++ b/frontend/components/top_nav/UserMenu/UserMenu.tsx @@ -1,12 +1,22 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; +import { keyframes } from "@emotion/react"; +import Select, { + StylesConfig, + DropdownIndicatorProps, + OptionProps, + components, + GroupBase, +} from "react-select-5"; import { IUser } from "interfaces/user"; import { ITeam } from "interfaces/team"; +import { IDropdownOption } from "interfaces/dropdownOption"; +import PATHS from "router/paths"; import { getSortedTeamOptions } from "utilities/helpers"; -import PATHS from "router/paths"; +import { PADDING } from "styles/var/padding"; +import { COLORS } from "styles/var/colors"; -// @ts-ignore -import DropdownButton from "components/buttons/DropdownButton"; +import Icon from "components/Icon"; import AvatarTopNav from "../../AvatarTopNav"; const baseClass = "user-menu"; @@ -19,6 +29,61 @@ interface IUserMenuProps { currentUser: IUser; } +const bounceDownAnimation = keyframes` + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(3px); + } +`; + +const getOptionBackgroundColor = (state: any) => { + return state.isFocused ? COLORS["ui-vibrant-blue-10"] : "transparent"; +}; + +const CustomDropdownIndicator = ( + props: DropdownIndicatorProps< + IDropdownOption, + false, + GroupBase + > +) => { + return ( + + + + ); +}; + +const CustomOption: React.FC< + OptionProps & { isKeyboardFocus: boolean } +> = (props) => { + const { innerRef, data, isFocused, isKeyboardFocus } = props; + + return ( + +
+ {data.label} +
+
+ ); +}; + const UserMenu = ({ onLogout, onUserMenuItemClick, @@ -26,28 +91,55 @@ const UserMenu = ({ isGlobalAdmin, currentUser, }: IUserMenuProps): JSX.Element => { - const accountNavigate = onUserMenuItemClick(PATHS.ACCOUNT); + // Work around for react-select-5 not having :focus-visible pseudo class that can style dropdown on keyboard tab only + // Work around preventing react-select-5 from auto focusing first option unless using keyboard + const [isKeyboardFocus, setIsKeyboardFocus] = useState(false); + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Tab") { + setIsKeyboardFocus(true); + } + }; + + const handleMouseDown = () => { + setIsKeyboardFocus(false); + }; + + document.addEventListener("keydown", handleKeyDown); + document.addEventListener("mousedown", handleMouseDown); + + return () => { + document.removeEventListener("keydown", handleKeyDown); + document.removeEventListener("mousedown", handleMouseDown); + }; + }, []); + const dropdownItems = [ { label: "My account", - onClick: accountNavigate, + value: "my-account", + onClick: () => onUserMenuItemClick(PATHS.ACCOUNT), }, { label: "Documentation", - onClick: () => window.open("https://fleetdm.com/docs", "_blank"), + value: "documentation", + onClick: () => { + window.open("https://fleetdm.com/docs", "_blank"); + }, }, { label: "Sign out", + value: "sign-out", onClick: onLogout, }, ]; if (isGlobalAdmin) { - const manageUsersNavigate = onUserMenuItemClick(PATHS.ADMIN_USERS); - const manageUserNavItem = { label: "Manage users", - onClick: manageUsersNavigate, + value: "manage-users", + onClick: () => onUserMenuItemClick(PATHS.ADMIN_USERS), }; dropdownItems.unshift(manageUserNavItem); } @@ -61,24 +153,129 @@ const UserMenu = ({ currentUser.global_role === "admin" ? PATHS.ADMIN_ORGANIZATION : `${PATHS.TEAM_DETAILS_USERS(sortedTeams[0].value)}`; - const settingsNavigate = onUserMenuItemClick(settingsPath); const adminNavItem = { label: "Settings", - onClick: settingsNavigate, + value: "settings", + onClick: () => onUserMenuItemClick(settingsPath), }; dropdownItems.unshift(adminNavItem); } + const customStyles: StylesConfig = { + control: (provided, state) => ({ + ...provided, + display: "flex", + flexDirection: "row", + width: "max-content", + padding: "8px", + marginRight: "8px", + backgroundColor: "initial", + border: "2px solid transparent", // So tabbing doesn't shift dropdown + borderRadius: "6px", + boxShadow: "none", + cursor: "pointer", + "&:hover": { + boxShadow: "none", + ".user-menu-select__indicator svg": { + animation: `${bounceDownAnimation} 0.3s ease-in-out`, + }, + }, + ...(state.isFocused && + isKeyboardFocus && { + border: `2px solid ${COLORS["ui-blue-25"]}`, + }), + ...(state.menuIsOpen && { + ".user-menu-select__indicator svg": { + transform: "rotate(180deg)", + }, + }), + }), + dropdownIndicator: (provided) => ({ + ...provided, + display: "flex", + padding: "6px", + svg: { + transition: "transform 0.25s ease", + }, + }), + menu: (provided) => ({ + ...provided, + boxShadow: "0 2px 6px rgba(0, 0, 0, 0.1)", + borderRadius: "4px", + zIndex: 6, + marginTop: "7px", + marginRight: "8px", + width: "auto", + minWidth: "100%", + position: "absolute", + left: "auto", + right: "0", + animation: "fade-in 150ms ease-out", + }), + menuList: (provided) => ({ + ...provided, + padding: PADDING["pad-small"], + maxHeight: "initial", // Override react-select default height of 300px + }), + valueContainer: (provided) => ({ + ...provided, + padding: 0, + }), + option: (provided, state) => ({ + ...provided, + padding: "10px 8px", + fontSize: "15px", + backgroundColor: getOptionBackgroundColor(state), + color: COLORS["tooltip-bg"], // TODO: Why the mismatch in names in colors.scss and colors.ts + whiteSpace: "nowrap", + "&:hover": { + backgroundColor: COLORS["ui-vibrant-blue-10"], + }, + "&:active": { + backgroundColor: COLORS["ui-vibrant-blue-10"], + }, + "&:last-child, &:nth-last-of-type(2)": { + borderTop: `1px solid ${COLORS["ui-fleet-black-10"]}`, + }, + }), + }; + + const renderPlaceholder = () => { + return ( + + ); + }; + return (
- - - + + options={dropdownItems} + placeholder={renderPlaceholder()} + styles={customStyles} + components={{ + DropdownIndicator: CustomDropdownIndicator, + IndicatorSeparator: () => null, + Option: (props) => ( + + ), + SingleValue: () => null, + }} + controlShouldRenderValue={false} + isOptionSelected={() => false} + className={baseClass} + classNamePrefix={`${baseClass}-select`} + menuPlacement="bottom" + onChange={(option) => { + option?.onClick && option?.onClick(); + }} + isSearchable={false} + />
); }; + export default UserMenu; diff --git a/frontend/components/top_nav/UserMenu/_styles.scss b/frontend/components/top_nav/UserMenu/_styles.scss deleted file mode 100644 index 0ba541699720..000000000000 --- a/frontend/components/top_nav/UserMenu/_styles.scss +++ /dev/null @@ -1,60 +0,0 @@ -.user-menu { - button { - background-color: transparent; - margin-right: 24px; - transition: transform 0.3s ease; - - &.focus-visible { - border: 1px solid $ui-vibrant-blue-10; - border-right: 0; - } - &:focus:has(+ .dropdown-button__options--opened) { - .icon { - svg { - transform: rotate(180deg); - transition: transform 0.25s ease; - } - } - } - - svg { - transition: transform 0.25s ease; - } - - &:hover svg { - animation: bounceDown 0.3s ease-in-out; - } - - @keyframes bounceDown { - 0%, - 100% { - transform: translateY(0); - } - 50% { - transform: translateY(3px); - } - } - } - - .dropdown-button__options { - padding: $pad-small; - right: 10px; - margin-top: 2px; - - button { - display: inline-block; - min-width: 125px; - - &:hover, - &:focus { - background-color: $ui-vibrant-blue-10; - color: $core-fleet-black; - } - } - - .dropdown-button__option:last-child, - .dropdown-button__option:nth-last-child(2) { - border-top: 1px solid $ui-fleet-black-10; - } - } -} diff --git a/frontend/interfaces/dropdownOption.ts b/frontend/interfaces/dropdownOption.ts index c2e7a73e54e0..455e0ccd6b26 100644 --- a/frontend/interfaces/dropdownOption.ts +++ b/frontend/interfaces/dropdownOption.ts @@ -16,4 +16,5 @@ export interface IDropdownOption { helpText?: ReactNode; premiumOnly?: boolean; tooltipContent?: TooltipContent; + onClick?: (() => void) | void | null; } diff --git a/frontend/layouts/CoreLayout/CoreLayout.tsx b/frontend/layouts/CoreLayout/CoreLayout.tsx index 1074a875de5b..a6c9d5634306 100644 --- a/frontend/layouts/CoreLayout/CoreLayout.tsx +++ b/frontend/layouts/CoreLayout/CoreLayout.tsx @@ -50,17 +50,7 @@ const CoreLayout = ({ children, router, location }: ICoreLayoutProps) => { }; const onUserMenuItemClick = (path: string) => { - return (evt: React.MouseEvent) => { - evt.preventDefault(); - - if (path.indexOf("http") !== -1) { - global.window.open(path, "_blank"); - return false; - } - - router.push(path); - return false; - }; + router.push(path); }; const fullWidthFlash = !currentUser; diff --git a/frontend/styles/var/colors.ts b/frontend/styles/var/colors.ts index 9b3928c4174d..3cc7a2aa4e95 100644 --- a/frontend/styles/var/colors.ts +++ b/frontend/styles/var/colors.ts @@ -3,7 +3,7 @@ export type Colors = keyof typeof COLORS; export const COLORS = { // core colors "core-fleet-black": "#192147", - "core-fleet-blue": "#6A67FE", + "core-fleet-blue": "#6A67FE", // TODO: Why does this match ui-vibrant-blue and not core-fleet-blue "core-fleet-red": "#FF5C83", "core-fleet-purple": "#AE6DDF", "core-fleet-white": "#FFFFFF",