Closes #2413 - Building a chrome extension for twenty to store person/company data into a workspace. (#3430)

* build: create a new vite project for chrome extension

* feat: configure theme per the frontend codebase for chrome extension

* feat: inject the add to twenty button into linkedin profile page

* feat: create the api key form ui and render it on the options page

* feat: inject the add to twenty button into linkedin company page

* feat: scrape required data from both the user profile and the company profile

* refactor: move modules into options because it is the only page using react for now

* fix: show add to twenty button without having to reload the single page application

* fix: extract domain of the business website instead of scrapping the industry type

* feat: store api key to local storage and open options page when trying to store data without setting a key

* feat: send data to the backend upon click and store it to the database

* fix: open options page upon clicking the extension icon

* fix: update terminology from user to person to match the codebase convention

* fix: adopt chrome extension to monorepo approach using nx and get the development server working

* fix: update vite config for build command to work per the requirement

* feat: add instructions in the readme file to install the extension for local testing

* fix: move server base url to a dotenv file and replace the hard-coded url

* feat: permit user to configure a custom route for the server from the options page

* fix: fetch api key and route from local storage and display on options page to inform users of their choices

* fix: move front base url to dotenv and replace the hard-coded url

* fix: remove the trailing slash from person and company linkedin username

* fix: improve code commenting to explain implementation somewhat better

* ci: introduce a workflow to build chrome extension to ensure it can be published

* fix: format files to display code in a consistent manner per the prettier configuration in codebase

* fix: improve the commenting significantly to explain important and hard-to-understand parts of the code

* fix: remove unused permissions from the manifest file for publishing to the chrome web store

* Add nx

* Fix vale

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Abdullah
2024-02-12 16:30:23 +05:00
committed by GitHub
parent a15128df36
commit 1265dc74d0
59 changed files with 2103 additions and 10 deletions

View File

@ -0,0 +1,44 @@
import styled from '@emotion/styled';
type H2TitleProps = {
title: string;
description?: string;
addornment?: React.ReactNode;
};
const StyledContainer = styled.div`
display: flex;
flex-direction: column;
margin-bottom: ${({ theme }) => theme.spacing(4)};
`;
const StyledTitleContainer = styled.div`
align-items: center;
display: flex;
justify-content: space-between;
`;
const StyledTitle = styled.h2`
color: ${({ theme }) => theme.font.color.primary};
font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
margin: 0;
`;
const StyledDescription = styled.h3`
color: ${({ theme }) => theme.font.color.tertiary};
font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.regular};
margin: 0;
margin-top: ${({ theme }) => theme.spacing(3)};
`;
export const H2Title = ({ title, description, addornment }: H2TitleProps) => (
<StyledContainer>
<StyledTitleContainer>
<StyledTitle>{title}</StyledTitle>
{addornment}
</StyledTitleContainer>
{description && <StyledDescription>{description}</StyledDescription>}
</StyledContainer>
);

View File

@ -0,0 +1,85 @@
import React from 'react';
import styled from '@emotion/styled';
export type ButtonSize = 'medium' | 'small';
export type ButtonPosition = 'standalone' | 'left' | 'middle' | 'right';
export type ButtonVariant = 'primary' | 'secondary' | 'tertiary';
export type ButtonAccent = 'default' | 'blue' | 'danger';
export type ButtonProps = {
className?: string;
Icon?: React.ReactNode;
title?: string;
fullWidth?: boolean;
variant?: ButtonVariant;
size?: ButtonSize;
position?: ButtonPosition;
accent?: ButtonAccent;
soon?: boolean;
disabled?: boolean;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
};
const StyledButton = styled.button<ButtonProps>`
border: 1px solid transparent;
border-radius: ${({ position, theme }) => {
switch (position) {
case 'left':
return `${theme.border.radius.sm} 0px 0px ${theme.border.radius.sm}`;
case 'right':
return `0px ${theme.border.radius.sm} ${theme.border.radius.sm} 0px`;
case 'middle':
return '0px';
case 'standalone':
return theme.border.radius.sm;
default:
return theme.border.radius.sm;
}
}};
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
display: inline-flex;
align-items: center;
font-family: ${({ theme }) => theme.font.family};
font-weight: 500;
gap: ${({ theme }) => theme.spacing(1)};
height: ${({ size }) => (size === 'small' ? '24px' : '32px')};
padding: 0 ${({ theme }) => theme.spacing(2)};
white-space: nowrap;
width: ${({ fullWidth }) => (fullWidth ? '100%' : 'auto')};
&:hover {
border-color: transparent;
filter: brightness(0.9);
}
&:focus {
outline: none;
}
`;
export const Button = ({
className,
Icon,
title,
fullWidth = false,
variant = 'primary',
size = 'medium',
position = 'standalone',
soon = false,
disabled = false,
onClick,
}: ButtonProps) => (
<StyledButton
fullWidth={fullWidth}
variant={variant}
size={size}
position={position}
disabled={soon || disabled}
className={className}
onClick={onClick}
>
{Icon && Icon}
{title}
{soon && 'Soon'}
</StyledButton>
);

