From 3c89d079641db772c90877a026db959fd2acb5b3 Mon Sep 17 00:00:00 2001 From: Leshe4ka Date: Fri, 17 May 2024 14:39:16 +0500 Subject: [PATCH] Colored menu. --- .../Nav/ClusterMenu/ClusterMenu.tsx | 10 +++- .../MenuColorPicker/MenuColorPicker.styled.ts | 50 +++++++++++++++++ .../Menu/MenuColorPicker/MenuColorPicker.tsx | 45 +++++++++++++++ frontend/src/components/Nav/Menu/MenuTab.tsx | 15 ++++- frontend/src/components/Nav/Menu/styled.ts | 14 +++++ frontend/src/components/Nav/Nav.styled.ts | 13 ++++- .../common/Dropdown/Dropdown.styled.ts | 7 ++- .../components/common/Dropdown/Dropdown.tsx | 13 ++++- .../common/Icons/ColorPickerIcon.tsx | 37 ++++++++++++ frontend/src/lib/hooks/useLocalStorage.ts | 2 +- frontend/src/theme/theme.ts | 56 +++++++++++++++++-- 11 files changed, 248 insertions(+), 14 deletions(-) create mode 100644 frontend/src/components/Nav/Menu/MenuColorPicker/MenuColorPicker.styled.ts create mode 100644 frontend/src/components/Nav/Menu/MenuColorPicker/MenuColorPicker.tsx create mode 100644 frontend/src/components/common/Icons/ColorPickerIcon.tsx diff --git a/frontend/src/components/Nav/ClusterMenu/ClusterMenu.tsx b/frontend/src/components/Nav/ClusterMenu/ClusterMenu.tsx index 6618dd5b1..09ffef05e 100644 --- a/frontend/src/components/Nav/ClusterMenu/ClusterMenu.tsx +++ b/frontend/src/components/Nav/ClusterMenu/ClusterMenu.tsx @@ -20,6 +20,7 @@ import { clusterTopicsRelativePath, } from 'lib/paths'; import { useLocation } from 'react-router-dom'; +import { useLocalStorage } from 'lib/hooks/useLocalStorage'; interface ClusterMenuProps { name: Cluster['name']; @@ -38,17 +39,22 @@ const ClusterMenu: FC = ({ features?.includes(key); const [isOpen, setIsOpen] = useState(!!singleMode); const location = useLocation(); + const [colorKey, setColorKey] = useLocalStorage( + `clusterColor-${name}`, + 'transparent' + ); const getIsMenuItemActive = (path: string) => location.pathname.includes(path); return ( - + ); }; diff --git a/frontend/src/components/Nav/Menu/MenuColorPicker/MenuColorPicker.styled.ts b/frontend/src/components/Nav/Menu/MenuColorPicker/MenuColorPicker.styled.ts new file mode 100644 index 000000000..2d0e53f61 --- /dev/null +++ b/frontend/src/components/Nav/Menu/MenuColorPicker/MenuColorPicker.styled.ts @@ -0,0 +1,50 @@ +import styled from 'styled-components'; +import { ClusterColorKey } from 'theme/theme'; + +export const Container = styled.div` + display: grid; + grid-template-columns: repeat(5, 1fr); + grid-template-rows: repeat(2, auto); + padding: 0 8px; + gap: 8px; + border-radius: 8px; + cursor: auto; +`; + +export const ColorCircle = styled.div<{ $colorKey: ClusterColorKey }>` + width: 16px; + height: 16px; + border-radius: 50%; + margin: 4px; + background-color: ${({ $colorKey, theme }) => + theme.clusterColorPicker.backgroundColor[$colorKey]}; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + + &:hover { + outline: ${({ theme }) => `1px solid ${theme.clusterColorPicker.outline}`}; + } + + &:active { + outline: ${({ theme }) => `2px solid ${theme.clusterColorPicker.outline}`}; + } + + ${({ $colorKey, theme }) => + $colorKey === 'transparent' && + ` + border: 1px solid ${theme.clusterColorPicker.transparentCircle.border}; + &:before { + content: '\\2716'; + font-size: 8px; + color: ${theme.clusterColorPicker.transparentCircle.cross}; + } + &:hover { + border: none; + } + &:active { + border: none; + } + `} +`; diff --git a/frontend/src/components/Nav/Menu/MenuColorPicker/MenuColorPicker.tsx b/frontend/src/components/Nav/Menu/MenuColorPicker/MenuColorPicker.tsx new file mode 100644 index 000000000..716561d7f --- /dev/null +++ b/frontend/src/components/Nav/Menu/MenuColorPicker/MenuColorPicker.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { Dropdown } from 'components/common/Dropdown'; +import ColorPickerIcon from 'components/common/Icons/ColorPickerIcon'; +import { ClusterColorKey } from 'theme/theme'; + +import * as S from './MenuColorPicker.styled'; + +interface MenuColorPickerProps { + setColorKey: (key: ClusterColorKey) => void; +} + +const COLOR_KEYS: ClusterColorKey[] = [ + 'transparent', + 'gray', + 'red', + 'orange', + 'lettuce', + 'green', + 'turquoise', + 'blue', + 'violet', + 'pink', +]; + +const MenuColorPicker = ({ setColorKey }: MenuColorPickerProps) => { + const handleCircleCLick = (colorKey: ClusterColorKey) => () => { + setColorKey(colorKey); + }; + + return ( + }> + + {COLOR_KEYS.map((key) => ( + + ))} + + + ); +}; + +export default MenuColorPicker; diff --git a/frontend/src/components/Nav/Menu/MenuTab.tsx b/frontend/src/components/Nav/Menu/MenuTab.tsx index 9214c3b00..7c60b8729 100644 --- a/frontend/src/components/Nav/Menu/MenuTab.tsx +++ b/frontend/src/components/Nav/Menu/MenuTab.tsx @@ -1,5 +1,6 @@ import React, { type FC } from 'react'; import { ServerStatus } from 'generated-sources'; +import MenuColorPicker from 'components/Nav/Menu/MenuColorPicker/MenuColorPicker'; import * as S from './styled'; @@ -8,6 +9,7 @@ export interface MenuTabProps { status: ServerStatus; isOpen: boolean; toggleClusterMenu: () => void; + setColorKey: () => void; } const MenuTab: FC = ({ @@ -15,6 +17,7 @@ const MenuTab: FC = ({ toggleClusterMenu, status, isOpen, + setColorKey, }) => ( @@ -27,9 +30,15 @@ const MenuTab: FC = ({ {title} - - - + + + + + + + + + ); diff --git a/frontend/src/components/Nav/Menu/styled.ts b/frontend/src/components/Nav/Menu/styled.ts index 930d93a85..9658e0d04 100644 --- a/frontend/src/components/Nav/Menu/styled.ts +++ b/frontend/src/components/Nav/Menu/styled.ts @@ -1,6 +1,11 @@ import styled, { css } from 'styled-components'; import { ServerStatus } from 'generated-sources'; +export const ColorPickerWrapper = styled.div` + display: flex; + visibility: hidden; +`; + export const MenuItem = styled('li').attrs({ role: 'menuitem' })<{ $variant: 'primary' | 'secondary'; $isActive?: boolean; @@ -28,6 +33,10 @@ export const MenuItem = styled('li').attrs({ role: 'menuitem' })<{ &:hover { background-color: ${theme.menu[$variant].backgroundColor.hover}; color: ${theme.menu[$variant].color.hover}; + + ${ColorPickerWrapper} { + visibility: visible; + } } &:active { @@ -96,3 +105,8 @@ export const ChevronIcon = styled.path.attrs( )` stroke: ${({ theme }) => theme.menu.primary.chevronIconColor}; `; + +export const ActionsWrapper = styled.div` + display: flex; + align-items: center; +`; diff --git a/frontend/src/components/Nav/Nav.styled.ts b/frontend/src/components/Nav/Nav.styled.ts index 4f6bde408..2c6b89cb8 100644 --- a/frontend/src/components/Nav/Nav.styled.ts +++ b/frontend/src/components/Nav/Nav.styled.ts @@ -1,7 +1,8 @@ import styled from 'styled-components'; +import { ClusterColorKey } from 'theme/theme'; export const List = styled.ul.attrs({ role: 'menu' })` - padding: 2px 4px 6px 4px; + padding: 2px 4px 6px 8px; & > & { padding: 0 0 0 8px; @@ -11,3 +12,13 @@ export const List = styled.ul.attrs({ role: 'menu' })` margin-bottom: 2px; } `; + +export const ClusterList = styled.ul.attrs<{ $colorKey: ClusterColorKey }>({ + role: 'menu', +})` + border-radius: 8px; + padding: 4px 4px 4px 4px; + margin-bottom: 8px; + background-color: ${({ theme, $colorKey }) => + theme.clusterMenu.backgroundColor[$colorKey]}; +`; diff --git a/frontend/src/components/common/Dropdown/Dropdown.styled.ts b/frontend/src/components/common/Dropdown/Dropdown.styled.ts index d7db888a0..99acfca38 100644 --- a/frontend/src/components/common/Dropdown/Dropdown.styled.ts +++ b/frontend/src/components/common/Dropdown/Dropdown.styled.ts @@ -1,6 +1,6 @@ import styled, { css, keyframes } from 'styled-components'; import { ControlledMenu } from '@szhsin/react-menu'; -import { menuSelector, menuItemSelector } from '@szhsin/react-menu/style-utils'; +import { menuItemSelector, menuSelector } from '@szhsin/react-menu/style-utils'; import '@szhsin/react-menu/dist/core.css'; @@ -18,6 +18,7 @@ const menuHide = keyframes` export const Dropdown = styled(ControlledMenu)( ({ theme: { dropdown } }) => css` // container for the menu items + ${menuSelector.name} { border: 1px solid ${dropdown.borderColor}; box-shadow: 0 4px 16px ${dropdown.shadow}; @@ -26,6 +27,7 @@ export const Dropdown = styled(ControlledMenu)( font-size: 14px; background-color: ${dropdown.backgroundColor}; text-align: left; + cursor: auto; } ${menuSelector.stateOpening} { @@ -34,6 +36,7 @@ export const Dropdown = styled(ControlledMenu)( // NOTE: animation-fill-mode: forwards is required to // prevent flickering with React 18 createRoot() + ${menuSelector.stateClosing} { animation: ${menuHide} 0.2s ease-out forwards; } @@ -62,6 +65,8 @@ export const DropdownButton = styled.button` display: flex; cursor: pointer; align-self: center; + padding: 0; + margin: 0 4px; &:disabled { opacity: 0.5; diff --git a/frontend/src/components/common/Dropdown/Dropdown.tsx b/frontend/src/components/common/Dropdown/Dropdown.tsx index daf26ac12..a9b977ec6 100644 --- a/frontend/src/components/common/Dropdown/Dropdown.tsx +++ b/frontend/src/components/common/Dropdown/Dropdown.tsx @@ -10,7 +10,12 @@ interface DropdownProps extends PropsWithChildren> { disabled?: boolean; } -const Dropdown: React.FC = ({ label, disabled, children }) => { +const Dropdown: React.FC = ({ + label, + disabled, + children, + offsetY, +}) => { const ref = useRef(null); const { value: isOpen, setFalse, setTrue } = useBoolean(false); @@ -37,8 +42,12 @@ const Dropdown: React.FC = ({ label, disabled, children }) => { onClose={setFalse} align="end" direction="bottom" - offsetY={10} + offsetY={offsetY ?? 10} viewScroll="auto" + onClick={(e) => { + e.preventDefault(); + e.stopPropagation(); + }} > {children} diff --git a/frontend/src/components/common/Icons/ColorPickerIcon.tsx b/frontend/src/components/common/Icons/ColorPickerIcon.tsx new file mode 100644 index 000000000..3102d97dc --- /dev/null +++ b/frontend/src/components/common/Icons/ColorPickerIcon.tsx @@ -0,0 +1,37 @@ +import React from 'react'; + +const ColorPickerIcon: React.FC = () => { + return ( + + + + + + + + + + + ); +}; + +export default ColorPickerIcon; diff --git a/frontend/src/lib/hooks/useLocalStorage.ts b/frontend/src/lib/hooks/useLocalStorage.ts index d8945620d..4faa99079 100644 --- a/frontend/src/lib/hooks/useLocalStorage.ts +++ b/frontend/src/lib/hooks/useLocalStorage.ts @@ -1,5 +1,5 @@ import { LOCAL_STORAGE_KEY_PREFIX } from 'lib/constants'; -import { useState, useEffect } from 'react'; +import { useEffect, useState } from 'react'; export const useLocalStorage = (featureKey: string, defaultValue: string) => { const key = `${LOCAL_STORAGE_KEY_PREFIX}-${featureKey}`; diff --git a/frontend/src/theme/theme.ts b/frontend/src/theme/theme.ts index 6c8284afd..6fff69fa4 100644 --- a/frontend/src/theme/theme.ts +++ b/frontend/src/theme/theme.ts @@ -75,6 +75,30 @@ const Colors = { '70': '#5959A6', '80': '#3E3E74', }, + clusterColorPicker: { + transparent: 'transparent', + gray: '#E3E6E8', + red: '#E63B19', + orange: '#FF9D00', + lettuce: '#9DD926', + green: '#33CC33', + turquoise: '#40BF95', + blue: '#1A5DE5', + violet: '#6633CC', + pink: '#D926D9', + }, + clusterMenuBackgroundColor: { + transparent: 'transparent', + gray: hexToRgba('#808080', 0.1), + red: hexToRgba('#BF4040', 0.1), + orange: hexToRgba('#BF8340', 0.1), + lettuce: hexToRgba('#93BF40', 0.1), + green: hexToRgba('#40BF40', 0.1), + turquoise: hexToRgba('#40BF95', 0.1), + blue: hexToRgba('#406ABF', 0.1), + violet: hexToRgba('#6A40BF', 0.1), + pink: hexToRgba('#BF40BF', 0.1), + }, }; const baseTheme = { @@ -380,8 +404,8 @@ export const theme = { borderTop: 'none', }, dropdown: { - backgroundColor: Colors.neutral[0], - borderColor: Colors.neutral[5], + backgroundColor: Colors.brand[0], + borderColor: Colors.brand[5], shadow: Colors.transparency[20], item: { color: { @@ -532,6 +556,9 @@ export const theme = { fontWeight: 400, }, }, + clusterMenu: { + backgroundColor: Colors.clusterMenuBackgroundColor, + }, schema: { backgroundColor: { tr: Colors.neutral[5], @@ -815,9 +842,19 @@ export const theme = { }, }, }, + clusterColorPicker: { + backgroundColor: Colors.clusterColorPicker, + outline: Colors.brand[80], + transparentCircle: { + border: Colors.brand[10], + cross: Colors.brand[30], + }, + }, }; export type ThemeType = typeof theme; +export type ClusterColorKey = + keyof ThemeType['clusterColorPicker']['backgroundColor']; export const darkTheme: ThemeType = { ...baseTheme, @@ -864,8 +901,8 @@ export const darkTheme: ThemeType = { borderTop: Colors.neutral[80], }, dropdown: { - backgroundColor: Colors.neutral[85], - borderColor: Colors.neutral[80], + backgroundColor: Colors.brand[85], + borderColor: Colors.brand[70], shadow: Colors.transparency[20], item: { color: { @@ -1016,6 +1053,9 @@ export const darkTheme: ThemeType = { fontWeight: 400, }, }, + clusterMenu: { + backgroundColor: Colors.clusterMenuBackgroundColor, + }, schema: { backgroundColor: { tr: Colors.neutral[5], @@ -1383,4 +1423,12 @@ export const darkTheme: ThemeType = { }, }, }, + clusterColorPicker: { + backgroundColor: Colors.clusterColorPicker, + outline: Colors.brand[80], + transparentCircle: { + border: Colors.brand[60], + cross: Colors.brand[30], + }, + }, };