"react-helmet-async": "1.3.0",
"react-highlight-words": "0.20.0",
"react-intl": "6.2.5",
+ "react-modal": "3.16.1",
"react-router-dom": "6.10.0",
"react-select": "5.7.2",
"tailwindcss": "3.3.1"
export { ClipboardIconButton } from './clipboard';
export * from './icons';
export * from './layouts';
+export * from './modal/Modal';
export * from './popups';
export * from './subnavigation';
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import { Global, css, useTheme } from '@emotion/react';
+import classNames from 'classnames';
+import { ReactNode } from 'react';
+import ReactModal from 'react-modal';
+import tw from 'twin.macro';
+import { themeColor } from '../../helpers';
+import { REACT_DOM_CONTAINER } from '../../helpers/constants';
+import { translate } from '../../helpers/l10n';
+import { Theme } from '../../types/theme';
+import { ButtonSecondary } from '../buttons';
+import { ModalBody } from './ModalBody';
+import { ModalFooter } from './ModalFooter';
+import { ModalHeader } from './ModalHeader';
+
+ReactModal.setAppElement(REACT_DOM_CONTAINER);
+
+interface CommonProps {
+ closeOnOverlayClick?: boolean;
+ isLarge?: boolean;
+ isOpen?: boolean;
+ isScrollable?: boolean;
+ onClose: VoidFunction;
+}
+
+interface ChildrenProp {
+ children: React.ReactNode;
+}
+
+interface NotChildrenProp {
+ children?: never;
+}
+
+interface SectionsProps {
+ body: React.ReactNode;
+ headerDescription?: string | ReactNode;
+ headerTitle: string | ReactNode;
+ loading?: boolean;
+ primaryButton?: ReactNode;
+ secondaryButtonLabel: ReactNode;
+}
+
+type NotSectionsProps = {
+ [prop in keyof SectionsProps]?: never;
+};
+
+export type PropsWithChildren = CommonProps & ChildrenProp & NotSectionsProps;
+
+export type PropsWithSections = CommonProps & SectionsProps & NotChildrenProp;
+
+type Props = PropsWithChildren | PropsWithSections;
+
+function hasNoChildren(props: Partial<Props>): props is PropsWithSections {
+ return (props as PropsWithChildren).children === undefined;
+}
+
+export function Modal({
+ closeOnOverlayClick = true,
+ isLarge,
+ isOpen = true,
+ isScrollable = true,
+ onClose,
+ ...props
+}: Props) {
+ const theme = useTheme();
+
+ return (
+ <>
+ <Global styles={globalStyles({ theme })} />
+
+ <ReactModal
+ className={classNames('design-system-modal-contents', { large: isLarge })}
+ isOpen={isOpen}
+ onRequestClose={onClose}
+ overlayClassName="design-system-modal-overlay"
+ shouldCloseOnEsc={true}
+ shouldCloseOnOverlayClick={closeOnOverlayClick}
+ shouldFocusAfterRender={true}
+ shouldReturnFocusAfterClose={true}
+ >
+ {hasNoChildren(props) ? (
+ <>
+ <ModalHeader description={props.headerDescription} title={props.headerTitle} />
+
+ <ModalBody isScrollable={isScrollable}>{props.body}</ModalBody>
+
+ <ModalFooter
+ loading={props.loading}
+ primaryButton={props.primaryButton}
+ secondaryButton={
+ <ButtonSecondary
+ className="js-modal-close sw-capitalize"
+ disabled={props.loading}
+ onClick={onClose}
+ type="reset"
+ >
+ {props.secondaryButtonLabel ?? translate('close')}
+ </ButtonSecondary>
+ }
+ />
+ </>
+ ) : (
+ (props as PropsWithChildren).children
+ )}
+ </ReactModal>
+ </>
+ );
+}
+
+const globalStyles = ({ theme }: { theme: Theme }) => css`
+ .design-system-modal-contents {
+ ${tw`sw-container sw-flex sw-flex-col`}
+ ${tw`sw-p-9`}
+ ${tw`sw-rounded-2`}
+ ${tw`sw-z-modal`}
+
+ background-color: ${themeColor('modalContents')({ theme })};
+ max-height: calc(100vh - 30px);
+ min-height: 160px;
+ width: 544px;
+
+ &.large {
+ max-width: 1280px;
+ min-width: 1040px;
+ }
+ }
+
+ .design-system-modal-overlay {
+ ${tw`sw-fixed sw-inset-0`}
+ ${tw`sw-flex sw-items-center sw-justify-center`}
+ ${tw`sw-z-modal-overlay`}
+
+ background-color: ${themeColor('modalOverlay')({ theme })};
+ }
+`;
+
+Modal.Body = ModalBody;
+Modal.Footer = ModalFooter;
+Modal.Header = ModalHeader;
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import styled from '@emotion/styled';
+import classNames from 'classnames';
+import { ReactNode } from 'react';
+import tw from 'twin.macro';
+import { themeColor } from '../../helpers/theme';
+
+interface Props {
+ children: ReactNode;
+ isScrollable?: boolean;
+}
+
+export function ModalBody({ children, isScrollable = true }: Props) {
+ return <StyledMain className={classNames({ scrollable: isScrollable })}>{children}</StyledMain>;
+}
+
+const StyledMain = styled.main`
+ ${tw`sw-body-sm`}
+ ${tw`sw-pr-3`} // to accomodate a possible scrollbar
+ ${tw`sw-my-12`}
+ ${tw`sw-overflow-x-hidden`}
+
+ color: ${themeColor('pageContent')};
+
+ &.scrollable {
+ overflow-y: auto;
+ }
+`;
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import styled from '@emotion/styled';
+import tw from 'twin.macro';
+import { DeferredSpinner } from '../DeferredSpinner';
+
+interface Props {
+ loading?: boolean;
+ primaryButton?: React.ReactNode;
+ secondaryButton: React.ReactNode;
+}
+
+export function ModalFooter({ loading = false, primaryButton, secondaryButton }: Props) {
+ return (
+ <StyledFooter>
+ <DeferredSpinner loading={loading} />
+ {primaryButton}
+ {secondaryButton}
+ </StyledFooter>
+ );
+}
+
+const StyledFooter = styled.footer`
+ ${tw`sw-flex sw-gap-3 sw-items-center sw-justify-end`}
+`;
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import styled from '@emotion/styled';
+import { ReactNode } from 'react';
+import tw from 'twin.macro';
+import { themeColor } from '../../helpers/theme';
+
+interface Props {
+ description?: string | ReactNode;
+ title: string | ReactNode;
+}
+
+export function ModalHeader({ description, title }: Props) {
+ return (
+ <header>
+ <Title>{title}</Title>
+ {description && <Description>{description}</Description>}
+ </header>
+ );
+}
+
+const Description = styled.p`
+ ${tw`sw-body-sm`}
+ ${tw`sw-mt-2`}
+
+ color: ${themeColor('pageContent')};
+`;
+
+const Title = styled.p`
+ ${tw`sw-heading-lg`}
+
+ color: ${themeColor('pageTitle')};
+`;
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import { screen } from '@testing-library/react';
+import { render } from '../../../helpers/testUtils';
+import { Modal, PropsWithChildren, PropsWithSections } from '../Modal';
+
+it('should render default modal with predefined content', async () => {
+ setupPredefinedContent({
+ body: 'Modal body',
+ headerTitle: 'Hello',
+ headerDescription: 'Does this look OK?',
+ secondaryButtonLabel: undefined, // should use the default of 'Close'
+ });
+
+ expect(await screen.findByText('Modal body')).toBeVisible();
+ expect(await screen.findByText('Hello')).toBeVisible();
+ expect(await screen.findByText('Does this look OK?')).toBeVisible();
+ expect(await screen.findByRole('button', { name: 'close' })).toBeVisible();
+});
+
+it('should request close when pressing esc', async () => {
+ const onClose = jest.fn();
+ const { user } = setupPredefinedContent({ onClose });
+
+ await user.keyboard('{Escape}');
+
+ expect(onClose).toHaveBeenCalled();
+});
+
+it('should render modal with loose content', async () => {
+ setupLooseContent(undefined, <div>Hello</div>);
+
+ expect(await screen.findByText('Hello')).toBeVisible();
+});
+
+it('should request close when pressing esc on loose content', async () => {
+ const onClose = jest.fn();
+ const { user } = setupLooseContentWithMultipleChildren({ onClose });
+
+ await user.keyboard('{Escape}');
+
+ expect(onClose).toHaveBeenCalled();
+});
+
+function setupPredefinedContent(props: Partial<PropsWithSections> = {}) {
+ return render(
+ <Modal
+ body="Body"
+ headerTitle="Hello"
+ onClose={jest.fn()}
+ secondaryButtonLabel="Close"
+ {...props}
+ />
+ );
+}
+
+function setupLooseContent(props: Partial<PropsWithChildren> = {}, children = <div />) {
+ return render(
+ <Modal onClose={jest.fn()} {...props}>
+ {children}
+ </Modal>
+ );
+}
+
+function setupLooseContentWithMultipleChildren(props: Partial<PropsWithChildren> = {}) {
+ return render(
+ <Modal onClose={jest.fn()} {...props}>
+ <div>Hello there!</div>
+ <div>How are you?</div>
+ </Modal>
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import { render } from '../../../helpers/testUtils';
+import { ModalBody } from '../ModalBody';
+
+it('renders with children', () => {
+ const children = <div>Hello!</div>;
+ const { container } = setup(children);
+
+ expect(container).toMatchSnapshot();
+});
+
+function setup(children = <div />) {
+ return render(<ModalBody>{children}</ModalBody>);
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import { render } from '../../../helpers/testUtils';
+import { FCProps } from '../../../types/misc';
+import { ModalFooter } from '../ModalFooter';
+
+it('should render with secondary button', () => {
+ const { container } = setupWithProps({
+ secondaryButton: (
+ <button onClick={() => {}} type="button">
+ Close
+ </button>
+ ),
+ });
+
+ expect(container).toMatchSnapshot();
+});
+
+it('should render with primary and secondary buttons', () => {
+ const { container } = setupWithProps({
+ primaryButton: (
+ <button onClick={undefined} type="button">
+ Primary
+ </button>
+ ),
+ secondaryButton: (
+ <button onClick={undefined} type="reset">
+ Reset
+ </button>
+ ),
+ });
+
+ expect(container).toMatchSnapshot();
+});
+
+function setupWithProps(props: Partial<FCProps<typeof ModalFooter>> = {}) {
+ return render(<ModalFooter secondaryButton={<div />} {...props} />);
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import { render } from '../../../helpers/testUtils';
+import { FCProps } from '../../../types/misc';
+import { ModalHeader } from '../ModalHeader';
+
+it('should use the default title if not provided', () => {
+ const { container } = setupWithProps();
+
+ expect(container).toMatchSnapshot();
+});
+
+it('should render with title', () => {
+ const { container } = setupWithProps({ title: 'Foo' });
+
+ expect(container).toMatchSnapshot();
+});
+
+it('should render with title and description', () => {
+ const { container } = setupWithProps({ title: 'Foo', description: 'Bar' });
+
+ expect(container).toMatchSnapshot();
+});
+
+function setupWithProps(props: Partial<FCProps<typeof ModalHeader>> = {}) {
+ return render(<ModalHeader title="Modal title" {...props} />);
+}
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders with children 1`] = `
+.emotion-0 {
+ font-family: Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+ font-weight: 400;
+ padding-right: 0.75rem;
+ margin-top: 3rem;
+ margin-bottom: 3rem;
+ overflow-x: hidden;
+ color: rgb(62,67,87);
+}
+
+.emotion-0.scrollable {
+ overflow-y: auto;
+}
+
+<div>
+ <main
+ class="scrollable emotion-0 emotion-1"
+ >
+ <div>
+ Hello!
+ </div>
+ </main>
+</div>
+`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render with primary and secondary buttons 1`] = `
+.emotion-0 {
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ -webkit-box-pack: end;
+ -ms-flex-pack: end;
+ -webkit-justify-content: flex-end;
+ justify-content: flex-end;
+ gap: 0.75rem;
+}
+
+<div>
+ <footer
+ class="emotion-0 emotion-1"
+ >
+ <button
+ type="button"
+ >
+ Primary
+ </button>
+ <button
+ type="reset"
+ >
+ Reset
+ </button>
+ </footer>
+</div>
+`;
+
+exports[`should render with secondary button 1`] = `
+.emotion-0 {
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ -webkit-box-pack: end;
+ -ms-flex-pack: end;
+ -webkit-justify-content: flex-end;
+ justify-content: flex-end;
+ gap: 0.75rem;
+}
+
+<div>
+ <footer
+ class="emotion-0 emotion-1"
+ >
+ <button
+ type="button"
+ >
+ Close
+ </button>
+ </footer>
+</div>
+`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render with title 1`] = `
+.emotion-0 {
+ font-family: Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
+ font-size: 1.5rem;
+ line-height: 1.75rem;
+ font-weight: 600;
+ color: rgb(29,33,47);
+}
+
+<div>
+ <header>
+ <p
+ class="emotion-0 emotion-1"
+ >
+ Foo
+ </p>
+ </header>
+</div>
+`;
+
+exports[`should render with title and description 1`] = `
+.emotion-0 {
+ font-family: Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
+ font-size: 1.5rem;
+ line-height: 1.75rem;
+ font-weight: 600;
+ color: rgb(29,33,47);
+}
+
+.emotion-2 {
+ font-family: Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+ font-weight: 400;
+ margin-top: 0.5rem;
+ color: rgb(62,67,87);
+}
+
+<div>
+ <header>
+ <p
+ class="emotion-0 emotion-1"
+ >
+ Foo
+ </p>
+ <p
+ class="emotion-2 emotion-3"
+ >
+ Bar
+ </p>
+ </header>
+</div>
+`;
+
+exports[`should use the default title if not provided 1`] = `
+.emotion-0 {
+ font-family: Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
+ font-size: 1.5rem;
+ line-height: 1.75rem;
+ font-weight: 600;
+ color: rgb(29,33,47);
+}
+
+<div>
+ <header>
+ <p
+ class="emotion-0 emotion-1"
+ >
+ Modal title
+ </p>
+ </header>
+</div>
+`;
export const DEFAULT_LOCALE = 'en';
export const IS_SSR = typeof window === 'undefined';
-export const REACT_DOM_CONTAINER = '#___gatsby';
+export const REACT_DOM_CONTAINER = '#content';
export const RULE_STATUSES = ['READY', 'BETA', 'DEPRECATED'];
export const DARK_THEME_ID = 'dark-theme';
export const OPACITY_20_PERCENT = 0.2;
+
+export const OPACITY_75_PERCENT = 0.75;
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { OPACITY_20_PERCENT } from '../helpers/constants';
+import { OPACITY_20_PERCENT, OPACITY_75_PERCENT } from '../helpers/constants';
import COLORS from './colors';
const primary = {
popup: COLORS.white,
popupBorder: secondary.default,
+ // modal
+ modalContents: COLORS.white,
+ modalOverlay: [...COLORS.blueGrey[900], OPACITY_75_PERCENT],
+
// dropdown menu
dropdownMenu: COLORS.white,
dropdownMenuHover: secondary.light,
'global-popup': '5000',
'dropdown-menu': '7500',
tooltip: '8000',
+ 'modal-overlay': 8500,
+ modal: '9000',
},
extend: {
width: {
react-helmet-async: 1.3.0
react-highlight-words: 0.20.0
react-intl: 6.2.5
+ react-modal: 3.16.1
react-router-dom: 6.10.0
react-select: 5.7.2
tailwindcss: 3.3.1