View File

@ -0,0 +1,85 @@
import React from 'react';
import styled from '@emotion/styled';
interface TextInputProps {
label?: string;
value: string;
onChange: (value: string) => void;
fullWidth?: boolean;
error?: string;
placeholder?: string;
icon?: React.ReactNode;
}
const StyledContainer = styled.div<{ fullWidth?: boolean }>`
display: flex;
flex-direction: column;
width: ${({ fullWidth }) => (fullWidth ? `100%` : 'auto')};
margin-bottom: ${({ theme }) => theme.spacing(4)};
`;
const StyledLabel = styled.span`
color: ${({ theme }) => theme.font.color.light};
font-size: ${({ theme }) => theme.font.size.xs};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
margin-bottom: ${({ theme }) => theme.spacing(1)};
text-transform: uppercase;
`;
const StyledInputContainer = styled.div`
display: flex;
align-items: center;
border: 1px solid #ccc;
border-radius: 4px;
padding: 8px;
`;
const StyledIcon = styled.span`
margin-right: 8px;
`;
const StyledInput = styled.input`
flex: 1;
border: none;
outline: none;
font-family: Arial, sans-serif;
font-size: 14px;
&::placeholder {
color: #aaa;
}
`;
const StyledErrorHelper = styled.div`
color: #ff0000;
font-size: 12px;
padding: 5px 0;
`;
const TextInput: React.FC<TextInputProps> = ({
label,
value,
onChange,
fullWidth,
error,
placeholder,
icon,
}) => {
return (
<StyledContainer fullWidth={fullWidth}>
{label && <StyledLabel>{label}</StyledLabel>}
<StyledInputContainer>
{icon && <StyledIcon>{icon}</StyledIcon>}
<StyledInput
type="text"
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
/>
</StyledInputContainer>
{error && <StyledErrorHelper>{error}</StyledErrorHelper>}
</StyledContainer>
);
};
export { TextInput };

View File

@ -0,0 +1,83 @@
import { useEffect, useState } from 'react';
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
export type ToggleSize = 'small' | 'medium';
type ContainerProps = {
isOn: boolean;
color?: string;
toggleSize: ToggleSize;
};
const StyledContainer = styled.div<ContainerProps>`
align-items: center;
background-color: ${({ theme, isOn, color }) =>
isOn ? color ?? theme.color.blue : theme.background.quaternary};
border-radius: 10px;
cursor: pointer;
display: flex;
height: ${({ toggleSize }) => (toggleSize === 'small' ? 16 : 20)}px;
transition: background-color 0.3s ease;
width: ${({ toggleSize }) => (toggleSize === 'small' ? 24 : 32)}px;
`;
const StyledCircle = styled(motion.div)<{
toggleSize: ToggleSize;
}>`
background-color: ${({ theme }) => theme.background.primary};
border-radius: 50%;
height: ${({ toggleSize }) => (toggleSize === 'small' ? 12 : 16)}px;
width: ${({ toggleSize }) => (toggleSize === 'small' ? 12 : 16)}px;
`;
export type ToggleProps = {
value?: boolean;
onChange?: (value: boolean) => void;
color?: string;
toggleSize?: ToggleSize;
};
export const Toggle = ({
value,
onChange,
color,
toggleSize = 'medium',
}: ToggleProps) => {
const [isOn, setIsOn] = useState(value ?? false);
const circleVariants = {
on: { x: toggleSize === 'small' ? 10 : 14 },
off: { x: 2 },
};
const handleChange = () => {
setIsOn(!isOn);
if (onChange) {
onChange(!isOn);
}
};
useEffect(() => {
if (value !== isOn) {
setIsOn(value ?? false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value]);
return (
<StyledContainer
onClick={handleChange}
isOn={isOn}
color={color}
toggleSize={toggleSize}
>
<StyledCircle
animate={isOn ? 'on' : 'off'}
variants={circleVariants}
toggleSize={toggleSize}
/>
</StyledContainer>
);
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -0,0 +1,15 @@
import { ThemeProvider } from '@emotion/react';
import { lightTheme } from '../constants/theme';
type AppThemeProviderProps = {
children: JSX.Element;
};
const AppThemeProvider: React.FC<AppThemeProviderProps> = ({ children }) => {
const theme = lightTheme;
return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
};
export { AppThemeProvider };

View File

@ -0,0 +1,19 @@
import { color } from './colors';
export const accentLight = {
primary: color.blueAccent25,
secondary: color.blueAccent20,
tertiary: color.blueAccent15,
quaternary: color.blueAccent10,
accent3570: color.blueAccent35,
accent4060: color.blueAccent40,
};
export const accentDark = {
primary: color.blueAccent75,
secondary: color.blueAccent80,
tertiary: color.blueAccent85,
quaternary: color.blueAccent90,
accent3570: color.blueAccent70,
accent4060: color.blueAccent60,
};

View File

@ -0,0 +1,9 @@
export const animation = {
duration: {
instant: 0.075,
fast: 0.15,
normal: 0.3,
},
};
export type AnimationDuration = 'instant' | 'fast' | 'normal';

View File

@ -0,0 +1,47 @@
/* eslint-disable twenty/no-hardcoded-colors */
import DarkNoise from '../assets/dark-noise.jpg';
import LightNoise from '../assets/light-noise.png';
import { color, grayScale, rgba } from './colors';
export const backgroundLight = {
noisy: `url(${LightNoise.toString()});`,
primary: grayScale.gray0,
secondary: grayScale.gray10,
tertiary: grayScale.gray15,
quaternary: grayScale.gray20,
danger: color.red10,
transparent: {
primary: rgba(grayScale.gray0, 0.8),
secondary: rgba(grayScale.gray10, 0.8),
strong: rgba(grayScale.gray100, 0.16),
medium: rgba(grayScale.gray100, 0.08),
light: rgba(grayScale.gray100, 0.04),
lighter: rgba(grayScale.gray100, 0.02),
danger: rgba(color.red, 0.08),
},
overlay: rgba(grayScale.gray80, 0.8),
radialGradient: `radial-gradient(50% 62.62% at 50% 0%, #505050 0%, ${grayScale.gray60} 100%)`,
radialGradientHover: `radial-gradient(76.32% 95.59% at 50% 0%, #505050 0%, ${grayScale.gray60} 100%)`,
};
export const backgroundDark = {
noisy: `url(${DarkNoise.toString()});`,
primary: grayScale.gray85,
secondary: grayScale.gray80,
tertiary: grayScale.gray75,
quaternary: grayScale.gray70,
danger: color.red80,
transparent: {
primary: rgba(grayScale.gray85, 0.8),
secondary: rgba(grayScale.gray80, 0.8),
strong: rgba(grayScale.gray0, 0.14),
medium: rgba(grayScale.gray0, 0.1),
light: rgba(grayScale.gray0, 0.06),
lighter: rgba(grayScale.gray0, 0.03),
danger: rgba(color.red, 0.08),
},
overlay: rgba(grayScale.gray80, 0.8),
radialGradient: `radial-gradient(50% 62.62% at 50% 0%, #505050 0%, ${grayScale.gray60} 100%)`,
radialGradientHover: `radial-gradient(76.32% 95.59% at 50% 0%, #505050 0%, ${grayScale.gray60} 100%)`,
};

View File

@ -0,0 +1,4 @@
export const blur = {
light: 'blur(6px)',
strong: 'blur(20px)',
};

View File

@ -0,0 +1,34 @@
import { color, grayScale } from './colors';
const common = {
radius: {
xs: '2px',
sm: '4px',
md: '8px',
rounded: '100%',
},
};
export const borderLight = {
color: {
strong: grayScale.gray25,
medium: grayScale.gray20,
light: grayScale.gray15,
secondaryInverted: grayScale.gray50,
inverted: grayScale.gray60,
danger: color.red20,
},
...common,
};
export const borderDark = {
color: {
strong: grayScale.gray55,
medium: grayScale.gray65,
light: grayScale.gray70,
secondaryInverted: grayScale.gray35,
inverted: grayScale.gray20,
danger: color.red70,
},
...common,
};

View File

@ -0,0 +1,27 @@
import { grayScale, rgba } from './colors';
export const boxShadowLight = {
extraLight: `0px 1px 0px 0px ${rgba(grayScale.gray100, 0.04)}`,
light: `0px 2px 4px 0px ${rgba(
grayScale.gray100,
0.04,
)}, 0px 0px 4px 0px ${rgba(grayScale.gray100, 0.08)}`,
strong: `2px 4px 16px 0px ${rgba(
grayScale.gray100,
0.12,
)}, 0px 2px 4px 0px ${rgba(grayScale.gray100, 0.04)}`,
underline: `0px 1px 0px 0px ${rgba(grayScale.gray100, 0.32)}`,
};
export const boxShadowDark = {
extraLight: `0px 1px 0px 0px ${rgba(grayScale.gray100, 0.04)}`,
light: `0px 2px 4px 0px ${rgba(
grayScale.gray100,
0.04,
)}, 0px 0px 4px 0px ${rgba(grayScale.gray100, 0.08)}`,
strong: `2px 4px 16px 0px ${rgba(
grayScale.gray100,
0.16,
)}, 0px 2px 4px 0px ${rgba(grayScale.gray100, 0.08)}`,
underline: `0px 1px 0px 0px ${rgba(grayScale.gray100, 0.32)}`,
};

View File

@ -0,0 +1,153 @@
/* eslint-disable twenty/no-hardcoded-colors */
import hexRgb from 'hex-rgb';
export const grayScale = {
gray100: '#000000',
gray90: '#141414',
gray85: '#171717',
gray80: '#1b1b1b',
gray75: '#1d1d1d',
gray70: '#222222',
gray65: '#292929',
gray60: '#333333',
gray55: '#4c4c4c',
gray50: '#666666',
gray45: '#818181',
gray40: '#999999',
gray35: '#b3b3b3',
gray30: '#cccccc',
gray25: '#d6d6d6',
gray20: '#ebebeb',
gray15: '#f1f1f1',
gray10: '#fcfcfc',
gray0: '#ffffff',
};
export const mainColors = {
yellow: '#ffd338',
green: '#55ef3c',
turquoise: '#15de8f',
sky: '#00e0ff',
blue: '#1961ed',
purple: '#915ffd',
pink: '#f54bd0',
red: '#f83e3e',
orange: '#ff7222',
gray: grayScale.gray30,
};
export type ThemeColor = keyof typeof mainColors;
export const secondaryColors = {
yellow80: '#2e2a1a',
yellow70: '#453d1e',
yellow60: '#746224',
yellow50: '#b99b2e',
yellow40: '#ffe074',
yellow30: '#ffedaf',
yellow20: '#fff6d7',
yellow10: '#fffbeb',
green80: '#1d2d1b',
green70: '#23421e',
green60: '#2a5822',
green50: '#42ae31',
green40: '#88f477',
green30: '#ccfac5',
green20: '#ddfcd8',
green10: '#eefdec',
turquoise80: '#172b23',
turquoise70: '#173f2f',
turquoise60: '#166747',
turquoise50: '#16a26b',
turquoise40: '#5be8b1',
turquoise30: '#a1f2d2',
turquoise20: '#d0f8e9',
turquoise10: '#e8fcf4',
sky80: '#152b2e',
sky70: '#123f45',
sky60: '#0e6874',
sky50: '#07a4b9',
sky40: '#4de9ff',
sky30: '#99f3ff',
sky20: '#ccf9ff',
sky10: '#e5fcff',
blue80: '#171e2c',
blue70: '#172642',
blue60: '#18356d',
blue50: '#184bad',
blue40: '#5e90f2',
blue30: '#a3c0f8',
blue20: '#d1dffb',
blue10: '#e8effd',
purple80: '#231e2e',
purple70: '#2f2545',
purple60: '#483473',
purple50: '#6c49b8',
purple40: '#b28ffe',
purple30: '#d3bffe',
purple20: '#e9dfff',
purple10: '#f4efff',
pink80: '#2d1c29',
pink70: '#43213c',
pink60: '#702c61',
pink50: '#b23b98',
pink40: '#f881de',
pink30: '#fbb7ec',
pink20: '#fddbf6',
pink10: '#feedfa',
red80: '#2d1b1b',
red70: '#441f1f',
red60: '#712727',
red50: '#b43232',
red40: '#fa7878',
red30: '#fcb2b2',
red20: '#fed8d8',
red10: '#feecec',
orange80: '#2e2018',
orange70: '#452919',
orange60: '#743b1b',
orange50: '#b9571f',
orange40: '#ff9c64',
orange30: '#ffc7a7',
orange20: '#ffe3d3',
orange10: '#fff1e9',
gray80: grayScale.gray70,
gray70: grayScale.gray65,
gray60: grayScale.gray55,
gray50: grayScale.gray40,
gray40: grayScale.gray25,
gray30: grayScale.gray20,
gray20: grayScale.gray15,
gray10: grayScale.gray10,
blueAccent90: '#141a25',
blueAccent85: '#151d2e',
blueAccent80: '#152037',
blueAccent75: '#16233f',
blueAccent70: '#17294a',
blueAccent60: '#18356d',
blueAccent40: '#a3c0f8',
blueAccent35: '#c8d9fb',
blueAccent25: '#dae6fc',
blueAccent20: '#e2ecfd',
blueAccent15: '#edf2fe',
blueAccent10: '#f5f9fd',
};
export const color = {
...mainColors,
...secondaryColors,
};
export const rgba = (hex: string, alpha: number) => {
const rgb = hexRgb(hex, { format: 'array' }).slice(0, -1).join(',');
return `rgba(${rgb},${alpha})`;
};

View File

@ -0,0 +1,37 @@
import { css } from '@emotion/react';
import { ThemeType } from './theme';
export const overlayBackground = (props: { theme: ThemeType }) =>
css`
backdrop-filter: blur(8px);
background: ${props.theme.background.transparent.secondary};
box-shadow: ${props.theme.boxShadow.strong};
`;
export const textInputStyle = (props: { theme: ThemeType }) =>
css`
background-color: transparent;
border: none;
color: ${props.theme.font.color.primary};
font-family: ${props.theme.font.family};
font-size: inherit;
font-weight: inherit;
outline: none;
padding: ${props.theme.spacing(0)} ${props.theme.spacing(2)};
&::placeholder,
&::-webkit-input-placeholder {
color: ${props.theme.font.color.light};
font-family: ${props.theme.font.family};
font-weight: ${props.theme.font.weight.medium};
}
`;
export const hoverBackground = (props: any) =>
css`
transition: background 0.1s ease;
&:hover {
background: ${props.theme.background.transparent.light};
}
`;

View File

@ -0,0 +1,45 @@
import { color, grayScale } from './colors';
const common = {
size: {
xxs: '0.625rem',
xs: '0.85rem',
sm: '0.92rem',
md: '1rem',
lg: '1.23rem',
xl: '1.54rem',
xxl: '1.85rem',
},
weight: {
regular: 400,
medium: 500,
semiBold: 600,
},
family: 'Inter, sans-serif',
};
export const fontLight = {
color: {
primary: grayScale.gray60,
secondary: grayScale.gray50,
tertiary: grayScale.gray40,
light: grayScale.gray35,
extraLight: grayScale.gray30,
inverted: grayScale.gray0,
danger: color.red,
},
...common,
};
export const fontDark = {
color: {
primary: grayScale.gray20,
secondary: grayScale.gray35,
tertiary: grayScale.gray45,
light: grayScale.gray50,
extraLight: grayScale.gray55,
inverted: grayScale.gray100,
danger: color.red,
},
...common,
};

View File

@ -0,0 +1,13 @@
export const icon = {
size: {
sm: 14,
md: 16,
lg: 20,
xl: 40,
},
stroke: {
sm: 1.6,
md: 2,
lg: 2.5,
},
};

View File

@ -0,0 +1,7 @@
export const modal = {
size: {
sm: '300px',
md: '400px',
lg: '53%',
},
};

View File

@ -0,0 +1,55 @@
import { color } from './colors';
export const tagLight: { [key: string]: { [key: string]: string } } = {
text: {
green: color.green60,
turquoise: color.turquoise60,
sky: color.sky60,
blue: color.blue60,
purple: color.purple60,
pink: color.pink60,
red: color.red60,
orange: color.orange60,
yellow: color.yellow60,
gray: color.gray60,
},
background: {
green: color.green20,
turquoise: color.turquoise20,
sky: color.sky20,
blue: color.blue20,
purple: color.purple20,
pink: color.pink20,
red: color.red20,
orange: color.orange20,
yellow: color.yellow20,
gray: color.gray20,
},
};
export const tagDark = {
text: {
green: color.green10,
turquoise: color.turquoise10,
sky: color.sky10,
blue: color.blue10,
purple: color.purple10,
pink: color.pink10,
red: color.red10,
orange: color.orange10,
yellow: color.yellow10,
gray: color.gray10,
},
background: {
green: color.green60,
turquoise: color.turquoise60,
sky: color.sky60,
blue: color.blue60,
purple: color.purple60,
pink: color.pink60,
red: color.red60,
orange: color.orange60,
yellow: color.yellow60,
gray: color.gray60,
},
};

View File

@ -0,0 +1,13 @@
export const text = {
lineHeight: {
lg: 1.5,
md: 1.2,
},
iconSizeMedium: 16,
iconSizeSmall: 14,
iconStrikeLight: 1.6,
iconStrikeMedium: 2,
iconStrikeBold: 2.5,
};

View File

@ -0,0 +1,76 @@
/* eslint-disable twenty/no-hardcoded-colors */
import { accentDark, accentLight } from './accent';
import { animation } from './animation';
import { backgroundDark, backgroundLight } from './background';
import { blur } from './blur';
import { borderDark, borderLight } from './border';
import { boxShadowDark, boxShadowLight } from './boxShadow';
import { color, grayScale } from './colors';
import { fontDark, fontLight } from './font';
import { icon } from './icon';
import { modal } from './modal';
import { tagDark, tagLight } from './tag';
import { text } from './text';
const common = {
color: color,
grayScale: grayScale,
icon: icon,
modal: modal,
text: text,
blur: blur,
animation: animation,
snackBar: {
success: {
background: '#16A26B',
color: '#D0F8E9',
},
error: {
background: '#B43232',
color: '#FED8D8',
},
info: {
background: color.gray80,
color: grayScale.gray0,
},
},
spacingMultiplicator: 4,
spacing: (multiplicator: number) => `${multiplicator * 4}px`,
betweenSiblingsGap: `2px`,
table: {
horizontalCellMargin: '8px',
checkboxColumnWidth: '32px',
},
rightDrawerWidth: '500px',
clickableElementBackgroundTransition: 'background 0.1s ease',
lastLayerZIndex: 2147483647,
};
export const lightTheme = {
...common,
...{
accent: accentLight,
background: backgroundLight,
border: borderLight,
tag: tagLight,
boxShadow: boxShadowLight,
font: fontLight,
name: 'light',
},
};
export type ThemeType = typeof lightTheme;
export const darkTheme: ThemeType = {
...common,
...{
accent: accentDark,
background: backgroundDark,
border: borderDark,
tag: tagDark,
boxShadow: boxShadowDark,
font: fontDark,
name: 'dark',
},
};
export const MOBILE_VIEWPORT = 768;