From b33a7cd2193a47f90b22568dd0d58f404bc5f6d7 Mon Sep 17 00:00:00 2001 From: Jeremy Davis Date: Wed, 22 Feb 2023 16:18:48 +0100 Subject: [PATCH] SONAR-18524 New Main App bar --- .cirrus.yml | 2 + server/sonar-web/build.gradle | 2 +- server/sonar-web/config/jest/SetupTheme.js | 26 + .../sonar-web/design-system/babel.config.js | 3 +- server/sonar-web/design-system/build.gradle | 40 + .../config/jest/SetupReactTestingLibrary.ts | 25 + .../config/jest/SetupTestEnvironment.js | 50 + .../design-system/config/jest/SetupTheme.js | 26 + server/sonar-web/design-system/jest.config.js | 61 + server/sonar-web/design-system/package.json | 49 +- .../design-system/src/@types/css.d.ts | 27 + .../design-system/src/@types/emotion.d.ts | 25 + .../design-system/src/components/Avatar.tsx | 117 + .../design-system/src/components/Checkbox.tsx | 174 ++ .../src/components/ClickEventBoundary.tsx | 35 + .../src/components/DeferredSpinner.tsx | 138 ++ .../design-system/src/components/Dropdown.tsx | 140 ++ .../src/components/DropdownMenu.tsx | 370 +++ .../src/components/DropdownToggler.tsx | 48 + .../src/components/EscKeydownHandler.tsx | 48 + .../src/components/GenericAvatar.tsx | 60 + .../src/components/InputSearch.tsx | 243 ++ .../src/components/InteractiveIcon.tsx | 182 ++ .../design-system/src/components/Link.tsx | 173 ++ .../src/components/MainAppBar.tsx | 89 + .../design-system/src/components/MainMenu.tsx | 30 + .../src/components/MainMenuItem.tsx | 59 + .../design-system/src/components/NavLink.tsx | 74 + .../src/components/OutsideClickHandler.tsx | 68 + .../src/components/RadioButton.tsx | 125 ++ .../src/components/SonarQubeLogo.tsx | 50 + .../design-system/src/components/Text.tsx | 62 + .../design-system/src/components/Tooltip.tsx | 504 +++++ .../src/components/__tests__/Avatar-test.tsx | 69 + .../__tests__/DeferredSpinner-test.tsx | 73 + .../components/__tests__/Dropdown-test.tsx | 65 + .../__tests__/DropdownMenu-test.tsx | 100 + .../__tests__/GenericAvatar-test.tsx | 51 + .../components/__tests__/InputSearch-test.tsx | 90 + .../src/components/__tests__/Link-test.tsx | 129 ++ .../components/__tests__/MainAppBar-test.tsx | 54 + .../__tests__/MainMenuItem-test.tsx | 64 + .../src/components/__tests__/NavLink-test.tsx | 112 + .../src/components/__tests__/Text-test.tsx | 41 + .../src/components/__tests__/Tooltip-test.tsx | 126 ++ .../components/__tests__/clipboard-test.tsx | 69 + .../design-system/src/components/buttons.tsx | 219 ++ .../src/components/clipboard.tsx | 170 ++ .../src/components/icons/CheckIcon.tsx | 36 + .../src/components/icons/ClockIcon.tsx | 23 + .../src/components/icons/CloseIcon.tsx | 23 + .../src/components/icons/CopyIcon.tsx | 23 + .../src/components/icons/Icon.tsx | 86 + .../src/components/icons/MenuHelpIcon.tsx | 36 + .../src/components/icons/MenuIcon.tsx | 29 + .../src/components/icons/MenuSearchIcon.tsx | 37 + .../src/components/icons/OpenNewTabIcon.tsx | 23 + .../src/components/icons/SearchIcon.tsx | 23 + .../src/components/icons/StarIcon.tsx | 23 + .../components/icons/__tests__/Icon-test.tsx | 54 + .../src/components/icons/index.ts | 24 + .../design-system/src/components/index.ts | 19 +- .../design-system/src/components/popups.tsx | 256 +++ .../src/helpers/__tests__/colors-test.ts | 61 + .../src/helpers/__tests__/positioning-test.ts | 167 ++ .../src/helpers/__tests__/theme-test.ts | 148 ++ .../design-system/src/helpers/colors.ts | 56 + .../design-system/src/helpers/constants.ts | 68 + .../DummyComponent.tsx => helpers/index.ts} | 6 +- .../design-system/src/helpers/keyboard.ts | 52 + .../design-system/src/helpers/l10n.ts | 30 + .../design-system/src/helpers/positioning.ts | 185 ++ .../design-system/src/helpers/testUtils.tsx | 117 + .../design-system/src/helpers/theme.ts | 130 ++ .../design-system/src/helpers/types.ts | 22 + server/sonar-web/design-system/src/index.ts | 23 + .../design-system/src/theme/colors.ts | 136 ++ .../design-system/src/theme/index.ts | 20 + .../design-system/src/theme/light.ts | 743 ++++++ .../sonar-web/design-system/src/types/misc.ts | 21 + .../design-system/src/types/theme.ts | 45 + server/sonar-web/design-system/tsconfig.json | 8 +- server/sonar-web/design-system/vite.config.js | 4 +- server/sonar-web/jest.config.js | 8 +- server/sonar-web/package.json | 3 +- .../js/app/components/GlobalContainer.tsx | 60 +- .../js/app/components/SimpleContainer.tsx | 5 +- .../GlobalSearch.tsx} | 169 +- .../global-search/GlobalSearchResult.tsx | 65 + .../GlobalSearchResults.tsx} | 33 +- .../GlobalSearchShowMore.tsx} | 45 +- .../__tests__/GlobalSearch-it.tsx | 214 ++ .../{search => global-search}/utils.ts | 0 .../app/components/nav/global/GlobalNav.css | 125 -- .../app/components/nav/global/GlobalNav.tsx | 30 +- .../components/nav/global/GlobalNavMenu.tsx | 79 +- .../components/nav/global/GlobalNavMore.tsx | 69 + .../components/nav/global/GlobalNavUser.tsx | 132 +- .../nav/global/GlobalNavUserMenu.tsx | 66 + .../nav/global/MainSonarQubeBar.tsx | 42 + .../nav/global/__tests__/GlobalNav-test.tsx | 54 +- .../__tests__/GlobalNavBranding-test.tsx | 50 - .../global/__tests__/GlobalNavMenu-test.tsx | 12 +- .../global/__tests__/GlobalNavUser-test.tsx | 20 +- .../__snapshots__/GlobalNav-test.tsx.snap | 95 - .../GlobalNavBranding-test.tsx.snap | 46 - .../main/js/app/components/search/Search.css | 126 -- .../js/app/components/search/SearchResult.tsx | 80 - .../search/__tests__/Search-test.tsx | 175 -- .../search/__tests__/SearchResult-test.tsx | 76 - .../search/__tests__/SearchResults-test.tsx | 83 - .../search/__tests__/SearchShowMore-test.tsx | 61 - .../__snapshots__/Search-test.tsx.snap | 28 - .../__snapshots__/SearchResult-test.tsx.snap | 242 -- .../__snapshots__/SearchResults-test.tsx.snap | 86 - .../SearchShowMore-test.tsx.snap | 41 - .../src/main/js/app/utils/startReactApp.tsx | 106 +- .../embed-docs-modal/DocItemLink.tsx | 44 + .../embed-docs-modal/EmbedDocsPopup.tsx | 204 +- .../embed-docs-modal/EmbedDocsPopupHelper.tsx | 90 +- .../__tests__/EmbedDocsPopup-test.tsx | 79 +- .../__tests__/SuggestionsProvider-test.tsx | 53 - server/sonar-web/tailwind-utilities.js | 87 + server/sonar-web/tailwind.base.config.js | 195 ++ server/sonar-web/tailwind.config.js | 6 +- server/sonar-web/yarn.lock | 1994 +++++++++++++++-- settings.gradle | 1 + .../resources/org/sonar/l10n/core.properties | 8 + 128 files changed, 10388 insertions(+), 2213 deletions(-) create mode 100644 server/sonar-web/config/jest/SetupTheme.js create mode 100644 server/sonar-web/design-system/build.gradle create mode 100644 server/sonar-web/design-system/config/jest/SetupReactTestingLibrary.ts create mode 100644 server/sonar-web/design-system/config/jest/SetupTestEnvironment.js create mode 100644 server/sonar-web/design-system/config/jest/SetupTheme.js create mode 100644 server/sonar-web/design-system/jest.config.js create mode 100644 server/sonar-web/design-system/src/@types/css.d.ts create mode 100644 server/sonar-web/design-system/src/@types/emotion.d.ts create mode 100644 server/sonar-web/design-system/src/components/Avatar.tsx create mode 100644 server/sonar-web/design-system/src/components/Checkbox.tsx create mode 100644 server/sonar-web/design-system/src/components/ClickEventBoundary.tsx create mode 100644 server/sonar-web/design-system/src/components/DeferredSpinner.tsx create mode 100644 server/sonar-web/design-system/src/components/Dropdown.tsx create mode 100644 server/sonar-web/design-system/src/components/DropdownMenu.tsx create mode 100644 server/sonar-web/design-system/src/components/DropdownToggler.tsx create mode 100644 server/sonar-web/design-system/src/components/EscKeydownHandler.tsx create mode 100644 server/sonar-web/design-system/src/components/GenericAvatar.tsx create mode 100644 server/sonar-web/design-system/src/components/InputSearch.tsx create mode 100644 server/sonar-web/design-system/src/components/InteractiveIcon.tsx create mode 100644 server/sonar-web/design-system/src/components/Link.tsx create mode 100644 server/sonar-web/design-system/src/components/MainAppBar.tsx create mode 100644 server/sonar-web/design-system/src/components/MainMenu.tsx create mode 100644 server/sonar-web/design-system/src/components/MainMenuItem.tsx create mode 100644 server/sonar-web/design-system/src/components/NavLink.tsx create mode 100644 server/sonar-web/design-system/src/components/OutsideClickHandler.tsx create mode 100644 server/sonar-web/design-system/src/components/RadioButton.tsx create mode 100644 server/sonar-web/design-system/src/components/SonarQubeLogo.tsx create mode 100644 server/sonar-web/design-system/src/components/Text.tsx create mode 100644 server/sonar-web/design-system/src/components/Tooltip.tsx create mode 100644 server/sonar-web/design-system/src/components/__tests__/Avatar-test.tsx create mode 100644 server/sonar-web/design-system/src/components/__tests__/DeferredSpinner-test.tsx create mode 100644 server/sonar-web/design-system/src/components/__tests__/Dropdown-test.tsx create mode 100644 server/sonar-web/design-system/src/components/__tests__/DropdownMenu-test.tsx create mode 100644 server/sonar-web/design-system/src/components/__tests__/GenericAvatar-test.tsx create mode 100644 server/sonar-web/design-system/src/components/__tests__/InputSearch-test.tsx create mode 100644 server/sonar-web/design-system/src/components/__tests__/Link-test.tsx create mode 100644 server/sonar-web/design-system/src/components/__tests__/MainAppBar-test.tsx create mode 100644 server/sonar-web/design-system/src/components/__tests__/MainMenuItem-test.tsx create mode 100644 server/sonar-web/design-system/src/components/__tests__/NavLink-test.tsx create mode 100644 server/sonar-web/design-system/src/components/__tests__/Text-test.tsx create mode 100644 server/sonar-web/design-system/src/components/__tests__/Tooltip-test.tsx create mode 100644 server/sonar-web/design-system/src/components/__tests__/clipboard-test.tsx create mode 100644 server/sonar-web/design-system/src/components/buttons.tsx create mode 100644 server/sonar-web/design-system/src/components/clipboard.tsx create mode 100644 server/sonar-web/design-system/src/components/icons/CheckIcon.tsx create mode 100644 server/sonar-web/design-system/src/components/icons/ClockIcon.tsx create mode 100644 server/sonar-web/design-system/src/components/icons/CloseIcon.tsx create mode 100644 server/sonar-web/design-system/src/components/icons/CopyIcon.tsx create mode 100644 server/sonar-web/design-system/src/components/icons/Icon.tsx create mode 100644 server/sonar-web/design-system/src/components/icons/MenuHelpIcon.tsx create mode 100644 server/sonar-web/design-system/src/components/icons/MenuIcon.tsx create mode 100644 server/sonar-web/design-system/src/components/icons/MenuSearchIcon.tsx create mode 100644 server/sonar-web/design-system/src/components/icons/OpenNewTabIcon.tsx create mode 100644 server/sonar-web/design-system/src/components/icons/SearchIcon.tsx create mode 100644 server/sonar-web/design-system/src/components/icons/StarIcon.tsx create mode 100644 server/sonar-web/design-system/src/components/icons/__tests__/Icon-test.tsx create mode 100644 server/sonar-web/design-system/src/components/icons/index.ts create mode 100644 server/sonar-web/design-system/src/components/popups.tsx create mode 100644 server/sonar-web/design-system/src/helpers/__tests__/colors-test.ts create mode 100644 server/sonar-web/design-system/src/helpers/__tests__/positioning-test.ts create mode 100644 server/sonar-web/design-system/src/helpers/__tests__/theme-test.ts create mode 100644 server/sonar-web/design-system/src/helpers/colors.ts create mode 100644 server/sonar-web/design-system/src/helpers/constants.ts rename server/sonar-web/design-system/src/{components/DummyComponent.tsx => helpers/index.ts} (91%) create mode 100644 server/sonar-web/design-system/src/helpers/keyboard.ts create mode 100644 server/sonar-web/design-system/src/helpers/l10n.ts create mode 100644 server/sonar-web/design-system/src/helpers/positioning.ts create mode 100644 server/sonar-web/design-system/src/helpers/testUtils.tsx create mode 100644 server/sonar-web/design-system/src/helpers/theme.ts create mode 100644 server/sonar-web/design-system/src/helpers/types.ts create mode 100644 server/sonar-web/design-system/src/index.ts create mode 100644 server/sonar-web/design-system/src/theme/colors.ts create mode 100644 server/sonar-web/design-system/src/theme/index.ts create mode 100644 server/sonar-web/design-system/src/theme/light.ts create mode 100644 server/sonar-web/design-system/src/types/misc.ts create mode 100644 server/sonar-web/design-system/src/types/theme.ts rename server/sonar-web/src/main/js/app/components/{search/Search.tsx => global-search/GlobalSearch.tsx} (73%) create mode 100644 server/sonar-web/src/main/js/app/components/global-search/GlobalSearchResult.tsx rename server/sonar-web/src/main/js/app/components/{search/SearchResults.tsx => global-search/GlobalSearchResults.tsx} (73%) rename server/sonar-web/src/main/js/app/components/{search/SearchShowMore.tsx => global-search/GlobalSearchShowMore.tsx} (50%) create mode 100644 server/sonar-web/src/main/js/app/components/global-search/__tests__/GlobalSearch-it.tsx rename server/sonar-web/src/main/js/app/components/{search => global-search}/utils.ts (100%) delete mode 100644 server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.css create mode 100644 server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMore.tsx create mode 100644 server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUserMenu.tsx create mode 100644 server/sonar-web/src/main/js/app/components/nav/global/MainSonarQubeBar.tsx delete mode 100644 server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavBranding-test.tsx delete mode 100644 server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNav-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavBranding-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/app/components/search/Search.css delete mode 100644 server/sonar-web/src/main/js/app/components/search/SearchResult.tsx delete mode 100644 server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.tsx delete mode 100644 server/sonar-web/src/main/js/app/components/search/__tests__/SearchResult-test.tsx delete mode 100644 server/sonar-web/src/main/js/app/components/search/__tests__/SearchResults-test.tsx delete mode 100644 server/sonar-web/src/main/js/app/components/search/__tests__/SearchShowMore-test.tsx delete mode 100644 server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/Search-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResults-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchShowMore-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/components/embed-docs-modal/DocItemLink.tsx delete mode 100644 server/sonar-web/src/main/js/components/embed-docs-modal/__tests__/SuggestionsProvider-test.tsx create mode 100644 server/sonar-web/tailwind-utilities.js create mode 100644 server/sonar-web/tailwind.base.config.js diff --git a/.cirrus.yml b/.cirrus.yml index 0c5d418fe89..9f0cddeecba 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -144,6 +144,7 @@ eslint_report_cache_template: &ESLINT_REPORT_CACHE_TEMPLATE eslint_report_cache: folders: - server/sonar-web/eslint-report/ + - server/sonar-web/design-system/eslint-report/ - private/core-extension-securityreport/eslint-report/ - private/core-extension-license/eslint-report/ - private/core-extension-enterprise-server/eslint-report/ @@ -154,6 +155,7 @@ jest_report_cache_template: &JEST_REPORT_CACHE_TEMPLATE jest_report_cache: folders: - server/sonar-web/coverage/ + - server/sonar-web/design-system/coverage/ - private/core-extension-securityreport/coverage/ - private/core-extension-license/coverage/ - private/core-extension-enterprise-server/coverage/ diff --git a/server/sonar-web/build.gradle b/server/sonar-web/build.gradle index 77062307f76..c588b6e7bc1 100644 --- a/server/sonar-web/build.gradle +++ b/server/sonar-web/build.gradle @@ -31,7 +31,7 @@ task yarn_run(type: Exec) { ['config', 'public', 'scripts', 'src'].each { inputs.dir(it).withPathSensitivity(PathSensitivity.RELATIVE) } - ['package.json', 'tsconfig.json', 'yarn.lock'].each { + ['package.json', 'tsconfig.json', 'yarn.lock', 'tailwind.config.js', 'tailwind.base.config.js'].each { inputs.file(it).withPathSensitivity(PathSensitivity.RELATIVE) } outputs.dir(webappDir) diff --git a/server/sonar-web/config/jest/SetupTheme.js b/server/sonar-web/config/jest/SetupTheme.js new file mode 100644 index 00000000000..55c454899b6 --- /dev/null +++ b/server/sonar-web/config/jest/SetupTheme.js @@ -0,0 +1,26 @@ +/* + * 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 { ThemeContext } from '@emotion/react'; +import { lightTheme } from 'design-system'; + +// Hack : override the default value of the context used for theme by emotion +// This allows tests to get the theme value without specifiying a theme provider +ThemeContext['_currentValue'] = lightTheme; +ThemeContext['_currentValue2'] = lightTheme; diff --git a/server/sonar-web/design-system/babel.config.js b/server/sonar-web/design-system/babel.config.js index 039a97f749d..1066ebdbecc 100644 --- a/server/sonar-web/design-system/babel.config.js +++ b/server/sonar-web/design-system/babel.config.js @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -export default { +module.exports = { plugins: [ 'babel-plugin-macros', [ @@ -30,5 +30,6 @@ export default { }, ], ['@babel/plugin-transform-react-jsx', { pragma: '__cssprop' }, 'twin.macro'], + '@emotion', ], }; diff --git a/server/sonar-web/design-system/build.gradle b/server/sonar-web/design-system/build.gradle new file mode 100644 index 00000000000..24e1bbdc446 --- /dev/null +++ b/server/sonar-web/design-system/build.gradle @@ -0,0 +1,40 @@ +sonar { + properties { + property 'sonar.projectName', "${projectTitle} :: Web :: Design System" + property "sonar.sources", "src" + property "sonar.exclusions", "src/**/__tests__/**,src/types/**,src/@types/**,src/helpers/testUtils.tsx" + property "sonar.tests", "src" + property "sonar.test.inclusions", "src/**/__tests__/**" + property "sonar.javascript.lcov.reportPaths", "./coverage/lcov.info" + } +} + +task "yarn_validate-ci"(type: Exec) { + dependsOn ":server:sonar-web:yarn_design-system" + + inputs.dir('src') + + ['package.json', '../yarn.lock', 'jest.config.js'].each { + inputs.file(it).withPathSensitivity(PathSensitivity.RELATIVE) + } + + outputs.dir('coverage') + outputs.cacheIf { true } + + commandLine osAdaptiveCommand(['npm', 'run', 'validate-ci']) +} + +task "yarn_lint-report-ci"(type: Exec) { + dependsOn ":server:sonar-web:yarn_design-system" + + ['src'].each { + inputs.dir(it) + } + ['package.json', '../yarn.lock', 'tsconfig.json', '.eslintrc'].each { + inputs.file(it) + } + outputs.dir('eslint-report') + outputs.cacheIf { true } + + commandLine osAdaptiveCommand(['npm', 'run', 'lint-report-ci']) +} \ No newline at end of file diff --git a/server/sonar-web/design-system/config/jest/SetupReactTestingLibrary.ts b/server/sonar-web/design-system/config/jest/SetupReactTestingLibrary.ts new file mode 100644 index 00000000000..afaa0a4fcfb --- /dev/null +++ b/server/sonar-web/design-system/config/jest/SetupReactTestingLibrary.ts @@ -0,0 +1,25 @@ +/* + * 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 '@testing-library/jest-dom'; +import { configure } from '@testing-library/react'; + +configure({ + asyncUtilTimeout: 3000, +}); diff --git a/server/sonar-web/design-system/config/jest/SetupTestEnvironment.js b/server/sonar-web/design-system/config/jest/SetupTestEnvironment.js new file mode 100644 index 00000000000..3c7139c2b4c --- /dev/null +++ b/server/sonar-web/design-system/config/jest/SetupTestEnvironment.js @@ -0,0 +1,50 @@ +/* + * 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 'whatwg-fetch'; + +const content = document.createElement('div'); +content.id = 'content'; +document.documentElement.appendChild(content); + +Element.prototype.scrollIntoView = () => {}; + +global.___loader = { + enqueue: jest.fn(), +}; + +const MockResizeObserverEntries = [ + { + contentRect: { + width: 100, + height: 200, + }, + }, +]; + +const MockResizeObserver = { + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +}; + +global.ResizeObserver = jest.fn().mockImplementation((callback) => { + callback(MockResizeObserverEntries, MockResizeObserver); + return MockResizeObserver; +}); diff --git a/server/sonar-web/design-system/config/jest/SetupTheme.js b/server/sonar-web/design-system/config/jest/SetupTheme.js new file mode 100644 index 00000000000..ac30c5a83bd --- /dev/null +++ b/server/sonar-web/design-system/config/jest/SetupTheme.js @@ -0,0 +1,26 @@ +/* + * 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 { ThemeContext } from '@emotion/react'; +import { lightTheme } from '../../src/theme'; + +// Hack : override the default value of the context used for theme by emotion +// This allows tests to get the theme value without specifiying a theme provider +ThemeContext['_currentValue'] = lightTheme; +ThemeContext['_currentValue2'] = lightTheme; diff --git a/server/sonar-web/design-system/jest.config.js b/server/sonar-web/design-system/jest.config.js new file mode 100644 index 00000000000..7da6e0a75ad --- /dev/null +++ b/server/sonar-web/design-system/jest.config.js @@ -0,0 +1,61 @@ +/* + * 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. + */ + +const babelConfig = require('./babel.config'); + +babelConfig.presets = [ + ['@babel/preset-env', { targets: { node: 'current' } }], + '@babel/preset-typescript', +]; + +module.exports = { + coverageDirectory: '/coverage', + collectCoverageFrom: [ + 'src/components/**/*.{ts,tsx,js}', + 'src/helpers/**/*.{ts,tsx,js}', + '!src/helpers/{keycodes,testUtils}.{ts,tsx}', + ], + coverageReporters: ['lcovonly', 'text'], + globals: { + 'ts-jest': { + diagnostics: false, + }, + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'json'], + moduleNameMapper: { + '^.+\\.(md|jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': + '/config/jest/FileStub.js', + // '^.+\\.css$': '/config/jest/CSSStub.js', + }, + setupFiles: [ + '/config/jest/SetupTestEnvironment.js', + '/config/jest/SetupTheme.js', + ], + setupFilesAfterEnv: ['/config/jest/SetupReactTestingLibrary.ts'], + snapshotSerializers: ['@emotion/jest/serializer'], + testEnvironment: 'jsdom', + testPathIgnorePatterns: ['/config/jest', '/node_modules', '/scripts'], + testRegex: '(/__tests__/.*|\\-test)\\.(ts|tsx|js)$', + transform: { + '^.+\\.(t|j)sx?$': ['babel-jest', babelConfig], + }, + transformIgnorePatterns: ['/node_modules/(?!(d3-.+))/'], + testTimeout: 30000, +}; diff --git a/server/sonar-web/design-system/package.json b/server/sonar-web/design-system/package.json index 11fbab61a09..ff2d3f8d877 100644 --- a/server/sonar-web/design-system/package.json +++ b/server/sonar-web/design-system/package.json @@ -1,32 +1,63 @@ { "name": "design-system", "version": "1.0.0", - "main": "./lib/index.js", - "types": "./lib/index.d.ts", + "main": "lib/index.js", + "types": "lib/index.d.ts", "scripts": { "build": "yarn lint && vite build", "build-release": "yarn install --immutable && yarn build", - "lint": "npx eslint --ext js,ts,tsx,snap --quiet src" + "lint": "eslint --ext js,ts,tsx,snap --quiet src", + "lint-report-ci": "yarn install --immutable && eslint --ext js,ts,tsx -f json -o eslint-report/eslint-report.json src || yarn lint", + "test": "jest", + "validate-ci": "yarn install --immutable && yarn test --coverage --ci" }, "devDependencies": { "@babel/core": "7.20.5", "@babel/plugin-transform-react-jsx": "7.20.13", + "@babel/preset-env": "7.20.2", + "@babel/preset-typescript": "7.18.6", + "@emotion/babel-plugin": "11.10.6", "@emotion/babel-plugin-jsx-pragmatic": "0.2.0", + "@testing-library/dom": "8.20.0", + "@testing-library/jest-dom": "5.16.5", + "@testing-library/react": "12.1.5", + "@testing-library/user-event": "14.4.3", + "@types/react": "16.14.34", + "@typescript-eslint/parser": "5.49.0", "@vitejs/plugin-react": "3.1.0", + "autoprefixer": "10.4.13", + "eslint": "8.32.0", "eslint-plugin-header": "3.1.1", "eslint-plugin-typescript-sort-keys": "2.1.0", - "twin.macro": "3.1.0", + "history": "5.3.0", + "jest": "29.3.1", + "postcss": "8.4.21", + "postcss-calc": "8.2.4", + "postcss-custom-properties": "12.1.11", + "twin.macro": "2.8.2", + "typescript": "4.9.4", "vite": "4.1.1", - "vite-plugin-dts": "1.7.2" + "vite-plugin-dts": "2.0.2", + "whatwg-fetch": "3.6.2" }, "peerDependencies": { "@emotion/react": "11.10.5", "@emotion/styled": "11.10.5", - "@typescript-eslint/parser": "5.49.0", - "eslint": "8.32.0", + "@primer/octicons-react": "17.11.1", + "classnames": "2.3.2", + "clipboard": "2.0.11", + "lodash": "4.17.21", "react": "16.14.0", "react-dom": "16.14.0", - "tailwindcss": "3.2.6", - "typescript": "4.9.4" + "react-helmet-async": "1.3.0", + "react-intl": "6.2.5", + "react-router-dom": "6.7.0", + "tailwindcss": "2.2.19" + }, + "babelMacros": { + "twin": { + "config": "../tailwind.config.js", + "preset": "emotion" + } } } diff --git a/server/sonar-web/design-system/src/@types/css.d.ts b/server/sonar-web/design-system/src/@types/css.d.ts new file mode 100644 index 00000000000..446d5d09539 --- /dev/null +++ b/server/sonar-web/design-system/src/@types/css.d.ts @@ -0,0 +1,27 @@ +/* + * 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 * as CSS from 'csstype'; + +declare module 'csstype' { + interface Properties extends CSS.Properties { + // Support any CSS Custom Property in style prop of components + [index: `--${string}`]: string | number; + } +} diff --git a/server/sonar-web/design-system/src/@types/emotion.d.ts b/server/sonar-web/design-system/src/@types/emotion.d.ts new file mode 100644 index 00000000000..6ab3a1a59bb --- /dev/null +++ b/server/sonar-web/design-system/src/@types/emotion.d.ts @@ -0,0 +1,25 @@ +/* + * 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 '@emotion/react'; +import { Theme as SQTheme } from '../types/theme'; + +declare module '@emotion/react' { + export interface Theme extends SQTheme {} +} diff --git a/server/sonar-web/design-system/src/components/Avatar.tsx b/server/sonar-web/design-system/src/components/Avatar.tsx new file mode 100644 index 00000000000..8b454295681 --- /dev/null +++ b/server/sonar-web/design-system/src/components/Avatar.tsx @@ -0,0 +1,117 @@ +/* + * 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 { ReactEventHandler, useState } from 'react'; +import tw from 'twin.macro'; +import { themeBorder, themeColor } from '../helpers/theme'; +import { GenericAvatar } from './GenericAvatar'; + +type Size = 'xs' | 'sm' | 'md' | 'lg'; + +const sizeMap: Record = { + xs: 16, + sm: 24, + md: 40, + lg: 64, +}; + +interface AvatarProps { + border?: boolean; + className?: string; + enableGravatar?: boolean; + gravatarServerUrl?: string; + hash?: string; + name?: string; + organizationAvatar?: string; + organizationName?: string; + size?: Size; +} + +export function Avatar({ + className, + enableGravatar, + gravatarServerUrl, + hash, + name, + organizationAvatar, + organizationName, + size = 'sm', + border, +}: AvatarProps) { + const [imgError, setImgError] = useState(false); + const numberSize = sizeMap[size]; + const resolvedName = organizationName ?? name; + + const handleImgError: ReactEventHandler = () => { + setImgError(true); + }; + + if (!imgError) { + if (enableGravatar && gravatarServerUrl && hash) { + const url = gravatarServerUrl + .replace('{EMAIL_MD5}', hash) + .replace('{SIZE}', String(numberSize * 2)); + + return ( + + ); + } + + if (resolvedName && organizationAvatar) { + return ( + + ); + } + } + + if (!resolvedName) { + return ; + } + + return ; +} + +const StyledAvatar = styled.img<{ border?: boolean }>` + ${tw`sw-inline-flex`}; + ${tw`sw-items-center`}; + ${tw`sw-justify-center`}; + ${tw`sw-align-top`}; + ${tw`sw-rounded-1`}; + border: ${({ border }) => (border ? themeBorder('default', 'avatarBorder') : '')}; + background: ${themeColor('avatarBackground')}; +`; diff --git a/server/sonar-web/design-system/src/components/Checkbox.tsx b/server/sonar-web/design-system/src/components/Checkbox.tsx new file mode 100644 index 00000000000..7e352d04d3d --- /dev/null +++ b/server/sonar-web/design-system/src/components/Checkbox.tsx @@ -0,0 +1,174 @@ +/* + * 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 React from 'react'; +import tw from 'twin.macro'; +import { themeBorder, themeColor, themeContrast } from '../helpers/theme'; +import DeferredSpinner from './DeferredSpinner'; +import CheckIcon from './icons/CheckIcon'; +import { CustomIcon } from './icons/Icon'; + +interface Props { + checked: boolean; + children?: React.ReactNode; + className?: string; + disabled?: boolean; + id?: string; + loading?: boolean; + onCheck: (checked: boolean, id?: string) => void; + onClick?: (event: React.MouseEvent) => void; + onFocus?: VoidFunction; + right?: boolean; + thirdState?: boolean; + title?: string; +} + +export default function Checkbox({ + checked, + disabled, + children, + className, + id, + loading = false, + onCheck, + onFocus, + onClick, + right, + thirdState = false, + title, +}: Props) { + const handleChange = () => { + if (!disabled) { + onCheck(!checked, id); + } + }; + + return ( + + {right && children} + + + + + + + {!right && children} + + ); +} + +interface CheckIconProps { + checked?: boolean; + thirdState?: boolean; +} + +function CheckboxIcon({ checked, thirdState }: CheckIconProps) { + if (checked && thirdState) { + return ( + + + + ); + } else if (checked) { + return ; + } + return null; +} + +const CheckboxContainer = styled.label<{ disabled?: boolean }>` + color: ${themeContrast('backgroundSecondary')}; + user-select: none; + + ${tw`sw-inline-flex sw-items-center`}; + + &:hover { + ${tw`sw-cursor-pointer`} + } + + &:disabled { + color: ${themeContrast('checkboxDisabled')}; + ${tw`sw-cursor-not-allowed`} + } +`; + +export const StyledCheckbox = styled.span` + border: ${themeBorder('default', 'primary')}; + color: ${themeContrast('primary')}; + + ${tw`sw-w-4 sw-h-4`}; + ${tw`sw-rounded-1/2`}; + ${tw`sw-box-border`} + ${tw`sw-inline-flex sw-items-center sw-justify-center`}; +`; + +export const AccessibleCheckbox = styled.input` + // Following css makes the checkbox accessible and invisible + border: 0; + clip: rect(0 0 0 0); + clip-path: inset(50%); + height: 1px; + overflow: hidden; + padding: 0; + white-space: nowrap; + width: 1px; + + &:focus, + &:active { + &:not(:disabled) + ${StyledCheckbox} { + outline: ${themeBorder('focus', 'primary')}; + } + } + + &:checked { + & + ${StyledCheckbox} { + background: ${themeColor('primary')}; + } + &:disabled + ${StyledCheckbox} { + background: ${themeColor('checkboxDisabledChecked')}; + } + } + + &:hover { + &:not(:disabled) + ${StyledCheckbox} { + background: ${themeColor('checkboxHover')}; + border: ${themeBorder('default', 'primary')}; + } + + &:checked:not(:disabled) + ${StyledCheckbox} { + background: ${themeColor('checkboxCheckedHover')}; + border: ${themeBorder('default', 'checkboxCheckedHover')}; + } + } + + &:disabled + ${StyledCheckbox} { + background: ${themeColor('checkboxDisabled')}; + color: ${themeColor('checkboxDisabled')}; + border: ${themeBorder('default', 'checkboxDisabledChecked')}; + } +`; diff --git a/server/sonar-web/design-system/src/components/ClickEventBoundary.tsx b/server/sonar-web/design-system/src/components/ClickEventBoundary.tsx new file mode 100644 index 00000000000..d2c5b85f0e7 --- /dev/null +++ b/server/sonar-web/design-system/src/components/ClickEventBoundary.tsx @@ -0,0 +1,35 @@ +/* + * 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 React from 'react'; + +export interface ClickEventBoundaryProps { + children: React.ReactElement; +} + +export default function ClickEventBoundary({ children }: ClickEventBoundaryProps) { + return React.cloneElement(children, { + onClick: (e: React.SyntheticEvent) => { + e.stopPropagation(); + if (typeof children.props.onClick === 'function') { + children.props.onClick(e); + } + }, + }); +} diff --git a/server/sonar-web/design-system/src/components/DeferredSpinner.tsx b/server/sonar-web/design-system/src/components/DeferredSpinner.tsx new file mode 100644 index 00000000000..50df8bc2bf7 --- /dev/null +++ b/server/sonar-web/design-system/src/components/DeferredSpinner.tsx @@ -0,0 +1,138 @@ +/* + * 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 { keyframes } from '@emotion/react'; +import styled from '@emotion/styled'; +import React from 'react'; +import tw, { theme } from 'twin.macro'; +import { translate } from '../helpers/l10n'; +import { themeColor } from '../helpers/theme'; +import { InputSearchWrapper } from './InputSearch'; + +interface Props { + children?: React.ReactNode; + className?: string; + customSpinner?: JSX.Element; + loading?: boolean; + placeholder?: boolean; + timeout?: number; +} + +interface State { + showSpinner: boolean; +} + +const DEFAULT_TIMEOUT = 100; + +export default class DeferredSpinner extends React.PureComponent { + timer?: number; + + state: State = { showSpinner: false }; + + componentDidMount() { + if (this.props.loading == null || this.props.loading === true) { + this.startTimer(); + } + } + + componentDidUpdate(prevProps: Props) { + if (prevProps.loading === false && this.props.loading === true) { + this.stopTimer(); + this.startTimer(); + } + if (prevProps.loading === true && this.props.loading === false) { + this.stopTimer(); + this.setState({ showSpinner: false }); + } + } + + componentWillUnmount() { + this.stopTimer(); + } + + startTimer = () => { + this.timer = window.setTimeout( + () => this.setState({ showSpinner: true }), + this.props.timeout || DEFAULT_TIMEOUT + ); + }; + + stopTimer = () => { + window.clearTimeout(this.timer); + }; + + render() { + const { showSpinner } = this.state; + const { customSpinner, className, children, placeholder } = this.props; + if (showSpinner) { + if (customSpinner) { + return customSpinner; + } + return ; + } + if (children) { + return children; + } + if (placeholder) { + return ; + } + return null; + } +} + +const spinAnimation = keyframes` + from { + transform: rotate(0deg); + } + + to { + transform: rotate(-360deg); + } +`; + +const Spinner = styled.div` + border: 2px solid transparent; + background: linear-gradient(0deg, ${themeColor('primary')} 50%, transparent 50% 100%) border-box, + linear-gradient(90deg, ${themeColor('primary')} 25%, transparent 75% 100%) border-box; + mask: linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; + animation: ${spinAnimation} 1s infinite linear; + + ${tw`sw-h-4 sw-w-4`}; + ${tw`sw-inline-block`}; + ${tw`sw-box-border`}; + ${tw`sw-rounded-pill`} + + ${InputSearchWrapper} & { + top: calc((2.25rem - ${theme('spacing.4')}) / 2); + ${tw`sw-left-3`}; + ${tw`sw-absolute`}; + } +`; + +Spinner.defaultProps = { 'aria-label': translate('loading'), role: 'status' }; + +const Placeholder = styled.div` + position: relative; + visibility: hidden; + + ${tw`sw-inline-flex sw-items-center sw-justify-center`}; + ${tw`sw-h-4 sw-w-4`}; +`; diff --git a/server/sonar-web/design-system/src/components/Dropdown.tsx b/server/sonar-web/design-system/src/components/Dropdown.tsx new file mode 100644 index 00000000000..f04b595decd --- /dev/null +++ b/server/sonar-web/design-system/src/components/Dropdown.tsx @@ -0,0 +1,140 @@ +/* + * 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 React from 'react'; +import { translate } from '../helpers/l10n'; +import { PopupPlacement, PopupZLevel } from '../helpers/positioning'; +import { InputSizeKeys } from '../types/theme'; +import { DropdownMenu } from './DropdownMenu'; +import DropdownToggler from './DropdownToggler'; +import MenuIcon from './icons/MenuIcon'; +import { InteractiveIcon } from './InteractiveIcon'; + +type OnClickCallback = (event?: React.MouseEvent) => void; +type A11yAttrs = Pick & { + id: string; + role: React.AriaRole; +}; +interface RenderProps { + a11yAttrs: A11yAttrs; + closeDropdown: VoidFunction; + onToggleClick: OnClickCallback; + open: boolean; +} + +interface Props { + allowResizing?: boolean; + children: + | ((renderProps: RenderProps) => JSX.Element) + | React.ReactElement<{ onClick: OnClickCallback }>; + className?: string; + closeOnClick?: boolean; + id: string; + onOpen?: VoidFunction; + overlay: React.ReactNode; + placement?: PopupPlacement; + size?: InputSizeKeys; + zLevel?: PopupZLevel; +} + +interface State { + open: boolean; +} + +export default class Dropdown extends React.PureComponent { + state: State = { open: false }; + + componentDidUpdate(_: Props, prevState: State) { + if (!prevState.open && this.state.open && this.props.onOpen) { + this.props.onOpen(); + } + } + + handleClose = () => { + this.setState({ open: false }); + }; + + handleToggleClick: OnClickCallback = (event) => { + if (event) { + event.preventDefault(); + event.currentTarget.blur(); + } + this.setState((state) => ({ open: !state.open })); + }; + + render() { + const { open } = this.state; + const { allowResizing, className, closeOnClick = true, id, size = 'full', zLevel } = this.props; + const a11yAttrs: A11yAttrs = { + 'aria-controls': `${id}-dropdown`, + 'aria-expanded': open, + 'aria-haspopup': 'menu', + id: `${id}-trigger`, + role: 'button', + }; + + const children = React.isValidElement(this.props.children) + ? React.cloneElement(this.props.children, { onClick: this.handleToggleClick, ...a11yAttrs }) + : this.props.children({ + a11yAttrs, + closeDropdown: this.handleClose, + onToggleClick: this.handleToggleClick, + open, + }); + + return ( + + {this.props.overlay} + + } + placement={this.props.placement} + zLevel={zLevel} + > + {children} + + ); + } +} + +interface ActionsDropdownProps extends Omit { + buttonSize?: 'small' | 'medium'; + children: React.ReactNode; +} + +export function ActionsDropdown(props: ActionsDropdownProps) { + const { children, buttonSize, ...dropdownProps } = props; + return ( + + + + ); +} diff --git a/server/sonar-web/design-system/src/components/DropdownMenu.tsx b/server/sonar-web/design-system/src/components/DropdownMenu.tsx new file mode 100644 index 00000000000..d16011785b9 --- /dev/null +++ b/server/sonar-web/design-system/src/components/DropdownMenu.tsx @@ -0,0 +1,370 @@ +/* + * 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 { css } from '@emotion/react'; +import styled from '@emotion/styled'; +import classNames from 'classnames'; +import React from 'react'; +import tw from 'twin.macro'; +import { INPUT_SIZES } from '../helpers/constants'; +import { translate } from '../helpers/l10n'; +import { themeBorder, themeColor, themeContrast } from '../helpers/theme'; +import { InputSizeKeys, ThemedProps } from '../types/theme'; +import Checkbox from './Checkbox'; +import { ClipboardBase } from './clipboard'; +import { BaseLink, LinkProps } from './Link'; +import NavLink from './NavLink'; +import RadioButton from './RadioButton'; +import Tooltip from './Tooltip'; + +interface Props extends React.HtmlHTMLAttributes { + children?: React.ReactNode; + className?: string; + innerRef?: React.Ref; + maxHeight?: string; + size?: InputSizeKeys; +} + +export function DropdownMenu({ + children, + className, + innerRef, + maxHeight = 'inherit', + size = 'small', + ...menuProps +}: Props) { + return ( + + {children} + + ); +} + +interface ListItemProps { + children?: React.ReactNode; + className?: string; + innerRef?: React.Ref; + onFocus?: VoidFunction; + onPointerEnter?: VoidFunction; + onPointerLeave?: VoidFunction; +} + +type ItemLinkProps = Omit & + Pick & { + innerRef?: React.Ref; + }; + +export function ItemLink(props: ItemLinkProps) { + const { children, className, disabled, icon, onClick, innerRef, to, ...liProps } = props; + return ( +
  • + + {children} + +
  • + ); +} + +interface ItemNavLinkProps extends ItemLinkProps { + end?: boolean; +} + +export function ItemNavLink(props: ItemNavLinkProps) { + const { children, className, disabled, end, icon, onClick, innerRef, to, ...liProps } = props; + return ( +
  • + + {icon} + {children} + +
  • + ); +} + +interface ItemButtonProps extends ListItemProps { + disabled?: boolean; + icon?: React.ReactNode; + onClick: React.MouseEventHandler; +} + +export function ItemButton(props: ItemButtonProps) { + const { children, className, disabled, icon, innerRef, onClick, ...liProps } = props; + return ( +
  • + + {icon} + {children} + +
  • + ); +} + +export const ItemDangerButton = styled(ItemButton)` + --color: ${themeContrast('dropdownMenuDanger')}; +`; + +interface ItemCheckboxProps extends ListItemProps { + checked: boolean; + disabled?: boolean; + id?: string; + onCheck: (checked: boolean, id?: string) => void; +} + +export function ItemCheckbox(props: ItemCheckboxProps) { + const { checked, children, className, disabled, id, innerRef, onCheck, onFocus, ...liProps } = + props; + return ( +
  • + + {children} + +
  • + ); +} + +interface ItemRadioButtonProps extends ListItemProps { + checked: boolean; + disabled?: boolean; + onCheck: (value: string) => void; + value: string; +} + +export function ItemRadioButton(props: ItemRadioButtonProps) { + const { checked, children, className, disabled, innerRef, onCheck, value, ...liProps } = props; + return ( +
  • + + {children} + +
  • + ); +} + +interface ItemCopyProps { + children?: React.ReactNode; + className?: string; + copyValue: string; +} + +export function ItemCopy(props: ItemCopyProps) { + const { children, className, copyValue } = props; + return ( + + {({ setCopyButton, copySuccess }) => ( + +
  • + + {children} + +
  • +
    + )} +
    + ); +} + +interface ItemDownloadProps extends ListItemProps { + download: string; + href: string; +} + +export function ItemDownload(props: ItemDownloadProps) { + const { children, className, download, href, innerRef, ...liProps } = props; + return ( +
  • + + {children} + +
  • + ); +} + +export const ItemHeaderHighlight = styled.span` + color: ${themeContrast('searchHighlight')}; + font-weight: 600; +`; + +export const ItemHeader = styled.li` + background-color: ${themeColor('dropdownMenuHeader')}; + color: ${themeContrast('dropdownMenuHeader')}; + + ${tw`sw-py-2 sw-px-3`} +`; +ItemHeader.defaultProps = { className: 'dropdown-menu-header', role: 'menuitem' }; + +export const ItemDivider = styled.li` + height: 1px; + background-color: ${themeColor('popupBorder')}; + + ${tw`sw-my-1 sw--mx-2`} + ${tw`sw-overflow-hidden`}; +`; +ItemDivider.defaultProps = { role: 'separator' }; + +const DropdownMenuWrapper = styled.ul` + background-color: ${themeColor('dropdownMenu')}; + color: ${themeContrast('dropdownMenu')}; + width: var(--inputSize); + list-style: none; + + ${tw`sw-flex sw-flex-col`} + ${tw`sw-box-border`}; + ${tw`sw-min-w-input-small`} + ${tw`sw-py-2`} + ${tw`sw-body-sm`} + + &:focus { + outline: none; + } +`; + +const itemStyle = (props: ThemedProps) => css` + color: var(--color); + background-color: ${themeColor('dropdownMenu')(props)}; + border: none; + border-bottom: none; + text-decoration: none; + transition: none; + + ${tw`sw-flex sw-items-center`} + ${tw`sw-body-sm`} + ${tw`sw-box-border`} + ${tw`sw-w-full`} + ${tw`sw-text-left`} + ${tw`sw-py-2 sw-px-3`} + ${tw`sw-truncate`}; + ${tw`sw-cursor-pointer`} + + &.active, + &:active, + &.active:active, + &:hover, + &.active:hover { + color: var(--color); + background-color: ${themeColor('dropdownMenuHover')(props)}; + text-decoration: none; + outline: none; + border: none; + border-bottom: none; + } + + &:focus, + &:focus-within, + &.active:focus, + &.active:focus-within { + color: var(--color); + background-color: ${themeColor('dropdownMenuFocus')(props)}; + text-decoration: none; + outline: ${themeBorder('focus', 'dropdownMenuFocusBorder')(props)}; + outline-offset: -4px; + border: none; + border-bottom: none; + } + + &:disabled, + &.disabled { + color: ${themeContrast('dropdownMenuDisabled')(props)}; + background-color: ${themeColor('dropdownMenuDisabled')(props)}; + pointer-events: none !important; + + ${tw`sw-cursor-not-allowed`}; + } + + & > svg { + ${tw`sw-mr-2`} + } +`; + +const ItemNavLinkStyled = styled(NavLink)` + --color: ${themeContrast('dropdownMenu')}; + ${itemStyle}; +`; + +const ItemLinkStyled = styled(BaseLink)` + --color: ${themeContrast('dropdownMenu')}; + ${itemStyle} +`; + +const ItemButtonStyled = styled.button` + --color: ${themeContrast('dropdownMenu')}; + ${itemStyle} +`; + +const ItemDownloadStyled = styled.a` + --color: ${themeContrast('dropdownMenu')}; + ${itemStyle} +`; + +const ItemCheckboxStyled = styled(Checkbox)` + --color: ${themeContrast('dropdownMenu')}; + ${itemStyle} +`; + +const ItemRadioButtonStyled = styled(RadioButton)` + --color: ${themeContrast('dropdownMenu')}; + ${itemStyle} +`; diff --git a/server/sonar-web/design-system/src/components/DropdownToggler.tsx b/server/sonar-web/design-system/src/components/DropdownToggler.tsx new file mode 100644 index 00000000000..f46f3dc8456 --- /dev/null +++ b/server/sonar-web/design-system/src/components/DropdownToggler.tsx @@ -0,0 +1,48 @@ +/* + * 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 EscKeydownHandler from './EscKeydownHandler'; +import OutsideClickHandler from './OutsideClickHandler'; +import { PortalPopup } from './popups'; + +type PopupProps = PortalPopup['props']; + +interface Props extends PopupProps { + onRequestClose: VoidFunction; + open: boolean; +} + +export default function DropdownToggler(props: Props) { + const { children, open, onRequestClose, overlay, ...popupProps } = props; + + return ( + + {overlay} + + ) : undefined + } + {...popupProps} + > + {children} + + ); +} diff --git a/server/sonar-web/design-system/src/components/EscKeydownHandler.tsx b/server/sonar-web/design-system/src/components/EscKeydownHandler.tsx new file mode 100644 index 00000000000..9b0155ab6dd --- /dev/null +++ b/server/sonar-web/design-system/src/components/EscKeydownHandler.tsx @@ -0,0 +1,48 @@ +/* + * 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 React from 'react'; +import { Key } from '../helpers/keyboard'; + +interface Props { + children: React.ReactNode; + onKeydown: () => void; +} + +export default class EscKeydownHandler extends React.Component { + componentDidMount() { + setTimeout(() => { + document.addEventListener('keydown', this.handleKeyDown, false); + }, 0); + } + + componentWillUnmount() { + document.removeEventListener('keydown', this.handleKeyDown, false); + } + + handleKeyDown = (event: KeyboardEvent) => { + if (event.code === Key.Escape) { + this.props.onKeydown(); + } + }; + + render() { + return this.props.children; + } +} diff --git a/server/sonar-web/design-system/src/components/GenericAvatar.tsx b/server/sonar-web/design-system/src/components/GenericAvatar.tsx new file mode 100644 index 00000000000..4d8fa6901be --- /dev/null +++ b/server/sonar-web/design-system/src/components/GenericAvatar.tsx @@ -0,0 +1,60 @@ +/* + * 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 { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import React from 'react'; +import tw from 'twin.macro'; +import { themeAvatarColor } from '../helpers/theme'; +import { IconProps } from './icons/Icon'; + +export interface GenericAvatarProps { + Icon?: React.ComponentType; + className?: string; + name: string; + size?: number; +} + +export function GenericAvatar({ className, Icon, name, size = 24 }: GenericAvatarProps) { + const theme = useTheme(); + const text = name.length > 0 ? name[0].toUpperCase() : ''; + + return ( + + {Icon ? : text} + + ); +} + +export const StyledGenericAvatar = styled.div<{ name: string; size: number }>` + ${tw`sw-text-center`}; + ${tw`sw-align-top`}; + ${tw`sw-select-none`}; + ${tw`sw-font-regular`}; + ${tw`sw-rounded-1`}; + ${tw`sw-inline-flex`}; + ${tw`sw-items-center`}; + ${tw`sw-justify-center`}; + height: ${({ size }) => size}px; + width: ${({ size }) => size}px; + background-color: ${({ name, theme }) => themeAvatarColor(name)({ theme })}; + color: ${({ name, theme }) => themeAvatarColor(name, true)({ theme })}; + font-size: ${({ size }) => Math.max(Math.floor(size / 2), 8)}px; + line-height: ${({ size }) => size}px; +`; diff --git a/server/sonar-web/design-system/src/components/InputSearch.tsx b/server/sonar-web/design-system/src/components/InputSearch.tsx new file mode 100644 index 00000000000..5e5e9c0e3ee --- /dev/null +++ b/server/sonar-web/design-system/src/components/InputSearch.tsx @@ -0,0 +1,243 @@ +/* + * 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 { debounce } from 'lodash'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import tw, { theme } from 'twin.macro'; +import { DEBOUNCE_DELAY, INPUT_SIZES } from '../helpers/constants'; +import { Key } from '../helpers/keyboard'; +import { themeBorder, themeColor, themeContrast } from '../helpers/theme'; +import { isDefined } from '../helpers/types'; +import { InputSizeKeys } from '../types/theme'; +import DeferredSpinner from './DeferredSpinner'; +import CloseIcon from './icons/CloseIcon'; +import SearchIcon from './icons/SearchIcon'; +import { InteractiveIcon } from './InteractiveIcon'; + +interface Props { + autoFocus?: boolean; + className?: string; + clearIconAriaLabel: string; + id?: string; + innerRef?: React.RefCallback; + loading?: boolean; + maxLength?: number; + minLength?: number; + onBlur?: React.FocusEventHandler; + onChange: (value: string) => void; + onFocus?: React.FocusEventHandler; + onKeyDown?: React.KeyboardEventHandler; + onMouseDown?: React.MouseEventHandler; + placeholder: string; + searchInputAriaLabel: string; + size?: InputSizeKeys; + tooShortText: string; + value?: string; +} + +const DEFAULT_MAX_LENGTH = 100; + +export default function InputSearch({ + autoFocus, + id, + className, + innerRef, + onBlur, + onChange, + onFocus, + onKeyDown, + onMouseDown, + placeholder, + loading, + minLength, + maxLength = DEFAULT_MAX_LENGTH, + size = 'medium', + value: parentValue, + tooShortText, + searchInputAriaLabel, + clearIconAriaLabel, +}: Props) { + const input = useRef(null); + const [value, setValue] = useState(parentValue ?? ''); + const debouncedOnChange = useMemo(() => debounce(onChange, DEBOUNCE_DELAY), [onChange]); + + const tooShort = isDefined(minLength) && value.length > 0 && value.length < minLength; + const inputClassName = classNames('js-input-search', { + touched: value.length > 0 && (!minLength || minLength > value.length), + 'sw-pr-10': value.length > 0, + }); + + useEffect(() => { + if (parentValue !== undefined) { + setValue(parentValue); + } + }, [parentValue]); + + const changeValue = (newValue: string) => { + if (newValue.length === 0 || !minLength || minLength <= newValue.length) { + debouncedOnChange(newValue); + } + }; + + const handleInputChange = (event: React.SyntheticEvent) => { + const eventValue = event.currentTarget.value; + setValue(eventValue); + changeValue(eventValue); + }; + + const handleInputKeyDown = (event: React.KeyboardEvent) => { + if (event.key === Key.Escape) { + event.preventDefault(); + handleClearClick(); + } + onKeyDown?.(event); + }; + + const handleClearClick = () => { + onChange(''); + if (parentValue === undefined || parentValue === '') { + setValue(''); + } + input.current?.focus(); + }; + const ref = (node: HTMLInputElement | null) => { + input.current = node; + innerRef?.(node); + }; + + return ( + + + + + + + {value && ( + + )} + + {tooShort && isDefined(minLength) && ( + + {tooShortText} + + )} + + + ); +} + +export const InputSearchWrapper = styled.div` + width: var(--inputSize); + + ${tw`sw-relative sw-inline-block`} + ${tw`sw-whitespace-nowrap`} + ${tw`sw-align-middle`} + ${tw`sw-h-control`} +`; + +export const StyledInputWrapper = styled.div` + input { + background: ${themeColor('inputBackground')}; + color: ${themeContrast('inputBackground')}; + border: ${themeBorder('default', 'inputBorder')}; + + ${tw`sw-rounded-2`} + ${tw`sw-box-border`} + ${tw`sw-pl-10`} + ${tw`sw-body-sm`} + ${tw`sw-w-full sw-h-control`} + + &::placeholder { + color: ${themeColor('inputPlaceholder')}; + + ${tw`sw-truncate`} + } + + &:hover { + border: ${themeBorder('default', 'inputFocus')}; + } + + &:focus, + &:active { + border: ${themeBorder('default', 'inputFocus')}; + outline: ${themeBorder('focus', 'inputFocus')}; + } + + &::-webkit-search-decoration, + &::-webkit-search-cancel-button, + &::-webkit-search-results-button, + &::-webkit-search-results-decoration { + ${tw`sw-hidden sw-appearance-none`} + } + } +`; + +const StyledSearchIcon = styled(SearchIcon)` + color: ${themeColor('inputBorder')}; + top: calc((${theme('height.control')} - ${theme('spacing.4')}) / 2); + + ${tw`sw-left-3`} + ${tw`sw-absolute`} +`; + +export const StyledInteractiveIcon = styled(InteractiveIcon)` + ${tw`sw-absolute`} + ${tw`sw-right-2`} +`; + +const StyledNote = styled.span` + color: ${themeColor('inputPlaceholder')}; + top: calc(1px + ${theme('inset.2')}); + + ${tw`sw-absolute`} + ${tw`sw-left-12 sw-right-10`} + ${tw`sw-body-sm`} + ${tw`sw-text-right`} + ${tw`sw-truncate`} + ${tw`sw-pointer-events-none`} +`; diff --git a/server/sonar-web/design-system/src/components/InteractiveIcon.tsx b/server/sonar-web/design-system/src/components/InteractiveIcon.tsx new file mode 100644 index 00000000000..ebd9cb73e9a --- /dev/null +++ b/server/sonar-web/design-system/src/components/InteractiveIcon.tsx @@ -0,0 +1,182 @@ +/* + * 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 { css } from '@emotion/react'; +import styled from '@emotion/styled'; +import classNames from 'classnames'; +import React from 'react'; +import tw from 'twin.macro'; +import { themeBorder, themeColor, themeContrast } from '../helpers/theme'; +import { isDefined } from '../helpers/types'; +import { ThemedProps } from '../types/theme'; +import { IconProps } from './icons/Icon'; +import { BaseLink, LinkProps } from './Link'; + +export type InteractiveIconSize = 'small' | 'medium'; + +export interface InteractiveIconProps { + Icon: React.ComponentType; + 'aria-label': string; + children?: React.ReactNode; + className?: string; + currentColor?: boolean; + disabled?: boolean; + id?: string; + innerRef?: React.Ref; + onClick?: VoidFunction; + size?: InteractiveIconSize; + stopPropagation?: boolean; + to?: LinkProps['to']; +} + +export class InteractiveIconBase extends React.PureComponent { + handleClick = (event: React.MouseEvent) => { + const { disabled, onClick, stopPropagation = true } = this.props; + event.currentTarget.blur(); + + if (stopPropagation) { + event.stopPropagation(); + } + + if (onClick && !disabled) { + onClick(); + } + }; + + render() { + const { + Icon, + children, + disabled, + innerRef, + onClick, + size = 'medium', + to, + ...htmlProps + } = this.props; + + const props = { + ...htmlProps, + 'aria-disabled': disabled, + disabled, + size, + type: 'button' as const, + }; + + if (to) { + return ( + + + {children} + + ); + } + + return ( + + + {children} + + ); + } +} + +const buttonIconStyle = (props: ThemedProps & { size: InteractiveIconSize }) => css` + box-sizing: border-box; + border: none; + outline: none; + text-decoration: none; + color: var(--color); + background-color: var(--background); + transition: background-color 0.2s ease, outline 0.2s ease, color 0.2s ease; + + ${tw`sw-inline-flex sw-items-center sw-justify-center`} + ${tw`sw-cursor-pointer`} + + ${{ + small: tw`sw-h-6 sw-px-1 sw-rounded-1/2`, + medium: tw`sw-h-control sw-px-[0.625rem] sw-rounded-2`, + }[props.size]} + + + &:hover, + &:focus, + &:active { + color: var(--colorHover); + background-color: var(--backgroundHover); + } + + &:focus, + &:active { + outline: ${themeBorder('focus', 'var(--focus)')(props)}; + } + + &:disabled, + &:disabled:hover { + color: ${themeContrast('buttonDisabled')(props)}; + background-color: var(--background); + + ${tw`sw-cursor-not-allowed`} + } +`; + +const IconLink = styled(BaseLink)` + ${buttonIconStyle} +`; + +const IconButton = styled.button` + ${buttonIconStyle} +`; + +export const InteractiveIcon: React.FC = styled(InteractiveIconBase)` + --background: ${themeColor('interactiveIcon')}; + --backgroundHover: ${themeColor('interactiveIconHover')}; + --color: ${({ currentColor, theme }) => + currentColor ? 'currentColor' : themeContrast('interactiveIcon')({ theme })}; + --colorHover: ${themeContrast('interactiveIconHover')}; + --focus: ${themeColor('interactiveIconFocus', 0.2)}; +`; + +export const DiscreetInteractiveIcon: React.FC = styled(InteractiveIcon)` + --color: ${themeColor('discreetInteractiveIcon')}; +`; + +export const DestructiveIcon: React.FC = styled(InteractiveIconBase)` + --background: ${themeColor('destructiveIcon')}; + --backgroundHover: ${themeColor('destructiveIconHover')}; + --color: ${themeContrast('destructiveIcon')}; + --colorHover: ${themeContrast('destructiveIconHover')}; + --focus: ${themeColor('destructiveIconFocus', 0.2)}; +`; + +export const DismissProductNewsIcon: React.FC = styled(InteractiveIcon)` + --background: ${themeColor('productNews')}; + --backgroundHover: ${themeColor('productNewsHover')}; + --color: ${themeContrast('productNews')}; + --colorHover: ${themeContrast('productNewsHover')}; + --focus: ${themeColor('interactiveIconFocus', 0.2)}; + + height: 28px; +`; diff --git a/server/sonar-web/design-system/src/components/Link.tsx b/server/sonar-web/design-system/src/components/Link.tsx new file mode 100644 index 00000000000..5f427ece7e2 --- /dev/null +++ b/server/sonar-web/design-system/src/components/Link.tsx @@ -0,0 +1,173 @@ +/* + * 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 { css } from '@emotion/react'; +import styled from '@emotion/styled'; +import React, { HTMLAttributeAnchorTarget } from 'react'; +import { Link as RouterLink, LinkProps as RouterLinkProps } from 'react-router-dom'; +import tw, { theme as twTheme } from 'twin.macro'; +import { themeBorder, themeColor } from '../helpers/theme'; +import OpenNewTabIcon from './icons/OpenNewTabIcon'; +import { TooltipWrapperInner } from './Tooltip'; + +export interface LinkProps extends RouterLinkProps { + blurAfterClick?: boolean; + disabled?: boolean; + forceExternal?: boolean; + icon?: React.ReactNode; + onClick?: (event: React.MouseEvent) => void; + preventDefault?: boolean; + showExternalIcon?: boolean; + stopPropagation?: boolean; + target?: HTMLAttributeAnchorTarget; +} + +function BaseLinkWithRef(props: LinkProps, ref: React.ForwardedRef) { + const { + children, + blurAfterClick, + disabled, + icon, + onClick, + preventDefault, + showExternalIcon = !icon, + stopPropagation, + target = '_blank', + to, + ...rest + } = props; + const isExternal = typeof to === 'string' && to.startsWith('http'); + const handleClick = React.useCallback( + (event: React.MouseEvent) => { + if (blurAfterClick) { + event.currentTarget.blur(); + } + + if (preventDefault || disabled) { + event.preventDefault(); + } + + if (stopPropagation) { + event.stopPropagation(); + } + + if (onClick && !disabled) { + onClick(event); + } + }, + [onClick, blurAfterClick, preventDefault, stopPropagation, disabled] + ); + + return isExternal ? ( + + {icon} + {children} + {showExternalIcon && } + + ) : ( + + {icon} + {children} + + ); +} + +export const BaseLink = React.forwardRef(BaseLinkWithRef); + +const StyledBaseLink = styled(BaseLink)` + color: var(--color); + border-bottom: ${({ children, icon, theme }) => + icon && !children ? themeBorder('default', 'transparent')({ theme }) : 'var(--border)'}; + + &:visited { + color: var(--color); + } + + &:hover, + &:focus, + &:active { + color: var(--active); + border-bottom: ${({ children, icon, theme }) => + icon && !children ? themeBorder('default', 'transparent')({ theme }) : 'var(--borderActive)'}; + } + + & > svg { + ${tw`sw-align-text-bottom!`} + } + + ${({ icon }) => + icon && + css` + margin-left: calc(${twTheme('width.icon')} + ${twTheme('spacing.1')}); + + & > svg, + & > img { + ${tw`sw-mr-1`} + + margin-left: calc(-1 * (${twTheme('width.icon')} + ${twTheme('spacing.1')})); + } + `}; +`; + +export const HoverLink = styled(StyledBaseLink)` + text-decoration: none; + + --color: ${themeColor('linkDiscreet')}; + --active: ${themeColor('linkActive')}; + --border: ${themeBorder('default', 'transparent')}; + --borderActive: ${themeBorder('default', 'linkActive')}; + + ${TooltipWrapperInner} & { + --active: ${themeColor('linkTooltipActive')}; + --borderActive: ${themeBorder('default', 'linkTooltipActive')}; + } +`; +HoverLink.displayName = 'HoverLink'; + +export const DiscreetLink = styled(HoverLink)` + --border: ${themeBorder('default', 'linkDiscreet')}; +`; +DiscreetLink.displayName = 'DiscreetLink'; + +const StandoutLink = styled(StyledBaseLink)` + ${tw`sw-font-semibold`} + ${tw`sw-no-underline`} + + --color: ${themeColor('linkDefault')}; + --active: ${themeColor('linkActive')}; + --border: ${themeBorder('default', 'linkDefault')}; + --borderActive: ${themeBorder('default', 'linkDefault')}; + + ${TooltipWrapperInner} & { + --color: ${themeColor('linkTooltipDefault')}; + --active: ${themeColor('linkTooltipActive')}; + --border: ${themeBorder('default', 'linkTooltipDefault')}; + --borderActive: ${themeBorder('default', 'linkTooltipActive')}; + } +`; +StandoutLink.displayName = 'StandoutLink'; + +export default StandoutLink; diff --git a/server/sonar-web/design-system/src/components/MainAppBar.tsx b/server/sonar-web/design-system/src/components/MainAppBar.tsx new file mode 100644 index 00000000000..97303a00158 --- /dev/null +++ b/server/sonar-web/design-system/src/components/MainAppBar.tsx @@ -0,0 +1,89 @@ +/* + * 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 { + LAYOUT_GLOBAL_NAV_HEIGHT, + LAYOUT_LOGO_MARGIN_RIGHT, + LAYOUT_LOGO_MAX_HEIGHT, + LAYOUT_LOGO_MAX_WIDTH, +} from '../helpers/constants'; +import { themeBorder, themeColor, themeContrast } from '../helpers/theme'; + +const MainAppBarContainerDiv = styled.div` + height: ${LAYOUT_GLOBAL_NAV_HEIGHT}px; +`; + +const MainAppBarDiv = styled.div` + ${tw`sw-fixed`} + ${tw`sw-flex`}; + ${tw`sw-items-center`}; + ${tw`sw-left-0`}; + ${tw`sw-px-6`}; + ${tw`sw-right-0`}; + ${tw`sw-w-full`}; + ${tw`sw-box-border`}; + ${tw`sw-z-global-navbar`}; + + background: ${themeColor('mainBar')}; + border-bottom: ${themeBorder('default')}; + color: ${themeContrast('mainBar')}; + height: ${LAYOUT_GLOBAL_NAV_HEIGHT}px; +`; + +const MainAppBarNavLogoDiv = styled.div` + margin-right: ${LAYOUT_LOGO_MARGIN_RIGHT}px; + + img, + svg { + ${tw`sw-object-contain`}; + + max-height: ${LAYOUT_LOGO_MAX_HEIGHT}px; + max-width: ${LAYOUT_LOGO_MAX_WIDTH}px; + } +`; + +const MainAppBarNavLogoLink = styled.a` + border: none; +`; + +const MainAppBarNavRightDiv = styled.div` + flex-grow: 2; + height: 100%; +`; + +export function MainAppBar({ + children, + Logo, +}: React.PropsWithChildren<{ Logo: React.ElementType }>) { + return ( + + + + + + + + {children} + + + ); +} diff --git a/server/sonar-web/design-system/src/components/MainMenu.tsx b/server/sonar-web/design-system/src/components/MainMenu.tsx new file mode 100644 index 00000000000..e61964a77e3 --- /dev/null +++ b/server/sonar-web/design-system/src/components/MainMenu.tsx @@ -0,0 +1,30 @@ +/* + * 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'; + +const MainMenuUl = styled.ul` + ${tw`sw-flex sw-gap-8 sw-items-center`} +`; + +export function MainMenu({ children }: React.PropsWithChildren<{}>) { + return {children}; +} diff --git a/server/sonar-web/design-system/src/components/MainMenuItem.tsx b/server/sonar-web/design-system/src/components/MainMenuItem.tsx new file mode 100644 index 00000000000..9749ba9028a --- /dev/null +++ b/server/sonar-web/design-system/src/components/MainMenuItem.tsx @@ -0,0 +1,59 @@ +/* + * 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 { LAYOUT_GLOBAL_NAV_HEIGHT } from '../helpers/constants'; +import { themeBorder, themeContrast } from '../helpers/theme'; + +export const MainMenuItem = styled.li` + & a { + ${tw`sw-block sw-box-border`}; + ${tw`sw-text-sm sw-font-semibold`}; + ${tw`sw-whitespace-nowrap`}; + ${tw`sw-no-underline`}; + ${tw`sw-select-none`}; + ${tw`sw-font-sans`}; + + color: ${themeContrast('mainBar')}; + letter-spacing: 0.03em; + line-height: calc(${LAYOUT_GLOBAL_NAV_HEIGHT}px - 3px); // - 3px border bottom + border-bottom: ${themeBorder('active', 'transparent', 1)}; + + &:visited { + border-bottom: ${themeBorder('active', 'transparent', 1)}; + color: ${themeContrast('mainBar')}; + } + + &:active, + &.active, + &:focus { + border-bottom: ${themeBorder('active', 'menuBorder', 1)}; + color: ${themeContrast('mainBar')}; + } + + &:hover, + &.hover, + &[aria-expanded='true'] { + border-bottom: ${themeBorder('active', 'menuBorder', 1)}; + color: ${themeContrast('mainBarHover')}; + } + } +`; diff --git a/server/sonar-web/design-system/src/components/NavLink.tsx b/server/sonar-web/design-system/src/components/NavLink.tsx new file mode 100644 index 00000000000..8075c5e182c --- /dev/null +++ b/server/sonar-web/design-system/src/components/NavLink.tsx @@ -0,0 +1,74 @@ +/* + * 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 React from 'react'; +import { NavLink as RouterNavLink, NavLinkProps as RouterNavLinkProps } from 'react-router-dom'; + +export interface NavLinkProps extends RouterNavLinkProps { + blurAfterClick?: boolean; + disabled?: boolean; + onClick?: (event: React.MouseEvent) => void; + preventDefault?: boolean; + stopPropagation?: boolean; +} + +// Styling this component directly with Emotion should be avoided due to conflicts with react-router's classname. +// Use NavBarTabs as an example of this exception. +function NavLinkWithRef(props: NavLinkProps, ref: React.ForwardedRef) { + const { + blurAfterClick, + children, + disabled, + onClick, + preventDefault, + stopPropagation, + ...otherProps + } = props; + + const handleClick = React.useCallback( + (event: React.MouseEvent) => { + if (blurAfterClick) { + // explicitly lose focus after click + event.currentTarget.blur(); + } + + if (preventDefault || disabled) { + event.preventDefault(); + } + + if (stopPropagation) { + event.stopPropagation(); + } + + if (onClick && !disabled) { + onClick(event); + } + }, + [onClick, blurAfterClick, preventDefault, stopPropagation, disabled] + ); + + return ( + + {children} + + ); +} + +const NavLink = React.forwardRef(NavLinkWithRef); +export default NavLink; diff --git a/server/sonar-web/design-system/src/components/OutsideClickHandler.tsx b/server/sonar-web/design-system/src/components/OutsideClickHandler.tsx new file mode 100644 index 00000000000..07de4f5422f --- /dev/null +++ b/server/sonar-web/design-system/src/components/OutsideClickHandler.tsx @@ -0,0 +1,68 @@ +/* + * 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 React from 'react'; +import { findDOMNode } from 'react-dom'; + +export type MouseEventListener = 'click' | 'mousedown'; +interface Props { + children: React.ReactNode; + listenerType?: MouseEventListener; + onClickOutside: () => void; +} + +export default class OutsideClickHandler extends React.Component { + mounted = false; + + componentDidMount() { + this.mounted = true; + setTimeout(() => { + this.addClickHandler(); + }, 0); + } + + componentWillUnmount() { + this.mounted = false; + this.removeClickHandler(); + } + + addClickHandler = () => { + const { listenerType = 'click' } = this.props; + window.addEventListener(listenerType, this.handleWindowClick); + }; + + removeClickHandler = () => { + const { listenerType = 'click' } = this.props; + window.removeEventListener(listenerType, this.handleWindowClick); + }; + + handleWindowClick = (event: MouseEvent) => { + if (this.mounted) { + // eslint-disable-next-line react/no-find-dom-node + const node = findDOMNode(this); + if (!node || !node.contains(event.target as Node)) { + this.props.onClickOutside(); + } + } + }; + + render() { + return this.props.children; + } +} diff --git a/server/sonar-web/design-system/src/components/RadioButton.tsx b/server/sonar-web/design-system/src/components/RadioButton.tsx new file mode 100644 index 00000000000..89858e73574 --- /dev/null +++ b/server/sonar-web/design-system/src/components/RadioButton.tsx @@ -0,0 +1,125 @@ +/* + * 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 React from 'react'; +import tw from 'twin.macro'; +import { themeBorder, themeColor } from '../helpers/theme'; + +type AllowedRadioButtonAttributes = Pick< + React.InputHTMLAttributes, + 'aria-label' | 'autoFocus' | 'id' | 'name' | 'style' | 'title' | 'type' +>; + +interface Props extends AllowedRadioButtonAttributes { + checked: boolean; + children?: React.ReactNode; + className?: string; + disabled?: boolean; + onCheck: (value: string) => void; + value: string; +} + +export default function RadioButton({ + checked, + children, + className, + disabled, + onCheck, + value, + ...htmlProps +}: Props) { + const handleChange = () => { + if (!disabled) { + onCheck(value); + } + }; + + return ( + + ); +} + +export const RadioButtonStyled = styled.input` + appearance: none; //disables native style + border: ${themeBorder('default', 'radioBorder')}; + + ${tw`sw-w-4 sw-min-w-4 sw-h-4 sw-min-h-4`} + ${tw`sw-p-1 sw-mr-2`} + ${tw`sw-inline-block`} + ${tw`sw-box-border`} + ${tw`sw-rounded-pill`} + + &:hover { + background: ${themeColor('radioHover')}; + } + + &:focus, + &:focus-visible { + background: ${themeColor('radioHover')}; + border: ${themeBorder('default', 'radioFocusBorder')}; + outline: ${themeBorder('focus', 'radioFocusOutline')}; + } + + &:focus:checked, + &:focus-visible:checked, + &:hover:checked, + &:checked { + // Color cannot be used with multiple backgrounds, only image is allowed + background-image: linear-gradient(to right, ${themeColor('radio')}, ${themeColor('radio')}), + linear-gradient(to right, ${themeColor('radioChecked')}, ${themeColor('radioChecked')}); + background-clip: content-box, padding-box; + border: ${themeBorder('default', 'radioBorder')}; + } + + &:disabled { + background: ${themeColor('radioDisabledBackground')}; + border: ${themeBorder('default', 'radioDisabledBorder')}; + background-clip: unset; + + ${tw`sw-cursor-not-allowed`} + + &:checked { + background-image: linear-gradient( + to right, + ${themeColor('radioDisabled')}, + ${themeColor('radioDisabled')} + ), + linear-gradient( + to right, + ${themeColor('radioDisabledBackground')}, + ${themeColor('radioDisabledBackground')} + ); + background-clip: content-box, padding-box; + border: ${themeBorder('default', 'radioDisabledBorder')}; + } + } +`; diff --git a/server/sonar-web/design-system/src/components/SonarQubeLogo.tsx b/server/sonar-web/design-system/src/components/SonarQubeLogo.tsx new file mode 100644 index 00000000000..fcbe6b22798 --- /dev/null +++ b/server/sonar-web/design-system/src/components/SonarQubeLogo.tsx @@ -0,0 +1,50 @@ +/* + * 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'; + +const SonarQubeLogoSvg = styled.svg` + height: 40px; + width: 132px; +`; + +export function SonarQubeLogo() { + return ( + + + + + + + ); +} diff --git a/server/sonar-web/design-system/src/components/Text.tsx b/server/sonar-web/design-system/src/components/Text.tsx new file mode 100644 index 00000000000..277f5eca4b5 --- /dev/null +++ b/server/sonar-web/design-system/src/components/Text.tsx @@ -0,0 +1,62 @@ +/* + * 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 { themeColor, themeContrast } from '../helpers/theme'; + +interface MainTextProps { + match?: string; + name: string; +} + +export function SearchText({ match, name }: MainTextProps) { + return match ? ( + + ) : ( + {name} + ); +} + +export function TextMuted({ text }: { text: string }) { + return {text}; +} + +export const StyledText = styled.span` + ${tw`sw-inline-block`}; + ${tw`sw-truncate`}; + ${tw`sw-font-semibold`}; + ${tw`sw-max-w-abs-600`} + + mark { + ${tw`sw-inline-block`}; + + background: ${themeColor('searchHighlight')}; + color: ${themeContrast('searchHighlight')}; + } +`; + +const StyledMutedText = styled(StyledText)` + ${tw`sw-font-regular`}; + color: ${themeColor('dropdownMenuSubTitle')}; +`; diff --git a/server/sonar-web/design-system/src/components/Tooltip.tsx b/server/sonar-web/design-system/src/components/Tooltip.tsx new file mode 100644 index 00000000000..a298b7fdfd0 --- /dev/null +++ b/server/sonar-web/design-system/src/components/Tooltip.tsx @@ -0,0 +1,504 @@ +/* + * 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 { keyframes, ThemeContext } from '@emotion/react'; +import styled from '@emotion/styled'; +import classNames from 'classnames'; +import { throttle } from 'lodash'; +import React from 'react'; +import { createPortal, findDOMNode } from 'react-dom'; +import tw from 'twin.macro'; +import { THROTTLE_SCROLL_DELAY } from '../helpers/constants'; +import { + BasePlacement, + PLACEMENT_FLIP_MAP, + PopupPlacement, + popupPositioning, +} from '../helpers/positioning'; +import { themeColor, themeContrast } from '../helpers/theme'; + +const MILLISECONDS_IN_A_SECOND = 1000; + +export interface TooltipProps { + children: React.ReactElement<{}>; + mouseEnterDelay?: number; + mouseLeaveDelay?: number; + onHide?: VoidFunction; + onShow?: VoidFunction; + overlay: React.ReactNode; + placement?: BasePlacement; + visible?: boolean; +} + +interface Measurements { + height: number; + left: number; + leftFix: number; + top: number; + topFix: number; + width: number; +} + +interface OwnState { + flipped: boolean; + placement?: PopupPlacement; + visible: boolean; +} + +type State = OwnState & Partial; + +function isMeasured(state: State): state is OwnState & Measurements { + return state.height !== undefined; +} + +export default function Tooltip(props: TooltipProps) { + // overlay is a ReactNode, so it can be a boolean, `undefined` or `null` + // this allows to easily render a tooltip conditionally + // more generaly we avoid rendering empty tooltips + return props.overlay ? {props.children} : props.children; +} + +export class TooltipInner extends React.Component { + throttledPositionTooltip: VoidFunction; + mouseEnterTimeout?: number; + mouseLeaveTimeout?: number; + tooltipNode?: HTMLElement | null; + mounted = false; + mouseIn = false; + + static defaultProps = { + mouseEnterDelay: 0.1, + }; + + constructor(props: TooltipProps) { + super(props); + this.state = { + flipped: false, + placement: props.placement, + visible: props.visible !== undefined ? props.visible : false, + }; + this.throttledPositionTooltip = throttle(this.positionTooltip, THROTTLE_SCROLL_DELAY); + } + + componentDidMount() { + this.mounted = true; + if (this.props.visible === true) { + this.positionTooltip(); + this.addEventListeners(); + } + } + + componentDidUpdate(prevProps: TooltipProps, prevState: State) { + if (this.props.placement !== prevProps.placement) { + this.setState({ placement: this.props.placement }, () => + this.onUpdatePlacement(this.hasVisibleChanged(prevState.visible, prevProps.visible)) + ); + } else if (this.hasVisibleChanged(prevState.visible, prevProps.visible)) { + this.onUpdateVisible(); + } else if (!this.state.flipped && this.needsFlipping(this.state)) { + this.setState( + ({ placement = PopupPlacement.Bottom }) => ({ + flipped: true, + placement: PLACEMENT_FLIP_MAP[placement], + }), + () => { + if (this.state.visible) { + // Force a re-positioning, as "only" updating the state doesn't + // recompute the position, only re-renders with the previous + // position (which is no longer correct). + this.positionTooltip(); + } + } + ); + } + } + + componentWillUnmount() { + this.mounted = false; + this.removeEventListeners(); + this.clearTimeouts(); + } + + static contextType = ThemeContext; + + onUpdatePlacement = (visibleHasChanged: boolean) => { + this.setState({ placement: this.props.placement }, () => { + if (this.isVisible()) { + this.positionTooltip(); + if (visibleHasChanged) { + this.addEventListeners(); + } + } + }); + }; + + onUpdateVisible = () => { + if (this.isVisible()) { + this.positionTooltip(); + this.addEventListeners(); + } else { + this.clearPosition(); + this.removeEventListeners(); + } + }; + + addEventListeners = () => { + window.addEventListener('resize', this.throttledPositionTooltip); + window.addEventListener('scroll', this.throttledPositionTooltip); + }; + + removeEventListeners = () => { + window.removeEventListener('resize', this.throttledPositionTooltip); + window.removeEventListener('scroll', this.throttledPositionTooltip); + }; + + clearTimeouts = () => { + window.clearTimeout(this.mouseEnterTimeout); + window.clearTimeout(this.mouseLeaveTimeout); + }; + + hasVisibleChanged = (prevStateVisible: boolean, prevPropsVisible?: boolean) => { + if (this.props.visible === undefined) { + return prevPropsVisible || this.state.visible !== prevStateVisible; + } + return this.props.visible !== prevPropsVisible; + }; + + isVisible = () => { + return this.props.visible ?? this.state.visible; + }; + + getPlacement = (): PopupPlacement => { + return this.state.placement || PopupPlacement.Bottom; + }; + + tooltipNodeRef = (node: HTMLElement | null) => { + this.tooltipNode = node; + }; + + adjustArrowPosition = ( + placement: PopupPlacement, + { leftFix, topFix, height, width }: Measurements + ) => { + switch (placement) { + case PopupPlacement.Left: + case PopupPlacement.Right: + return { + marginTop: Math.max(0, Math.min(-topFix, height / 2 - ARROW_WIDTH * 2)), + }; + default: + return { + marginLeft: Math.max(0, Math.min(-leftFix, width / 2 - ARROW_WIDTH * 2)), + }; + } + }; + + positionTooltip = () => { + // `findDOMNode(this)` will search for the DOM node for the current component + // first it will find a React.Fragment (see `render`), + // so it will get the DOM node of the first child, i.e. DOM node of `this.props.children` + // docs: https://reactjs.org/docs/refs-and-the-dom.html#exposing-dom-refs-to-parent-components + + // eslint-disable-next-line react/no-find-dom-node + const toggleNode = findDOMNode(this); + if (toggleNode && toggleNode instanceof Element && this.tooltipNode) { + const { height, left, leftFix, top, topFix, width } = popupPositioning( + toggleNode, + this.tooltipNode, + this.getPlacement() + ); + + // save width and height (and later set in `render`) to avoid resizing the popup element, + // when it's placed close to the window edge + this.setState({ + left: window.scrollX + left, + leftFix, + top: window.scrollY + top, + topFix, + width, + height, + }); + } + }; + + clearPosition = () => { + this.setState({ + flipped: false, + left: undefined, + leftFix: undefined, + top: undefined, + topFix: undefined, + width: undefined, + height: undefined, + placement: this.props.placement, + }); + }; + + handlePointerEnter = () => { + this.mouseEnterTimeout = window.setTimeout(() => { + // for some reason even after the `this.mouseEnterTimeout` is cleared, it still triggers + // to workaround this issue, check that its value is not `undefined` + // (if it's `undefined`, it means the timer has been reset) + if ( + this.mounted && + this.props.visible === undefined && + this.mouseEnterTimeout !== undefined + ) { + this.setState({ visible: true }); + } + }, (this.props.mouseEnterDelay || 0) * MILLISECONDS_IN_A_SECOND); + + if (this.props.onShow) { + this.props.onShow(); + } + }; + + handlePointerLeave = () => { + if (this.mouseEnterTimeout !== undefined) { + window.clearTimeout(this.mouseEnterTimeout); + this.mouseEnterTimeout = undefined; + } + + if (!this.mouseIn) { + this.mouseLeaveTimeout = window.setTimeout(() => { + if (this.mounted && this.props.visible === undefined && !this.mouseIn) { + this.setState({ visible: false }); + } + }, (this.props.mouseLeaveDelay || 0) * MILLISECONDS_IN_A_SECOND); + + if (this.props.onHide) { + this.props.onHide(); + } + } + }; + + handleOverlayPointerEnter = () => { + this.mouseIn = true; + }; + + handleOverlayPointerLeave = () => { + this.mouseIn = false; + this.handlePointerLeave(); + }; + + handleChildPointerEnter = () => { + this.handlePointerEnter(); + + const { children } = this.props; + if (typeof children.props.onPointerEnter === 'function') { + children.props.onPointerEnter(); + } + }; + + handleChildPointerLeave = () => { + this.handlePointerLeave(); + + const { children } = this.props; + if (typeof children.props.onPointerLeave === 'function') { + children.props.onPointerLeave(); + } + }; + + needsFlipping = ({ leftFix, topFix }: State) => { + // We can live with a tooltip that's slightly positioned over the toggle + // node. Only trigger if it really starts overlapping, as the re-positioning + // is quite expensive, needing 2 re-renders. + const repositioningThreshold = 8; + switch (this.getPlacement()) { + case PopupPlacement.Left: + case PopupPlacement.Right: + return Boolean(leftFix && Math.abs(leftFix) > repositioningThreshold); + case PopupPlacement.Top: + case PopupPlacement.Bottom: + return Boolean(topFix && Math.abs(topFix) > repositioningThreshold); + default: + return false; + } + }; + + render() { + const placement = this.getPlacement(); + const style = isMeasured(this.state) + ? { + left: this.state.left, + top: this.state.top, + width: this.state.width, + height: this.state.height, + } + : undefined; + + return ( + <> + {React.cloneElement(this.props.children, { + onPointerEnter: this.handleChildPointerEnter, + onPointerLeave: this.handleChildPointerLeave, + })} + {this.isVisible() && ( + + + {this.props.overlay} + + + + )} + + ); + } +} + +class TooltipPortal extends React.Component { + el: HTMLElement; + + constructor(props: {}) { + super(props); + this.el = document.createElement('div'); + } + + componentDidMount() { + document.body.appendChild(this.el); + } + + componentWillUnmount() { + document.body.removeChild(this.el); + } + + render() { + return createPortal(this.props.children, this.el); + } +} + +const fadeIn = keyframes` + from { + opacity: 0; + } + + to { + opacity: 1; + } +`; + +const ARROW_WIDTH = 6; +const ARROW_HEIGHT = 7; +const ARROW_MARGIN = 3; + +export const TooltipWrapper = styled.div` + animation: ${fadeIn} 0.3s forwards; + + ${tw`sw-absolute`} + ${tw`sw-z-tooltip`}; + ${tw`sw-block`}; + ${tw`sw-box-border`}; + ${tw`sw-h-auto`}; + ${tw`sw-body-sm`}; + + &.top { + margin-top: -${ARROW_MARGIN}px; + padding: ${ARROW_HEIGHT}px 0; + } + + &.right { + margin-left: ${ARROW_MARGIN}px; + padding: 0 ${ARROW_HEIGHT}px; + } + + &.bottom { + margin-top: ${ARROW_MARGIN}px; + padding: ${ARROW_HEIGHT}px 0; + } + + &.left { + margin-left: -${ARROW_MARGIN}px; + padding: 0 ${ARROW_HEIGHT}px; + } +`; + +const TooltipWrapperArrow = styled.div` + ${tw`sw-absolute`}; + ${tw`sw-w-0`}; + ${tw`sw-h-0`}; + ${tw`sw-border-solid`}; + ${tw`sw-border-transparent`}; + ${TooltipWrapper}.top & { + border-width: ${ARROW_HEIGHT}px ${ARROW_WIDTH}px 0; + border-top-color: ${themeColor('tooltipBackground')}; + transform: translateX(-${ARROW_WIDTH}px); + + ${tw`sw-bottom-0`}; + ${tw`sw-left-1/2`}; + } + + ${TooltipWrapper}.right & { + border-width: ${ARROW_WIDTH}px ${ARROW_HEIGHT}px ${ARROW_WIDTH}px 0; + border-right-color: ${themeColor('tooltipBackground')}; + transform: translateY(-${ARROW_WIDTH}px); + + ${tw`sw-top-1/2`}; + ${tw`sw-left-0`}; + } + + ${TooltipWrapper}.left & { + border-width: ${ARROW_WIDTH}px 0 ${ARROW_WIDTH}px ${ARROW_HEIGHT}px; + border-left-color: ${themeColor('tooltipBackground')}; + transform: translateY(-${ARROW_WIDTH}px); + + ${tw`sw-top-1/2`}; + ${tw`sw-right-0`}; + } + + ${TooltipWrapper}.bottom & { + border-width: 0 ${ARROW_WIDTH}px ${ARROW_HEIGHT}px; + border-bottom-color: ${themeColor('tooltipBackground')}; + transform: translateX(-${ARROW_WIDTH}px); + + ${tw`sw-top-0`}; + ${tw`sw-left-1/2`}; + } +`; + +export const TooltipWrapperInner = styled.div` + color: ${themeContrast('tooltipBackground')}; + background-color: ${themeColor('tooltipBackground')}; + + ${tw`sw-max-w-[22rem]`} + ${tw`sw-py-3 sw-px-4`}; + ${tw`sw-overflow-hidden`}; + ${tw`sw-text-left`}; + ${tw`sw-no-underline`}; + ${tw`sw-break-words`}; + ${tw`sw-rounded-2`}; + + hr { + background-color: ${themeColor('tooltipSeparator')}; + + ${tw`sw-mx-4`}; + } +`; diff --git a/server/sonar-web/design-system/src/components/__tests__/Avatar-test.tsx b/server/sonar-web/design-system/src/components/__tests__/Avatar-test.tsx new file mode 100644 index 00000000000..d0aa180d0fa --- /dev/null +++ b/server/sonar-web/design-system/src/components/__tests__/Avatar-test.tsx @@ -0,0 +1,69 @@ +/* + * 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. + */ + +/* eslint-disable import/no-extraneous-dependencies */ + +import { fireEvent, screen } from '@testing-library/react'; +import { render } from '../../helpers/testUtils'; +import { FCProps } from '../../types/misc'; +import { Avatar } from '../Avatar'; + +const gravatarServerUrl = 'http://example.com/{EMAIL_MD5}.jpg?s={SIZE}'; + +it('should render avatar with border', () => { + setupWithProps({ border: true, hash: '7daf6c79d4802916d83f6266e24850af' }); + expect(screen.getByRole('img')).toHaveStyle('border: 1px solid rgb(225,230,243)'); +}); + +it('should be able to render with hash only', () => { + setupWithProps({ hash: '7daf6c79d4802916d83f6266e24850af' }); + expect(screen.getByRole('img')).toHaveAttribute( + 'src', + 'http://example.com/7daf6c79d4802916d83f6266e24850af.jpg?s=48' + ); +}); + +it('should fall back to generated on error', () => { + setupWithProps({ hash: '7daf6c79d4802916d83f6266e24850af' }); + fireEvent(screen.getByRole('img'), new Event('error')); + expect(screen.getByRole('img')).not.toHaveAttribute('src'); +}); + +it('should fall back to dummy avatar', () => { + setupWithProps({ enableGravatar: false }); + expect(screen.getByRole('img')).not.toHaveAttribute('src'); +}); + +it('should return null if no name is set', () => { + setupWithProps({ name: undefined }); + expect(screen.queryByRole('img')).not.toBeInTheDocument(); +}); + +it('should display organization avatar correctly', () => { + const avatar = 'http://example.com/avatar.png'; + setupWithProps({ organizationAvatar: avatar, organizationName: 'my-org' }); + expect(screen.getByRole('img')).toHaveAttribute('src', avatar); +}); + +function setupWithProps(props: Partial> = {}) { + return render( + + ); +} diff --git a/server/sonar-web/design-system/src/components/__tests__/DeferredSpinner-test.tsx b/server/sonar-web/design-system/src/components/__tests__/DeferredSpinner-test.tsx new file mode 100644 index 00000000000..d6b7c43d467 --- /dev/null +++ b/server/sonar-web/design-system/src/components/__tests__/DeferredSpinner-test.tsx @@ -0,0 +1,73 @@ +/* + * 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, screen } from '@testing-library/react'; +import * as React from 'react'; +import DeferredSpinner from '../DeferredSpinner'; + +beforeAll(() => { + jest.useFakeTimers(); +}); + +afterEach(() => { + jest.runOnlyPendingTimers(); +}); + +afterAll(() => { + jest.useRealTimers(); +}); + +it('renders children before timeout', () => { + renderDeferredSpinner({ children: foo }); + expect(screen.getByRole('link')).toBeInTheDocument(); + jest.runAllTimers(); + expect(screen.queryByRole('link')).not.toBeInTheDocument(); +}); + +it('renders spinner after timeout', () => { + renderDeferredSpinner(); + expect(screen.queryByLabelText('loading')).not.toBeInTheDocument(); + jest.runAllTimers(); + expect(screen.getByLabelText('loading')).toBeInTheDocument(); +}); + +it('allows setting a custom class name', () => { + renderDeferredSpinner({ className: 'foo' }); + jest.runAllTimers(); + expect(screen.getByLabelText('loading')).toHaveClass('foo'); +}); + +it('can be controlled by the loading prop', () => { + const { rerender } = renderDeferredSpinner({ loading: true }); + jest.runAllTimers(); + expect(screen.getByLabelText('loading')).toBeInTheDocument(); + + rerender(prepareDeferredSpinner({ loading: false })); + expect(screen.queryByLabelText('loading')).not.toBeInTheDocument(); +}); + +function renderDeferredSpinner(props: Partial = {}) { + // We don't use our renderComponent() helper here, as we have some tests that + // require changes in props. + return render(prepareDeferredSpinner(props)); +} + +function prepareDeferredSpinner(props: Partial = {}) { + return ; +} diff --git a/server/sonar-web/design-system/src/components/__tests__/Dropdown-test.tsx b/server/sonar-web/design-system/src/components/__tests__/Dropdown-test.tsx new file mode 100644 index 00000000000..52139a0489d --- /dev/null +++ b/server/sonar-web/design-system/src/components/__tests__/Dropdown-test.tsx @@ -0,0 +1,65 @@ +/* + * 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 { renderWithRouter } from '../../helpers/testUtils'; +import { ButtonSecondary } from '../buttons'; +import Dropdown, { ActionsDropdown } from '../Dropdown'; + +describe('Dropdown', () => { + it('renders', async () => { + const { user } = setupWithChildren(); + expect(screen.getByRole('button')).toBeInTheDocument(); + + await user.click(screen.getByRole('button')); + expect(screen.getByRole('menu')).toBeInTheDocument(); + }); + + it('toggles with render prop', async () => { + const { user } = setupWithChildren(({ onToggleClick }) => ( + + )); + + await user.click(screen.getByRole('button')); + expect(screen.getByRole('menu')).toBeVisible(); + }); + + function setupWithChildren(children?: Dropdown['props']['children']) { + return renderWithRouter( + }> + {children ?? } + + ); + } +}); + +describe('ActionsDropdown', () => { + it('renders', () => { + setup(); + expect(screen.getByRole('button')).toHaveAccessibleName('menu'); + }); + + function setup() { + return renderWithRouter( + +
    + + ); + } +}); diff --git a/server/sonar-web/design-system/src/components/__tests__/DropdownMenu-test.tsx b/server/sonar-web/design-system/src/components/__tests__/DropdownMenu-test.tsx new file mode 100644 index 00000000000..350c6874e22 --- /dev/null +++ b/server/sonar-web/design-system/src/components/__tests__/DropdownMenu-test.tsx @@ -0,0 +1,100 @@ +/* + * 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 { noop } from 'lodash'; +import { render, renderWithRouter } from '../../helpers/testUtils'; +import { + DropdownMenu, + ItemButton, + ItemCheckbox, + ItemCopy, + ItemDangerButton, + ItemDivider, + ItemHeader, + ItemLink, + ItemNavLink, + ItemRadioButton, +} from '../DropdownMenu'; +import MenuIcon from '../icons/MenuIcon'; +import Tooltip from '../Tooltip'; + +beforeEach(() => { + jest.useFakeTimers(); +}); + +afterEach(() => { + jest.runOnlyPendingTimers(); + jest.useRealTimers(); +}); + +it('should render a full menu correctly', () => { + renderDropdownMenu(); + expect(screen.getByRole('menuitem', { name: 'My header' })).toBeInTheDocument(); + expect(screen.getByRole('menuitem', { name: 'Test menu item' })).toBeInTheDocument(); + expect(screen.getByRole('menuitem', { name: 'Test disabled item' })).toHaveClass('disabled'); +}); + +it('menu items should work with tooltips', async () => { + const { user } = render( + + button + , + {}, + { delay: null } + ); + + expect(screen.queryByRole('tooltip')).not.toBeInTheDocument(); + + await user.hover(screen.getByRole('menuitem')); + expect(screen.queryByRole('tooltip')).not.toBeInTheDocument(); + + jest.runAllTimers(); + expect(screen.getByRole('tooltip')).toBeVisible(); + + await user.unhover(screen.getByRole('menuitem')); + expect(screen.getByRole('tooltip')).toBeVisible(); + + jest.runAllTimers(); + expect(screen.queryByRole('tooltip')).not.toBeInTheDocument(); +}); + +function renderDropdownMenu() { + return renderWithRouter( + + My header + Test menu item + + + Test disabled item + + } onClick={noop}> + Button + + DangerButton + Copy + + Checkbox item + + + Radio item + + + ); +} diff --git a/server/sonar-web/design-system/src/components/__tests__/GenericAvatar-test.tsx b/server/sonar-web/design-system/src/components/__tests__/GenericAvatar-test.tsx new file mode 100644 index 00000000000..83b7fdf6bc3 --- /dev/null +++ b/server/sonar-web/design-system/src/components/__tests__/GenericAvatar-test.tsx @@ -0,0 +1,51 @@ +/* + * 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 { GenericAvatar } from '../GenericAvatar'; +import { CustomIcon, IconProps } from '../icons/Icon'; + +function TestIcon(props: IconProps) { + return ( + + + + ); +} + +it('should render single word and size', () => { + render(); + const image = screen.getByRole('img'); + expect(image).toHaveAttribute('size', '15'); + expect(screen.getByText('F')).toBeInTheDocument(); +}); + +it('should render multiple word with default size', () => { + render(); + const image = screen.getByRole('img'); + expect(image).toHaveAttribute('size', '24'); + expect(screen.getByText('F')).toBeInTheDocument(); +}); + +it('should render without name', () => { + render(); + const image = screen.getByRole('img'); + expect(image).toHaveAttribute('size', '32'); +}); diff --git a/server/sonar-web/design-system/src/components/__tests__/InputSearch-test.tsx b/server/sonar-web/design-system/src/components/__tests__/InputSearch-test.tsx new file mode 100644 index 00000000000..1d9f6068e56 --- /dev/null +++ b/server/sonar-web/design-system/src/components/__tests__/InputSearch-test.tsx @@ -0,0 +1,90 @@ +/* + * 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, waitFor } from '@testing-library/react'; +import { render } from '../../helpers/testUtils'; +import { FCProps } from '../../types/misc'; +import InputSearch from '../InputSearch'; + +it('should warn when input is too short', async () => { + const { user } = setupWithProps({ value: 'f' }); + expect(screen.getByRole('note')).toBeInTheDocument(); + await user.type(screen.getByRole('searchbox'), 'oo'); + expect(screen.queryByRole('note')).not.toBeInTheDocument(); +}); + +it('should show clear button only when there is a value', async () => { + const { user } = setupWithProps({ value: 'f' }); + expect(screen.getByRole('button')).toBeInTheDocument(); + await user.clear(screen.getByRole('searchbox')); + expect(screen.queryByRole('button')).not.toBeInTheDocument(); +}); + +it('should attach ref', () => { + const ref = jest.fn(); + setupWithProps({ innerRef: ref }); + expect(ref).toHaveBeenCalled(); + expect(ref.mock.calls[0][0]).toBeInstanceOf(HTMLInputElement); +}); + +it('should trigger reset correctly with clear button', async () => { + const onChange = jest.fn(); + const { user } = setupWithProps({ onChange }); + await user.click(screen.getByRole('button')); + expect(onChange).toHaveBeenCalledWith(''); +}); + +it('should trigger change correctly', async () => { + const onChange = jest.fn(); + const { user } = setupWithProps({ onChange, value: 'f' }); + await user.type(screen.getByRole('searchbox'), 'oo'); + await waitFor(() => { + expect(onChange).toHaveBeenCalledWith('foo'); + }); +}); + +it('should not change when value is too short', async () => { + const onChange = jest.fn(); + const { user } = setupWithProps({ onChange, value: '', minLength: 3 }); + await user.type(screen.getByRole('searchbox'), 'fo'); + expect(onChange).not.toHaveBeenCalled(); +}); + +it('should clear input using escape', async () => { + const onChange = jest.fn(); + const { user } = setupWithProps({ onChange, value: 'foo' }); + await user.type(screen.getByRole('searchbox'), '{Escape}'); + expect(onChange).toHaveBeenCalledWith(''); +}); + +function setupWithProps(props: Partial> = {}) { + return render( + + ); +} diff --git a/server/sonar-web/design-system/src/components/__tests__/Link-test.tsx b/server/sonar-web/design-system/src/components/__tests__/Link-test.tsx new file mode 100644 index 00000000000..295469720f0 --- /dev/null +++ b/server/sonar-web/design-system/src/components/__tests__/Link-test.tsx @@ -0,0 +1,129 @@ +/* + * 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 React from 'react'; +import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom'; +import { render } from '../../helpers/testUtils'; +import Link, { DiscreetLink } from '../Link'; + +beforeAll(() => { + const { location } = window; + delete (window as any).location; + window.location = { ...location, href: '' }; +}); + +beforeEach(() => { + jest.clearAllMocks(); +}); + +// This functionality won't be needed once we update the breadcrumbs +it('should remove focus after link is clicked', async () => { + const { user } = setupWithMemoryRouter( + Icon
    } to="/initial" /> + ); + + await user.click(screen.getByRole('link')); + + expect(screen.getByRole('link')).not.toHaveFocus(); +}); + +it('should prevent default when preventDefault is true', async () => { + const { user } = setupWithMemoryRouter(); + + expect(screen.getByText('/initial')).toBeVisible(); + + await user.click(screen.getByRole('link')); + + // prevent default behavior of page navigation + expect(screen.getByText('/initial')).toBeVisible(); + expect(screen.queryByText('/second')).not.toBeInTheDocument(); +}); + +it('should stop propagation when stopPropagation is true', async () => { + const buttonOnClick = jest.fn(); + + const { user } = setupWithMemoryRouter( + + ); + + await user.click(screen.getByRole('link')); + + expect(buttonOnClick).not.toHaveBeenCalled(); +}); + +it('should call onClick when one is passed', async () => { + const onClick = jest.fn(); + const { user } = setupWithMemoryRouter( + + ); + + await user.click(screen.getByRole('link')); + + expect(onClick).toHaveBeenCalled(); +}); + +it('internal link should be clickable', async () => { + const { user } = setupWithMemoryRouter(internal link); + expect(screen.getByRole('link')).toBeVisible(); + + await user.click(screen.getByRole('link')); + + expect(screen.getByText('/second')).toBeVisible(); +}); + +it('external links are indicated by OpenNewTabIcon', () => { + setupWithMemoryRouter(external link); + expect(screen.getByRole('link')).toBeVisible(); + + expect(screen.getByRole('img', { hidden: true })).toBeInTheDocument(); +}); + +it('discreet links also can be external indicated by the OpenNewTabIcon', () => { + setupWithMemoryRouter(external link); + expect(screen.getByRole('link')).toBeVisible(); + + expect(screen.getByRole('img', { hidden: true })).toBeInTheDocument(); +}); + +function ShowPath() { + const { pathname } = useLocation(); + return
    {pathname}
    ; +} + +const setupWithMemoryRouter = (component: JSX.Element, initialEntries = ['/initial']) => { + return render( + + + + {component} + + + } + path="/initial" + /> + } path="/second" /> + + + ); +}; diff --git a/server/sonar-web/design-system/src/components/__tests__/MainAppBar-test.tsx b/server/sonar-web/design-system/src/components/__tests__/MainAppBar-test.tsx new file mode 100644 index 00000000000..fdc66f2a441 --- /dev/null +++ b/server/sonar-web/design-system/src/components/__tests__/MainAppBar-test.tsx @@ -0,0 +1,54 @@ +/* + * 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. + */ + +/* eslint-disable import/no-extraneous-dependencies */ + +import { screen } from '@testing-library/react'; +import { LAYOUT_LOGO_MAX_HEIGHT, LAYOUT_LOGO_MAX_WIDTH } from '../../helpers/constants'; +import { render } from '../../helpers/testUtils'; +import { FCProps } from '../../types/misc'; +import { MainAppBar } from '../MainAppBar'; +import { SonarQubeLogo } from '../SonarQubeLogo'; + +it('should render the main app bar with max-height and max-width constraints on the logo', () => { + setupWithProps(); + + expect(screen.getByRole('img')).toHaveStyle({ + border: 'none', + 'max-height': `${LAYOUT_LOGO_MAX_HEIGHT}px`, + 'max-width': `${LAYOUT_LOGO_MAX_WIDTH}px`, + 'object-fit': 'contain', + }); +}); + +it('should render the logo', () => { + const element = setupWithProps({ Logo: SonarQubeLogo }); + + // eslint-disable-next-line testing-library/no-node-access + expect(element.container.querySelector('svg')).toHaveStyle({ height: '40px', width: '132px' }); +}); + +function setupWithProps( + props: FCProps = { + Logo: () => logo, + } +) { + return render(); +} diff --git a/server/sonar-web/design-system/src/components/__tests__/MainMenuItem-test.tsx b/server/sonar-web/design-system/src/components/__tests__/MainMenuItem-test.tsx new file mode 100644 index 00000000000..b3120afbe71 --- /dev/null +++ b/server/sonar-web/design-system/src/components/__tests__/MainMenuItem-test.tsx @@ -0,0 +1,64 @@ +/* + * 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. + */ + +/* eslint-disable import/no-extraneous-dependencies */ + +import { screen } from '@testing-library/react'; +import { render } from '../../helpers/testUtils'; +import { MainMenuItem } from '../MainMenuItem'; + +it('should render default', () => { + render( + + Hi + + ); + + expect(screen.getByText('Hi')).toHaveStyle({ + color: 'rgb(62, 67, 87)', + 'border-bottom': '3px solid transparent', + }); +}); + +it('should render active link', () => { + render( + + Hi + + ); + + expect(screen.getByText('Hi')).toHaveStyle({ + color: 'rgb(62, 67, 87)', + 'border-bottom': '3px solid rgba(123,135,217,1)', + }); +}); + +it('should render hovered link', () => { + render( + + Hi + + ); + + expect(screen.getByText('Hi')).toHaveStyle({ + color: 'rgb(42, 47, 64)', + 'border-bottom': '3px solid rgba(123,135,217,1)', + }); +}); diff --git a/server/sonar-web/design-system/src/components/__tests__/NavLink-test.tsx b/server/sonar-web/design-system/src/components/__tests__/NavLink-test.tsx new file mode 100644 index 00000000000..548cfb6c238 --- /dev/null +++ b/server/sonar-web/design-system/src/components/__tests__/NavLink-test.tsx @@ -0,0 +1,112 @@ +/* + * 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 React from 'react'; +import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom'; +import { render } from '../../helpers/testUtils'; +import NavLink from '../NavLink'; + +beforeAll(() => { + const { location } = window; + delete (window as any).location; + window.location = { ...location, href: '' }; +}); + +beforeEach(() => { + jest.clearAllMocks(); +}); + +it('should remove focus after link is clicked', async () => { + const { user } = setupWithMemoryRouter(); + + await user.click(screen.getByRole('link')); + + expect(screen.getByRole('link')).not.toHaveFocus(); +}); + +it('should prevent default when preventDefault is true', async () => { + const { user } = setupWithMemoryRouter(); + + expect(screen.getByText('/initial')).toBeVisible(); + + await user.click(screen.getByRole('link')); + + // prevent default behavior of page navigation + expect(screen.getByText('/initial')).toBeVisible(); + expect(screen.queryByText('/second')).not.toBeInTheDocument(); +}); + +it('should stop propagation when stopPropagation is true', async () => { + const buttonOnClick = jest.fn(); + + const { user } = setupWithMemoryRouter( + + ); + + await user.click(screen.getByRole('link')); + + expect(buttonOnClick).not.toHaveBeenCalled(); +}); + +it('should call onClick when one is passed', async () => { + const onClick = jest.fn(); + const { user } = setupWithMemoryRouter( + + ); + + await user.click(screen.getByRole('link')); + + expect(onClick).toHaveBeenCalled(); +}); + +it('NavLink should be clickable', async () => { + const { user } = setupWithMemoryRouter(internal link); + expect(screen.getByRole('link')).toBeVisible(); + + await user.click(screen.getByRole('link')); + + expect(screen.getByText('/second')).toBeVisible(); +}); + +function ShowPath() { + const { pathname } = useLocation(); + return
    {pathname}
    ; +} + +const setupWithMemoryRouter = (component: JSX.Element, initialEntries = ['/initial']) => { + return render( + + + + {component} + + + } + path="/initial" + /> + } path="/second" /> + + + ); +}; diff --git a/server/sonar-web/design-system/src/components/__tests__/Text-test.tsx b/server/sonar-web/design-system/src/components/__tests__/Text-test.tsx new file mode 100644 index 00000000000..5743a92a7b0 --- /dev/null +++ b/server/sonar-web/design-system/src/components/__tests__/Text-test.tsx @@ -0,0 +1,41 @@ +/* + * 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. + */ + +/* eslint-disable import/no-extraneous-dependencies */ + +import { screen } from '@testing-library/react'; +import { render } from '../../helpers/testUtils'; +import { SearchText, TextMuted } from '../Text'; + +it('should render SearchText', () => { + render(); + + expect(screen.getByText('hi')).toHaveStyle({ + 'font-weight': '600', + }); +}); + +it('should render TextMuted', () => { + render(); + + expect(screen.getByText('Hi')).toHaveStyle({ + color: 'rgb(106, 117, 144)', + }); +}); diff --git a/server/sonar-web/design-system/src/components/__tests__/Tooltip-test.tsx b/server/sonar-web/design-system/src/components/__tests__/Tooltip-test.tsx new file mode 100644 index 00000000000..8b448d17521 --- /dev/null +++ b/server/sonar-web/design-system/src/components/__tests__/Tooltip-test.tsx @@ -0,0 +1,126 @@ +/* + * 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 { FCProps } from '../../types/misc'; +import Tooltip, { TooltipInner } from '../Tooltip'; + +jest.mock('react-dom', () => { + const reactDom = jest.requireActual('react-dom'); + return { ...reactDom, findDOMNode: jest.fn().mockReturnValue(undefined) }; +}); + +describe('TooltipInner', () => { + it('should open & close', async () => { + const onShow = jest.fn(); + const onHide = jest.fn(); + const { user } = setupWithProps({ onHide, onShow }); + + await user.hover(screen.getByRole('note')); + expect(await screen.findByRole('tooltip')).toBeInTheDocument(); + expect(onShow).toHaveBeenCalled(); + + await user.unhover(screen.getByRole('note')); + expect(screen.queryByRole('tooltip')).not.toBeInTheDocument(); + expect(onHide).toHaveBeenCalled(); + }); + + it('should not shadow children pointer events', async () => { + const onShow = jest.fn(); + const onHide = jest.fn(); + const onPointerEnter = jest.fn(); + const onPointerLeave = jest.fn(); + const { user } = setupWithProps( + { onHide, onShow }, +
    + ); + + await user.hover(screen.getByRole('note')); + expect(await screen.findByRole('tooltip')).toBeInTheDocument(); + expect(onShow).toHaveBeenCalled(); + expect(onPointerEnter).toHaveBeenCalled(); + + await user.unhover(screen.getByRole('note')); + expect(screen.queryByRole('tooltip')).not.toBeInTheDocument(); + expect(onHide).toHaveBeenCalled(); + expect(onPointerLeave).toHaveBeenCalled(); + }); + + it('should not open when mouse goes away quickly', async () => { + const { user } = setupWithProps(); + + await user.hover(screen.getByRole('note')); + await user.unhover(screen.getByRole('note')); + + expect(screen.queryByRole('tooltip')).not.toBeInTheDocument(); + }); + + it('should position the tooltip correctly', async () => { + const onShow = jest.fn(); + const onHide = jest.fn(); + const { user } = setupWithProps({ onHide, onShow }); + + await user.hover(screen.getByRole('note')); + expect(await screen.findByRole('tooltip')).toBeInTheDocument(); + expect(screen.getByRole('tooltip')).toHaveClass('bottom'); + }); + + function setupWithProps( + props: Partial = {}, + children =
    + ) { + return render( + } {...props}> + {children} + + ); + } +}); + +describe('Tooltip', () => { + it('should not render tooltip without overlay', async () => { + const { user } = setupWithProps({ overlay: undefined }); + await user.hover(screen.getByRole('note')); + expect(screen.queryByRole('tooltip')).not.toBeInTheDocument(); + }); + + it('should not render undefined tooltips', async () => { + const { user } = setupWithProps({ overlay: undefined, visible: true }); + await user.hover(screen.getByRole('note')); + expect(screen.queryByRole('tooltip')).not.toBeInTheDocument(); + }); + + it('should not render empty tooltips', async () => { + const { user } = setupWithProps({ overlay: '', visible: true }); + await user.hover(screen.getByRole('note')); + expect(screen.queryByRole('tooltip')).not.toBeInTheDocument(); + }); + + function setupWithProps( + props: Partial> = {}, + children =
    + ) { + return render( + } {...props}> + {children} + + ); + } +}); diff --git a/server/sonar-web/design-system/src/components/__tests__/clipboard-test.tsx b/server/sonar-web/design-system/src/components/__tests__/clipboard-test.tsx new file mode 100644 index 00000000000..84a44f103b1 --- /dev/null +++ b/server/sonar-web/design-system/src/components/__tests__/clipboard-test.tsx @@ -0,0 +1,69 @@ +/* + * 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, waitForElementToBeRemoved } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { renderWithContext } from '../../helpers/testUtils'; +import { ClipboardButton, ClipboardIconButton } from '../clipboard'; + +beforeEach(() => { + jest.useFakeTimers(); +}); + +afterEach(() => { + jest.runOnlyPendingTimers(); + jest.useRealTimers(); +}); + +describe('ClipboardButton', () => { + it('should display correctly', async () => { + /* Delay: null is necessary to play well with fake timers + * https://github.com/testing-library/user-event/issues/833 + */ + const user = userEvent.setup({ delay: null }); + renderClipboardButton(); + + expect(screen.getByRole('button', { name: 'copy' })).toBeInTheDocument(); + + await user.click(screen.getByRole('button', { name: 'copy' })); + + expect(await screen.findByText('copied_action')).toBeVisible(); + + await waitForElementToBeRemoved(() => screen.queryByText('copied_action')); + jest.runAllTimers(); + }); + + it('should render a custom label if provided', () => { + renderClipboardButton('Foo Bar'); + expect(screen.getByRole('button', { name: 'Foo Bar' })).toBeInTheDocument(); + }); + + function renderClipboardButton(children?: React.ReactNode) { + renderWithContext({children}); + } +}); + +describe('ClipboardIconButton', () => { + it('should display correctly', () => { + renderWithContext(); + + const copyButton = screen.getByRole('button', { name: 'copy_to_clipboard' }); + expect(copyButton).toBeInTheDocument(); + }); +}); diff --git a/server/sonar-web/design-system/src/components/buttons.tsx b/server/sonar-web/design-system/src/components/buttons.tsx new file mode 100644 index 00000000000..442026354ea --- /dev/null +++ b/server/sonar-web/design-system/src/components/buttons.tsx @@ -0,0 +1,219 @@ +/* + * 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 { css } from '@emotion/react'; +import styled from '@emotion/styled'; +import React from 'react'; +import tw from 'twin.macro'; +import { themeBorder, themeColor, themeContrast } from '../helpers/theme'; +import { ThemedProps } from '../types/theme'; +import { BaseLink, LinkProps } from './Link'; + +type AllowedButtonAttributes = Pick< + React.ButtonHTMLAttributes, + 'aria-label' | 'autoFocus' | 'id' | 'name' | 'style' | 'title' | 'type' +>; + +export interface ButtonProps extends AllowedButtonAttributes { + children?: React.ReactNode; + className?: string; + disabled?: boolean; + icon?: React.ReactNode; + innerRef?: React.Ref; + onClick?: VoidFunction; + + preventDefault?: boolean; + reloadDocument?: LinkProps['reloadDocument']; + stopPropagation?: boolean; + target?: LinkProps['target']; + to?: LinkProps['to']; +} + +class Button extends React.PureComponent { + handleClick = (event: React.MouseEvent) => { + const { disabled, onClick, stopPropagation = false, type } = this.props; + const { preventDefault = type !== 'submit' } = this.props; + + event.currentTarget.blur(); + + if (preventDefault || disabled) { + event.preventDefault(); + } + + if (stopPropagation) { + event.stopPropagation(); + } + + if (onClick && !disabled) { + onClick(); + } + }; + + render() { + const { + children, + disabled, + icon, + innerRef, + onClick, + preventDefault, + stopPropagation, + to, + type = 'button', + ...htmlProps + } = this.props; + + const props = { + ...htmlProps, + 'aria-disabled': disabled, + disabled, + type, + }; + + if (to) { + return ( + + {icon} + {children} + + ); + } + + return ( + + {icon} + {children} + + ); + } +} + +const buttonStyle = (props: ThemedProps) => css` + box-sizing: border-box; + text-decoration: none; + outline: none; + border: var(--border); + color: var(--color); + background-color: var(--background); + transition: background-color 0.2s ease, outline 0.2s ease; + + ${tw`sw-inline-flex sw-items-center`} + ${tw`sw-h-control`} + ${tw`sw-body-sm-highlight`} + ${tw`sw-py-2 sw-px-4`} + ${tw`sw-rounded-2`} + ${tw`sw-cursor-pointer`} + + &:hover { + color: var(--color); + background-color: var(--backgroundHover); + } + + &:focus, + &:active { + color: var(--color); + outline: ${themeBorder('focus', 'var(--focus)')(props)}; + } + + &:disabled, + &:disabled:hover { + color: ${themeContrast('buttonDisabled')(props)}; + background-color: ${themeColor('buttonDisabled')(props)}; + border: ${themeBorder('default', 'buttonDisabledBorder')(props)}; + + ${tw`sw-cursor-not-allowed`} + } + + & > svg { + ${tw`sw-mr-1`} + } +`; + +const BaseButtonLink = styled(BaseLink)` + ${buttonStyle} +`; + +const BaseButton = styled.button` + ${buttonStyle} + + /* Workaround for tooltips issue with onMouseLeave in disabled buttons: https://github.com/facebook/react/issues/4251 */ + & [disabled] { + ${tw`sw-pointer-events-none`}; + } +`; + +export const ButtonPrimary: React.FC = styled(Button)` + --background: ${themeColor('button')}; + --backgroundHover: ${themeColor('buttonHover')}; + --color: ${themeContrast('primary')}; + --focus: ${themeColor('button', 0.2)}; + --border: ${themeBorder('default', 'transparent')}; +`; + +export const ButtonSecondary: React.FC = styled(Button)` + --background: ${themeColor('buttonSecondary')}; + --backgroundHover: ${themeColor('buttonSecondaryHover')}; + --color: ${themeContrast('buttonSecondary')}; + --focus: ${themeColor('buttonSecondaryBorder', 0.2)}; + --border: ${themeBorder('default', 'buttonSecondaryBorder')}; +`; + +export const DangerButtonPrimary: React.FC = styled(Button)` + --background: ${themeColor('dangerButton')}; + --backgroundHover: ${themeColor('dangerButtonHover')}; + --color: ${themeContrast('dangerButton')}; + --focus: ${themeColor('dangerButtonFocus', 0.2)}; + --border: ${themeBorder('default', 'transparent')}; +`; + +export const DangerButtonSecondary: React.FC = styled(Button)` + --background: ${themeColor('dangerButtonSecondary')}; + --backgroundHover: ${themeColor('dangerButtonSecondaryHover')}; + --color: ${themeContrast('dangerButtonSecondary')}; + --focus: ${themeColor('dangerButtonSecondaryFocus', 0.2)}; + --border: ${themeBorder('default', 'dangerButtonSecondaryBorder')}; +`; + +interface ThirdPartyProps extends Omit { + iconPath: string; + name: string; +} + +export function ThirdPartyButton({ children, iconPath, name, ...buttonProps }: ThirdPartyProps) { + const size = 16; + return ( + + {name} + {children} + + ); +} + +const ThirdPartyButtonStyled: React.FC = styled(Button)` + --background: ${themeColor('thirdPartyButton')}; + --backgroundHover: ${themeColor('thirdPartyButtonHover')}; + --color: ${themeContrast('thirdPartyButton')}; + --focus: ${themeColor('thirdPartyButtonBorder', 0.2)}; + --border: ${themeBorder('default', 'thirdPartyButtonBorder')}; +`; + +export const BareButton = styled.button` + all: unset; + cursor: pointer; +`; diff --git a/server/sonar-web/design-system/src/components/clipboard.tsx b/server/sonar-web/design-system/src/components/clipboard.tsx new file mode 100644 index 00000000000..ea05963f772 --- /dev/null +++ b/server/sonar-web/design-system/src/components/clipboard.tsx @@ -0,0 +1,170 @@ +/* + * 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 classNames from 'classnames'; +import Clipboard from 'clipboard'; +import React from 'react'; +import { INTERACTIVE_TOOLTIP_DELAY } from '../helpers/constants'; +import { translate } from '../helpers/l10n'; +import { ButtonSecondary } from './buttons'; +import CopyIcon from './icons/CopyIcon'; +import { IconProps } from './icons/Icon'; +import { DiscreetInteractiveIcon, InteractiveIcon, InteractiveIconSize } from './InteractiveIcon'; +import Tooltip from './Tooltip'; + +const COPY_SUCCESS_NOTIFICATION_LIFESPAN = 1000; + +export interface State { + copySuccess: boolean; +} + +interface RenderProps { + copySuccess: boolean; + setCopyButton: (node: HTMLElement | null) => void; +} + +interface BaseProps { + children: (props: RenderProps) => React.ReactNode; +} + +export class ClipboardBase extends React.PureComponent { + private clipboard?: Clipboard; + private copyButton?: HTMLElement | null; + mounted = false; + state: State = { copySuccess: false }; + + componentDidMount() { + this.mounted = true; + if (this.copyButton) { + this.clipboard = new Clipboard(this.copyButton); + this.clipboard.on('success', this.handleSuccessCopy); + } + } + + componentDidUpdate() { + if (this.clipboard) { + this.clipboard.destroy(); + } + if (this.copyButton) { + this.clipboard = new Clipboard(this.copyButton); + this.clipboard.on('success', this.handleSuccessCopy); + } + } + + componentWillUnmount() { + this.mounted = false; + if (this.clipboard) { + this.clipboard.destroy(); + } + } + + setCopyButton = (node: HTMLElement | null) => { + this.copyButton = node; + }; + + handleSuccessCopy = () => { + if (this.mounted) { + this.setState({ copySuccess: true }); + setTimeout(() => { + if (this.mounted) { + this.setState({ copySuccess: false }); + } + }, COPY_SUCCESS_NOTIFICATION_LIFESPAN); + } + }; + + render() { + return this.props.children({ + setCopyButton: this.setCopyButton, + copySuccess: this.state.copySuccess, + }); + } +} + +interface ButtonProps { + children?: React.ReactNode; + className?: string; + copyValue: string; + icon?: React.ReactNode; +} + +export function ClipboardButton({ + icon = , + className, + children, + copyValue, +}: ButtonProps) { + return ( + + {({ setCopyButton, copySuccess }) => ( + + + {children || translate('copy')} + + + )} + + ); +} + +interface IconButtonProps { + Icon?: React.ComponentType; + 'aria-label'?: string; + className?: string; + copyValue: string; + discreet?: boolean; + size?: InteractiveIconSize; +} + +export function ClipboardIconButton(props: IconButtonProps) { + const { className, copyValue, discreet, size = 'small', Icon = CopyIcon } = props; + const InteractiveIconComponent = discreet ? DiscreetInteractiveIcon : InteractiveIcon; + + return ( + + {({ setCopyButton, copySuccess }) => { + return ( + + {translate(copySuccess ? 'copied_action' : 'copy_to_clipboard')} +
    + } + {...(copySuccess ? { visible: copySuccess } : undefined)} + > + + + ); + }} + + ); +} diff --git a/server/sonar-web/design-system/src/components/icons/CheckIcon.tsx b/server/sonar-web/design-system/src/components/icons/CheckIcon.tsx new file mode 100644 index 00000000000..dff5e8b4455 --- /dev/null +++ b/server/sonar-web/design-system/src/components/icons/CheckIcon.tsx @@ -0,0 +1,36 @@ +/* + * 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 { useTheme } from '@emotion/react'; +import { themeColor } from '../../helpers/theme'; +import { CustomIcon, IconProps } from './Icon'; + +export default function CheckIcon({ fill = 'iconCheck', ...iconProps }: IconProps) { + const theme = useTheme(); + return ( + + + + ); +} diff --git a/server/sonar-web/design-system/src/components/icons/ClockIcon.tsx b/server/sonar-web/design-system/src/components/icons/ClockIcon.tsx new file mode 100644 index 00000000000..15f81c7a302 --- /dev/null +++ b/server/sonar-web/design-system/src/components/icons/ClockIcon.tsx @@ -0,0 +1,23 @@ +/* + * 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 { ClockIcon } from '@primer/octicons-react'; +import { OcticonHoc } from './Icon'; + +export default OcticonHoc(ClockIcon); diff --git a/server/sonar-web/design-system/src/components/icons/CloseIcon.tsx b/server/sonar-web/design-system/src/components/icons/CloseIcon.tsx new file mode 100644 index 00000000000..79fb0888398 --- /dev/null +++ b/server/sonar-web/design-system/src/components/icons/CloseIcon.tsx @@ -0,0 +1,23 @@ +/* + * 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 { XIcon } from '@primer/octicons-react'; +import { OcticonHoc } from './Icon'; + +export default OcticonHoc(XIcon, 'CloseIcon'); diff --git a/server/sonar-web/design-system/src/components/icons/CopyIcon.tsx b/server/sonar-web/design-system/src/components/icons/CopyIcon.tsx new file mode 100644 index 00000000000..e9f12579961 --- /dev/null +++ b/server/sonar-web/design-system/src/components/icons/CopyIcon.tsx @@ -0,0 +1,23 @@ +/* + * 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 { CopyIcon } from '@primer/octicons-react'; +import { OcticonHoc } from './Icon'; + +export default OcticonHoc(CopyIcon); diff --git a/server/sonar-web/design-system/src/components/icons/Icon.tsx b/server/sonar-web/design-system/src/components/icons/Icon.tsx new file mode 100644 index 00000000000..0603fe83cfe --- /dev/null +++ b/server/sonar-web/design-system/src/components/icons/Icon.tsx @@ -0,0 +1,86 @@ +/* + * 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 { useTheme } from '@emotion/react'; +import { OcticonProps } from '@primer/octicons-react'; +import React from 'react'; +import { theme } from 'twin.macro'; +import { themeColor } from '../../helpers/theme'; +import { CSSColor, ThemeColors } from '../../types/theme'; + +interface Props { + 'aria-label'?: string; + children: React.ReactNode; + className?: string; +} + +export interface IconProps extends Omit { + fill?: ThemeColors | CSSColor; +} + +export function CustomIcon(props: Props) { + const { 'aria-label': ariaLabel, children, className, ...iconProps } = props; + return ( + + {children} + + ); +} + +export function OcticonHoc( + WrappedOcticon: React.ComponentType, + displayName?: string +): React.ComponentType { + function IconWrapper({ fill, ...props }: IconProps) { + const theme = useTheme(); + return ( + + ); + } + + IconWrapper.displayName = displayName || WrappedOcticon.displayName || WrappedOcticon.name; + return IconWrapper; +} diff --git a/server/sonar-web/design-system/src/components/icons/MenuHelpIcon.tsx b/server/sonar-web/design-system/src/components/icons/MenuHelpIcon.tsx new file mode 100644 index 00000000000..5fcebecdf93 --- /dev/null +++ b/server/sonar-web/design-system/src/components/icons/MenuHelpIcon.tsx @@ -0,0 +1,36 @@ +/* + * 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 { useTheme } from '@emotion/react'; +import { themeColor } from '../../helpers/theme'; +import { CustomIcon, IconProps } from './Icon'; + +export default function MenuHelpIcon({ fill = 'currentColor', ...iconProps }: IconProps) { + const theme = useTheme(); + return ( + + + + ); +} diff --git a/server/sonar-web/design-system/src/components/icons/MenuIcon.tsx b/server/sonar-web/design-system/src/components/icons/MenuIcon.tsx new file mode 100644 index 00000000000..ea30d7ddf9a --- /dev/null +++ b/server/sonar-web/design-system/src/components/icons/MenuIcon.tsx @@ -0,0 +1,29 @@ +/* + * 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 { KebabHorizontalIcon } from '@primer/octicons-react'; +import { OcticonHoc } from './Icon'; + +const MenuIcon = styled(OcticonHoc(KebabHorizontalIcon))` + transform: rotate(90deg); +`; + +MenuIcon.displayName = 'MenuIcon'; +export default MenuIcon; diff --git a/server/sonar-web/design-system/src/components/icons/MenuSearchIcon.tsx b/server/sonar-web/design-system/src/components/icons/MenuSearchIcon.tsx new file mode 100644 index 00000000000..a09077285e8 --- /dev/null +++ b/server/sonar-web/design-system/src/components/icons/MenuSearchIcon.tsx @@ -0,0 +1,37 @@ +/* + * 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 { useTheme } from '@emotion/react'; +import { themeColor } from '../../helpers/theme'; +import { CustomIcon, IconProps } from './Icon'; + +export default function MenuSearchIcon({ fill = 'currentColor', ...iconProps }: IconProps) { + const theme = useTheme(); + + return ( + + + + ); +} diff --git a/server/sonar-web/design-system/src/components/icons/OpenNewTabIcon.tsx b/server/sonar-web/design-system/src/components/icons/OpenNewTabIcon.tsx new file mode 100644 index 00000000000..f856c0ce7ee --- /dev/null +++ b/server/sonar-web/design-system/src/components/icons/OpenNewTabIcon.tsx @@ -0,0 +1,23 @@ +/* + * 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 { LinkExternalIcon } from '@primer/octicons-react'; +import { OcticonHoc } from './Icon'; + +export default OcticonHoc(LinkExternalIcon, 'OpenNewTabIcon'); diff --git a/server/sonar-web/design-system/src/components/icons/SearchIcon.tsx b/server/sonar-web/design-system/src/components/icons/SearchIcon.tsx new file mode 100644 index 00000000000..674ac699a6e --- /dev/null +++ b/server/sonar-web/design-system/src/components/icons/SearchIcon.tsx @@ -0,0 +1,23 @@ +/* + * 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 { SearchIcon } from '@primer/octicons-react'; +import { OcticonHoc } from './Icon'; + +export default OcticonHoc(SearchIcon); diff --git a/server/sonar-web/design-system/src/components/icons/StarIcon.tsx b/server/sonar-web/design-system/src/components/icons/StarIcon.tsx new file mode 100644 index 00000000000..f83c9a340a5 --- /dev/null +++ b/server/sonar-web/design-system/src/components/icons/StarIcon.tsx @@ -0,0 +1,23 @@ +/* + * 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 { StarIcon } from '@primer/octicons-react'; +import { OcticonHoc } from './Icon'; + +export default OcticonHoc(StarIcon); diff --git a/server/sonar-web/design-system/src/components/icons/__tests__/Icon-test.tsx b/server/sonar-web/design-system/src/components/icons/__tests__/Icon-test.tsx new file mode 100644 index 00000000000..4d25af63048 --- /dev/null +++ b/server/sonar-web/design-system/src/components/icons/__tests__/Icon-test.tsx @@ -0,0 +1,54 @@ +/* + * 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 { CheckIcon } from '@primer/octicons-react'; +import { screen } from '@testing-library/react'; +import { render } from '../../../helpers/testUtils'; +import { CustomIcon, OcticonHoc } from '../Icon'; + +it('should render custom icon correctly', () => { + render( + + + + ); + + expect(screen.queryByRole('img')).not.toBeInTheDocument(); + expect(screen.getByRole('img', { hidden: true })).toContainHTML(''); +}); + +it('should not be hidden when aria-label is set', () => { + render( + + + + ); + + expect(screen.getByRole('img')).toBeVisible(); +}); + +describe('Octicon HOC', () => { + it('should render correctly', () => { + const Wrapped = OcticonHoc(CheckIcon, 'TestIcon'); + + render(); + + expect(screen.getByRole('img')).toBeVisible(); + }); +}); diff --git a/server/sonar-web/design-system/src/components/icons/index.ts b/server/sonar-web/design-system/src/components/icons/index.ts new file mode 100644 index 00000000000..8b30b791711 --- /dev/null +++ b/server/sonar-web/design-system/src/components/icons/index.ts @@ -0,0 +1,24 @@ +/* + * 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. + */ +export { default as ClockIcon } from './ClockIcon'; +export { default as MenuHelpIcon } from './MenuHelpIcon'; +export { default as MenuSearchIcon } from './MenuSearchIcon'; +export { default as OpenNewTabIcon } from './OpenNewTabIcon'; +export { default as StarIcon } from './StarIcon'; diff --git a/server/sonar-web/design-system/src/components/index.ts b/server/sonar-web/design-system/src/components/index.ts index a96434d2ea2..e7bdcf4ca80 100644 --- a/server/sonar-web/design-system/src/components/index.ts +++ b/server/sonar-web/design-system/src/components/index.ts @@ -18,4 +18,21 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -export * from './DummyComponent'; +export * from './Avatar'; +export * from './buttons'; +export { default as DeferredSpinner } from './DeferredSpinner'; +export { default as Dropdown } from './Dropdown'; +export * from './DropdownMenu'; +export { default as DropdownToggler } from './DropdownToggler'; +export * from './GenericAvatar'; +export * from './icons'; +export { default as InputSearch } from './InputSearch'; +export * from './InteractiveIcon'; +export { default as Link } from './Link'; +export * from './MainAppBar'; +export * from './MainMenu'; +export { MainMenuItem } from './MainMenuItem'; +export * from './popups'; +export * from './SonarQubeLogo'; +export * from './Text'; +export { default as Tooltip } from './Tooltip'; diff --git a/server/sonar-web/design-system/src/components/popups.tsx b/server/sonar-web/design-system/src/components/popups.tsx new file mode 100644 index 00000000000..e517ceb7f7d --- /dev/null +++ b/server/sonar-web/design-system/src/components/popups.tsx @@ -0,0 +1,256 @@ +/* + * 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 { throttle } from 'lodash'; +import React, { AriaRole } from 'react'; +import { createPortal, findDOMNode } from 'react-dom'; +import tw from 'twin.macro'; +import { THROTTLE_SCROLL_DELAY } from '../helpers/constants'; +import { PopupPlacement, popupPositioning, PopupZLevel } from '../helpers/positioning'; +import { themeBorder, themeColor, themeContrast, themeShadow } from '../helpers/theme'; +import ClickEventBoundary from './ClickEventBoundary'; + +interface PopupProps { + 'aria-labelledby'?: string; + children?: React.ReactNode; + className?: string; + id?: string; + placement?: PopupPlacement; + role?: AriaRole; + style?: React.CSSProperties; + zLevel?: PopupZLevel; +} + +function PopupBase(props: PopupProps, ref: React.Ref) { + const { + children, + className, + placement = PopupPlacement.Bottom, + style, + zLevel = PopupZLevel.Default, + ...ariaProps + } = props; + return ( + + + {children} + + + ); +} + +const PopupWithRef = React.forwardRef(PopupBase); +PopupWithRef.displayName = 'Popup'; + +export const Popup = PopupWithRef; + +interface PortalPopupProps extends Omit { + allowResizing?: boolean; + children: React.ReactNode; + overlay: React.ReactNode; +} + +interface Measurements { + height: number; + left: number; + top: number; + width: number; +} + +type State = Partial; + +function isMeasured(state: State): state is Measurements { + return state.height !== undefined; +} + +export class PortalPopup extends React.PureComponent { + mounted = false; + popupNode = React.createRef(); + throttledPositionTooltip: () => void; + + constructor(props: PortalPopupProps) { + super(props); + this.state = {}; + this.throttledPositionTooltip = throttle(this.positionPopup, THROTTLE_SCROLL_DELAY); + } + + componentDidMount() { + this.positionPopup(); + this.addEventListeners(); + this.mounted = true; + } + + componentDidUpdate(prevProps: PortalPopupProps) { + if (this.props.placement !== prevProps.placement || this.props.overlay !== prevProps.overlay) { + this.positionPopup(); + } + } + + componentWillUnmount() { + this.removeEventListeners(); + this.mounted = false; + } + + addEventListeners = () => { + window.addEventListener('resize', this.throttledPositionTooltip); + if (this.props.zLevel !== PopupZLevel.Global) { + window.addEventListener('scroll', this.throttledPositionTooltip); + } + }; + + removeEventListeners = () => { + window.removeEventListener('resize', this.throttledPositionTooltip); + if (this.props.zLevel !== PopupZLevel.Global) { + window.removeEventListener('scroll', this.throttledPositionTooltip); + } + }; + + positionPopup = () => { + if (this.mounted) { + // `findDOMNode(this)` will search for the DOM node for the current component + // first it will find a React.Fragment (see `render`), + // so it will get the DOM node of the first child, i.e. DOM node of `this.props.children` + // docs: https://reactjs.org/docs/refs-and-the-dom.html#exposing-dom-refs-to-parent-components + + // eslint-disable-next-line react/no-find-dom-node + const toggleNode = findDOMNode(this); + if (toggleNode && toggleNode instanceof Element && this.popupNode.current) { + const { placement, zLevel } = this.props; + const isGlobal = zLevel === PopupZLevel.Global; + const { height, left, top, width } = popupPositioning( + toggleNode, + this.popupNode.current, + placement + ); + + // save width and height (and later set in `render`) to avoid resizing the popup element, + // when it's placed close to the window edge + this.setState({ + left: left + (isGlobal ? 0 : window.scrollX), + top: top + (isGlobal ? 0 : window.scrollY), + width, + height, + }); + } + } + }; + + render() { + const { + allowResizing, + children, + overlay, + placement = PopupPlacement.Bottom, + ...popupProps + } = this.props; + + let style: React.CSSProperties | undefined; + if (isMeasured(this.state)) { + style = { left: this.state.left, top: this.state.top }; + if (!allowResizing) { + style.width = this.state.width; + style.height = this.state.height; + } + } + return ( + <> + {this.props.children} + {this.props.overlay && ( + + + {overlay} + + + )} + + ); + } +} + +const PopupWrapper = styled.div<{ zLevel: PopupZLevel }>` + position: ${({ zLevel }) => (zLevel === PopupZLevel.Global ? 'fixed' : 'absolute')}; + background-color: ${themeColor('popup')}; + color: ${themeContrast('popup')}; + border: ${themeBorder('default', 'popupBorder')}; + box-shadow: ${themeShadow('md')}; + + ${tw`sw-box-border`}; + ${tw`sw-rounded-2`}; + ${tw`sw-cursor-default`}; + ${tw`sw-overflow-hidden`}; + ${({ zLevel }) => + ({ + [PopupZLevel.Default]: tw`sw-z-popup`, + [PopupZLevel.Global]: tw`sw-z-global-popup`, + [PopupZLevel.Content]: tw`sw-z-content-popup`, + }[zLevel])}; + + &.is-bottom, + &.is-bottom-left, + &.is-bottom-right { + ${tw`sw-mt-2`}; + } + + &.is-top, + &.is-top-left, + &.is-top-right { + ${tw`sw--mt-2`}; + } + + &.is-left, + &.is-left-top, + &.is-left-bottom { + ${tw`sw--ml-2`}; + } + + &.is-right, + &.is-right-top, + &.is-right-bottom { + ${tw`sw-ml-2`}; + } +`; + +class PortalWrapper extends React.Component { + el: HTMLElement; + + constructor(props: {}) { + super(props); + this.el = document.createElement('div'); + } + + componentDidMount() { + document.body.appendChild(this.el); + } + + componentWillUnmount() { + document.body.removeChild(this.el); + } + + render() { + return createPortal(this.props.children, this.el); + } +} diff --git a/server/sonar-web/design-system/src/helpers/__tests__/colors-test.ts b/server/sonar-web/design-system/src/helpers/__tests__/colors-test.ts new file mode 100644 index 00000000000..eead6e02c5c --- /dev/null +++ b/server/sonar-web/design-system/src/helpers/__tests__/colors-test.ts @@ -0,0 +1,61 @@ +/* + * 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 * as colors from '../colors'; + +describe('#stringToColor', () => { + it('should return a color for a text', () => { + expect(colors.stringToColor('skywalker')).toBe('#97f047'); + }); +}); + +describe('#isDarkColor', () => { + it('should be dark', () => { + expect(colors.isDarkColor('#000000')).toBe(true); + expect(colors.isDarkColor('#222222')).toBe(true); + expect(colors.isDarkColor('#000')).toBe(true); + }); + it('should be light', () => { + expect(colors.isDarkColor('#FFFFFF')).toBe(false); + expect(colors.isDarkColor('#CDCDCD')).toBe(false); + expect(colors.isDarkColor('#FFF')).toBe(false); + }); +}); + +describe('#getTextColor', () => { + it('should return dark color', () => { + expect(colors.getTextColor('#FFF', 'dark', 'light')).toBe('dark'); + expect(colors.getTextColor('#FFF')).toBe('#222'); + }); + it('should return light color', () => { + expect(colors.getTextColor('#000', 'dark', 'light')).toBe('light'); + expect(colors.getTextColor('#000')).toBe('#fff'); + }); +}); + +describe('rgb array to color', () => { + it('should return rgb color without opacity', () => { + expect(colors.getRGBAString([0, 0, 0])).toBe('rgb(0,0,0)'); + expect(colors.getRGBAString([255, 255, 255])).toBe('rgb(255,255,255)'); + }); + it('should return rgba color with opacity', () => { + expect(colors.getRGBAString([5, 6, 100], 0.05)).toBe('rgba(5,6,100,0.05)'); + expect(colors.getRGBAString([255, 255, 255], 0)).toBe('rgba(255,255,255,0)'); + }); +}); diff --git a/server/sonar-web/design-system/src/helpers/__tests__/positioning-test.ts b/server/sonar-web/design-system/src/helpers/__tests__/positioning-test.ts new file mode 100644 index 00000000000..7953d3531c9 --- /dev/null +++ b/server/sonar-web/design-system/src/helpers/__tests__/positioning-test.ts @@ -0,0 +1,167 @@ +/* + * 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 { PopupPlacement, popupPositioning } from '../positioning'; + +const toggleRect = { + getBoundingClientRect: jest.fn().mockReturnValue({ + left: 400, + top: 200, + width: 50, + height: 20, + }), +} as any; + +const popupRect = { + getBoundingClientRect: jest.fn().mockReturnValue({ + width: 200, + height: 100, + }), +} as any; + +beforeAll(() => { + Object.defineProperties(document.documentElement, { + clientWidth: { + configurable: true, + value: 1000, + }, + clientHeight: { + configurable: true, + value: 1000, + }, + }); +}); + +it('should calculate positioning based on placement', () => { + const fixes = { leftFix: 0, topFix: 0 }; + expect(popupPositioning(toggleRect, popupRect, PopupPlacement.Bottom)).toMatchObject({ + left: 325, + top: 220, + ...fixes, + }); + expect(popupPositioning(toggleRect, popupRect, PopupPlacement.BottomLeft)).toMatchObject({ + left: 400, + top: 220, + ...fixes, + }); + expect(popupPositioning(toggleRect, popupRect, PopupPlacement.BottomRight)).toMatchObject({ + left: 250, + top: 220, + ...fixes, + }); + expect(popupPositioning(toggleRect, popupRect, PopupPlacement.Top)).toMatchObject({ + left: 325, + top: 100, + ...fixes, + }); + expect(popupPositioning(toggleRect, popupRect, PopupPlacement.TopLeft)).toMatchObject({ + left: 400, + top: 100, + ...fixes, + }); + expect(popupPositioning(toggleRect, popupRect, PopupPlacement.TopRight)).toMatchObject({ + left: 250, + top: 100, + ...fixes, + }); + expect(popupPositioning(toggleRect, popupRect, PopupPlacement.Left)).toMatchObject({ + left: 200, + top: 160, + ...fixes, + }); + expect(popupPositioning(toggleRect, popupRect, PopupPlacement.LeftBottom)).toMatchObject({ + left: 200, + top: 120, + ...fixes, + }); + expect(popupPositioning(toggleRect, popupRect, PopupPlacement.LeftTop)).toMatchObject({ + left: 200, + top: 200, + ...fixes, + }); + expect(popupPositioning(toggleRect, popupRect, PopupPlacement.Right)).toMatchObject({ + left: 450, + top: 160, + ...fixes, + }); + expect(popupPositioning(toggleRect, popupRect, PopupPlacement.RightBottom)).toMatchObject({ + left: 450, + top: 120, + ...fixes, + }); + expect(popupPositioning(toggleRect, popupRect, PopupPlacement.RightTop)).toMatchObject({ + left: 450, + top: 200, + ...fixes, + }); +}); + +it('should position the element in the boundaries of the screen', () => { + toggleRect.getBoundingClientRect.mockReturnValueOnce({ + left: 0, + top: 850, + width: 50, + height: 50, + }); + expect(popupPositioning(toggleRect, popupRect, PopupPlacement.Bottom)).toMatchObject({ + left: 4, + leftFix: 79, + top: 896, + topFix: -4, + }); + toggleRect.getBoundingClientRect.mockReturnValueOnce({ + left: 900, + top: 0, + width: 50, + height: 50, + }); + expect(popupPositioning(toggleRect, popupRect, PopupPlacement.Top)).toMatchObject({ + left: 796, + leftFix: -29, + top: 4, + topFix: 104, + }); +}); + +it('should position the element outside the boundaries of the screen when the toggle is outside', () => { + toggleRect.getBoundingClientRect.mockReturnValueOnce({ + left: -100, + top: 1100, + width: 50, + height: 50, + }); + expect(popupPositioning(toggleRect, popupRect, PopupPlacement.Bottom)).toMatchObject({ + left: -75, + leftFix: 100, + top: 1025, + topFix: -125, + }); + toggleRect.getBoundingClientRect.mockReturnValueOnce({ + left: 1500, + top: -200, + width: 50, + height: 50, + }); + expect(popupPositioning(toggleRect, popupRect, PopupPlacement.Top)).toMatchObject({ + left: 1325, + leftFix: -100, + top: -175, + topFix: 125, + }); +}); diff --git a/server/sonar-web/design-system/src/helpers/__tests__/theme-test.ts b/server/sonar-web/design-system/src/helpers/__tests__/theme-test.ts new file mode 100644 index 00000000000..66f1b97ce05 --- /dev/null +++ b/server/sonar-web/design-system/src/helpers/__tests__/theme-test.ts @@ -0,0 +1,148 @@ +/* + * 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 * as ThemeHelper from '../../helpers/theme'; +import { lightTheme } from '../../theme'; + +const props = { + color: 'rgb(0,0,0)', +}; + +describe('getProp', () => { + it('should work', () => { + expect(ThemeHelper.getProp('color')(props)).toEqual('rgb(0,0,0)'); + }); +}); + +describe('themeColor', () => { + it('should work for light theme', () => { + expect(ThemeHelper.themeColor('backgroundPrimary')({ theme: lightTheme })).toEqual( + 'rgb(252,252,253)' + ); + }); + + it('should work with a theme-defined opacity', () => { + expect(ThemeHelper.themeColor('bannerIconHover')({ theme: lightTheme })).toEqual( + 'rgba(217,45,32,0.2)' + ); + }); + + it('should work for all kind of color parameters', () => { + expect(ThemeHelper.themeColor('transparent')({ theme: lightTheme })).toEqual('transparent'); + expect(ThemeHelper.themeColor('currentColor')({ theme: lightTheme })).toEqual('currentColor'); + expect(ThemeHelper.themeColor('var(--test)')({ theme: lightTheme })).toEqual('var(--test)'); + expect(ThemeHelper.themeColor('rgb(0,0,0)')({ theme: lightTheme })).toEqual('rgb(0,0,0)'); + expect(ThemeHelper.themeColor('rgba(0,0,0,1)')({ theme: lightTheme })).toEqual('rgba(0,0,0,1)'); + expect( + ThemeHelper.themeColor(ThemeHelper.themeContrast('backgroundPrimary')({ theme: lightTheme }))( + { + theme: lightTheme, + } + ) + ).toEqual('rgb(8,9,12)'); + expect( + ThemeHelper.themeColor(ThemeHelper.themeAvatarColor('luke')({ theme: lightTheme }))({ + theme: lightTheme, + }) + ).toEqual('rgb(209,215,254)'); + }); +}); + +describe('themeContrast', () => { + it('should work for light theme', () => { + expect(ThemeHelper.themeContrast('backgroundPrimary')({ theme: lightTheme })).toEqual( + 'rgb(8,9,12)' + ); + }); + + it('should work for all kind of color parameters', () => { + expect(ThemeHelper.themeContrast('var(--test)')({ theme: lightTheme })).toEqual('var(--test)'); + expect(ThemeHelper.themeContrast('rgb(0,0,0)')({ theme: lightTheme })).toEqual('rgb(0,0,0)'); + expect(ThemeHelper.themeContrast('rgba(0,0,0,1)')({ theme: lightTheme })).toEqual( + 'rgba(0,0,0,1)' + ); + expect( + ThemeHelper.themeContrast(ThemeHelper.themeColor('backgroundPrimary')({ theme: lightTheme }))( + { + theme: lightTheme, + } + ) + ).toEqual('rgb(252,252,253)'); + expect( + ThemeHelper.themeContrast(ThemeHelper.themeAvatarColor('luke')({ theme: lightTheme }))({ + theme: lightTheme, + }) + ).toEqual('rgb(209,215,254)'); + expect( + ThemeHelper.themeContrast('backgroundPrimary')({ + theme: { + ...lightTheme, + contrasts: { ...lightTheme.contrasts, backgroundPrimary: 'inherit' }, + }, + }) + ).toEqual('inherit'); + }); +}); + +describe('themeBorder', () => { + it('should work for light theme', () => { + expect(ThemeHelper.themeBorder()({ theme: lightTheme })).toEqual('1px solid rgb(235,235,235)'); + }); + it('should allow to override the color of the border', () => { + expect(ThemeHelper.themeBorder('focus', 'primaryLight')({ theme: lightTheme })).toEqual( + '4px solid rgba(123,135,217,0.2)' + ); + }); + it('should allow to override the opacity of the border', () => { + expect(ThemeHelper.themeBorder('focus', undefined, 0.5)({ theme: lightTheme })).toEqual( + '4px solid rgba(197,205,223,0.5)' + ); + }); + it('should allow to pass a CSS prop as color name', () => { + expect( + ThemeHelper.themeBorder('focus', 'var(--outlineColor)', 0.5)({ theme: lightTheme }) + ).toEqual('4px solid var(--outlineColor)'); + }); +}); + +describe('themeShadow', () => { + it('should work for light theme', () => { + expect(ThemeHelper.themeShadow('xs')({ theme: lightTheme })).toEqual( + '0px 1px 2px 0px rgba(29,33,47,0.05)' + ); + }); + it('should allow to override the color of the shadow', () => { + expect(ThemeHelper.themeShadow('xs', 'backgroundPrimary')({ theme: lightTheme })).toEqual( + '0px 1px 2px 0px rgba(252,252,253,0.05)' + ); + expect(ThemeHelper.themeShadow('xs', 'transparent')({ theme: lightTheme })).toEqual( + '0px 1px 2px 0px transparent' + ); + }); + it('should allow to override the opacity of the shadow', () => { + expect(ThemeHelper.themeShadow('xs', 'backgroundPrimary', 0.8)({ theme: lightTheme })).toEqual( + '0px 1px 2px 0px rgba(252,252,253,0.8)' + ); + }); + it('should allow to pass a CSS prop as color name', () => { + expect(ThemeHelper.themeShadow('xs', 'var(--shadowColor)')({ theme: lightTheme })).toEqual( + '0px 1px 2px 0px var(--shadowColor)' + ); + }); +}); diff --git a/server/sonar-web/design-system/src/helpers/colors.ts b/server/sonar-web/design-system/src/helpers/colors.ts new file mode 100644 index 00000000000..d0cb5e215ca --- /dev/null +++ b/server/sonar-web/design-system/src/helpers/colors.ts @@ -0,0 +1,56 @@ +/* + * 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 { CSSColor } from '../types/theme'; + +/* eslint-disable no-bitwise, no-mixed-operators */ +export function stringToColor(str: string) { + let hash = 0; + for (let i = 0; i < str.length; i++) { + hash = str.charCodeAt(i) + ((hash << 5) - hash); + } + let color = '#'; + for (let i = 0; i < 3; i++) { + const value = (hash >> (i * 8)) & 0xff; + color += ('00' + value.toString(16)).substr(-2); + } + return color; +} + +export function isDarkColor(color: string) { + color = color.substr(1); + if (color.length === 3) { + // shortcut notation: #f90 + color = color[0] + color[0] + color[1] + color[1] + color[2] + color[2]; + } + const rgb = parseInt(color.substr(1), 16); + const r = (rgb >> 16) & 0xff; + const g = (rgb >> 8) & 0xff; + const b = (rgb >> 0) & 0xff; + const luma = 0.2126 * r + 0.7152 * g + 0.0722 * b; + return luma < 140; +} + +export function getTextColor(background: string, dark = '#222', light = '#fff') { + return isDarkColor(background) ? light : dark; +} + +export function getRGBAString([r, g, b]: Array, a?: number | string) { + return (a !== undefined ? `rgba(${r},${g},${b},${a})` : `rgb(${r},${g},${b})`) as CSSColor; +} diff --git a/server/sonar-web/design-system/src/helpers/constants.ts b/server/sonar-web/design-system/src/helpers/constants.ts new file mode 100644 index 00000000000..68a385c3c1c --- /dev/null +++ b/server/sonar-web/design-system/src/helpers/constants.ts @@ -0,0 +1,68 @@ +/* + * 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 { theme } from 'twin.macro'; + +export const DEFAULT_LOCALE = 'en'; +export const IS_SSR = typeof window === 'undefined'; +export const REACT_DOM_CONTAINER = '#___gatsby'; + +export const RULE_STATUSES = ['READY', 'BETA', 'DEPRECATED']; + +export const THROTTLE_SCROLL_DELAY = 10; +export const THROTTLE_KEYPRESS_DELAY = 100; + +export const DEBOUNCE_DELAY = 250; + +export const DEBOUNCE_LONG_DELAY = 1000; + +export const DEBOUNCE_SUCCESS_DELAY = 1000; + +export const INTERACTIVE_TOOLTIP_DELAY = 0.5; + +export const LEAK_PERIOD = 'sonar.leak.period'; + +export const LEAK_PERIOD_TYPE = 'sonar.leak.period.type'; + +export const INPUT_SIZES = { + small: theme('width.input-small'), + medium: theme('width.input-medium'), + large: theme('width.input-large'), + full: theme('width.full'), + auto: theme('width.auto'), +}; + +export const LAYOUT_VIEWPORT_MIN_WIDTH = 1280; +export const LAYOUT_MAIN_CONTENT_GUTTER = 60; +export const LAYOUT_SIDEBAR_WIDTH = 240; +export const LAYOUT_SIDEBAR_COLLAPSED_WIDTH = 60; +export const LAYOUT_SIDEBAR_BREAKPOINT = 1320; +export const LAYOUT_BANNER_HEIGHT = 44; +export const LAYOUT_BRANDING_ICON_WIDTH = 198; +export const LAYOUT_FILTERBAR_HEADER = 56; +export const LAYOUT_GLOBAL_NAV_HEIGHT = 52; +export const LAYOUT_LOGO_MARGIN_RIGHT = 45; +export const LAYOUT_LOGO_MAX_HEIGHT = 40; +export const LAYOUT_LOGO_MAX_WIDTH = 150; +export const LAYOUT_FOOTER_HEIGHT = 52; +export const LAYOUT_NOTIFICATIONSBAR_WIDTH = 350; + +export const CORE_CONCEPTS_WIDTH = 350; + +export const DARK_THEME_ID = 'dark-theme'; diff --git a/server/sonar-web/design-system/src/components/DummyComponent.tsx b/server/sonar-web/design-system/src/helpers/index.ts similarity index 91% rename from server/sonar-web/design-system/src/components/DummyComponent.tsx rename to server/sonar-web/design-system/src/helpers/index.ts index 8470a1351a3..764e245473d 100644 --- a/server/sonar-web/design-system/src/components/DummyComponent.tsx +++ b/server/sonar-web/design-system/src/helpers/index.ts @@ -17,7 +17,5 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ - -export function DummyComponent() { - return
    I'm a dummy
    ; -} +export * from './constants'; +export * from './positioning'; diff --git a/server/sonar-web/design-system/src/helpers/keyboard.ts b/server/sonar-web/design-system/src/helpers/keyboard.ts new file mode 100644 index 00000000000..42bc6bdf52e --- /dev/null +++ b/server/sonar-web/design-system/src/helpers/keyboard.ts @@ -0,0 +1,52 @@ +/* + * 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. + */ +export enum Key { + ArrowLeft = 'ArrowLeft', + ArrowUp = 'ArrowUp', + ArrowRight = 'ArrowRight', + ArrowDown = 'ArrowDown', + + Alt = 'Alt', + Backspace = 'Backspace', + CapsLock = 'CapsLock', + Meta = 'Meta', + Control = 'Control', + Delete = 'Delete', + End = 'End', + Enter = 'Enter', + Escape = 'Escape', + Home = 'Home', + PageDown = 'PageDown', + PageUp = 'PageUp', + Shift = 'Shift', + Space = ' ', + Tab = 'Tab', +} + +export function isShortcut(event: KeyboardEvent): boolean { + return event.ctrlKey || event.metaKey; +} + +const INPUT_TAGS = ['INPUT', 'SELECT', 'TEXTAREA', 'UBCOMMENT']; + +export function isInput(event: KeyboardEvent): boolean { + const { tagName } = event.target as HTMLElement; + return INPUT_TAGS.includes(tagName); +} diff --git a/server/sonar-web/design-system/src/helpers/l10n.ts b/server/sonar-web/design-system/src/helpers/l10n.ts new file mode 100644 index 00000000000..96cf9467685 --- /dev/null +++ b/server/sonar-web/design-system/src/helpers/l10n.ts @@ -0,0 +1,30 @@ +/* + * 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. + */ + +export function translate(keys: string): string { + return keys; +} + +export function translateWithParameters( + messageKey: string, + ...parameters: Array +): string { + return `${messageKey}.${parameters.join('.')}`; +} diff --git a/server/sonar-web/design-system/src/helpers/positioning.ts b/server/sonar-web/design-system/src/helpers/positioning.ts new file mode 100644 index 00000000000..09384e2299b --- /dev/null +++ b/server/sonar-web/design-system/src/helpers/positioning.ts @@ -0,0 +1,185 @@ +/* + * 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. + */ +/** + * Positioning rules: + * - Bottom = below the block, horizontally centered + * - BottomLeft = below the block, horizontally left-aligned + * - BottomRight = below the block, horizontally right-aligned + * - Left = Left of the block, vertically centered + * - LeftTop = on the left-side of the block, vertically top-aligned + * - LeftBottom = on the left-side of the block, vertically bottom-aligned + * - Right = Right of the block, vertically centered + * - RightTop = on the right-side of the block, vertically top-aligned + * - RightBottom = on the right-side of the block, vetically bottom-aligned + * - Top = above the block, horizontally centered + * - TopLeft = above the block, horizontally left-aligned + * - TopRight = above the block, horizontally right-aligned + */ +export enum PopupPlacement { + Bottom = 'bottom', + BottomLeft = 'bottom-left', + BottomRight = 'bottom-right', + Left = 'left', + LeftTop = 'left-top', + LeftBottom = 'left-bottom', + Right = 'right', + RightTop = 'right-top', + RightBottom = 'right-bottom', + Top = 'top', + TopLeft = 'top-left', + TopRight = 'top-right', +} + +export enum PopupZLevel { + Content = 'content', + Default = 'popup', + Global = 'global', +} + +export type BasePlacement = Extract< + PopupPlacement, + PopupPlacement.Bottom | PopupPlacement.Top | PopupPlacement.Left | PopupPlacement.Right +>; + +export const PLACEMENT_FLIP_MAP: { [key in PopupPlacement]: PopupPlacement } = { + [PopupPlacement.Left]: PopupPlacement.Right, + [PopupPlacement.LeftBottom]: PopupPlacement.RightBottom, + [PopupPlacement.LeftTop]: PopupPlacement.RightTop, + [PopupPlacement.Right]: PopupPlacement.Left, + [PopupPlacement.RightBottom]: PopupPlacement.LeftBottom, + [PopupPlacement.RightTop]: PopupPlacement.LeftTop, + [PopupPlacement.Top]: PopupPlacement.Bottom, + [PopupPlacement.TopLeft]: PopupPlacement.BottomLeft, + [PopupPlacement.TopRight]: PopupPlacement.BottomRight, + [PopupPlacement.Bottom]: PopupPlacement.Top, + [PopupPlacement.BottomLeft]: PopupPlacement.TopLeft, + [PopupPlacement.BottomRight]: PopupPlacement.TopRight, +}; + +const MARGIN_TO_EDGE = 4; + +export function popupPositioning( + toggleNode: Element, + popupNode: Element, + placement: PopupPlacement = PopupPlacement.Bottom +) { + const toggleRect = toggleNode.getBoundingClientRect(); + const popupRect = popupNode.getBoundingClientRect(); + + let left = 0; + let top = 0; + + switch (placement) { + case PopupPlacement.Bottom: + left = toggleRect.left + toggleRect.width / 2 - popupRect.width / 2; + top = toggleRect.top + toggleRect.height; + break; + case PopupPlacement.BottomLeft: + left = toggleRect.left; + top = toggleRect.top + toggleRect.height; + break; + case PopupPlacement.BottomRight: + left = toggleRect.left + toggleRect.width - popupRect.width; + top = toggleRect.top + toggleRect.height; + break; + case PopupPlacement.Left: + left = toggleRect.left - popupRect.width; + top = toggleRect.top + toggleRect.height / 2 - popupRect.height / 2; + break; + case PopupPlacement.LeftTop: + left = toggleRect.left - popupRect.width; + top = toggleRect.top; + break; + case PopupPlacement.LeftBottom: + left = toggleRect.left - popupRect.width; + top = toggleRect.top + toggleRect.height - popupRect.height; + break; + case PopupPlacement.Right: + left = toggleRect.left + toggleRect.width; + top = toggleRect.top + toggleRect.height / 2 - popupRect.height / 2; + break; + case PopupPlacement.RightTop: + left = toggleRect.left + toggleRect.width; + top = toggleRect.top; + break; + case PopupPlacement.RightBottom: + left = toggleRect.left + toggleRect.width; + top = toggleRect.top + toggleRect.height - popupRect.height; + break; + case PopupPlacement.Top: + left = toggleRect.left + toggleRect.width / 2 - popupRect.width / 2; + top = toggleRect.top - popupRect.height; + break; + case PopupPlacement.TopLeft: + left = toggleRect.left; + top = toggleRect.top - popupRect.height; + break; + case PopupPlacement.TopRight: + left = toggleRect.left + toggleRect.width - popupRect.width; + top = toggleRect.top - popupRect.height; + break; + } + + const inBoundariesLeft = Math.min( + Math.max(left, getMinLeftPlacement(toggleRect)), + getMaxLeftPlacement(toggleRect, popupRect) + ); + const inBoundariesTop = Math.min( + Math.max(top, getMinTopPlacement(toggleRect)), + getMaxTopPlacement(toggleRect, popupRect) + ); + + return { + height: popupRect.height, + left: inBoundariesLeft, + leftFix: inBoundariesLeft - left, + top: inBoundariesTop, + topFix: inBoundariesTop - top, + width: popupRect.width, + }; +} + +function getMinLeftPlacement(toggleRect: DOMRect) { + return Math.min( + MARGIN_TO_EDGE, // Left edge of the sceen + toggleRect.left + toggleRect.width / 2 // Left edge of the screen when scrolled + ); +} + +function getMaxLeftPlacement(toggleRect: DOMRect, popupRect: DOMRect) { + return Math.max( + document.documentElement.clientWidth - popupRect.width - MARGIN_TO_EDGE, // Right edge of the screen + toggleRect.left + toggleRect.width / 2 - popupRect.width // Right edge of the screen when scrolled + ); +} + +function getMinTopPlacement(toggleRect: DOMRect) { + return Math.min( + MARGIN_TO_EDGE, // Top edge of the sceen + toggleRect.top + toggleRect.height / 2 // Top edge of the screen when scrolled + ); +} + +function getMaxTopPlacement(toggleRect: DOMRect, popupRect: DOMRect) { + return Math.max( + document.documentElement.clientHeight - popupRect.height - MARGIN_TO_EDGE, // Bottom edge of the screen + toggleRect.top + toggleRect.height / 2 - popupRect.height // Bottom edge of the screen when scrolled + ); +} diff --git a/server/sonar-web/design-system/src/helpers/testUtils.tsx b/server/sonar-web/design-system/src/helpers/testUtils.tsx new file mode 100644 index 00000000000..558906fe0dc --- /dev/null +++ b/server/sonar-web/design-system/src/helpers/testUtils.tsx @@ -0,0 +1,117 @@ +/* + * 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 as rtlRender, RenderOptions } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { Options as UserEventsOptions } from '@testing-library/user-event/dist/types/options'; +import { InitialEntry } from 'history'; +import { identity, kebabCase } from 'lodash'; +import React, { PropsWithChildren, ReactNode } from 'react'; +import { HelmetProvider } from 'react-helmet-async'; +import { IntlProvider } from 'react-intl'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; + +export function render( + ui: React.ReactElement, + options?: RenderOptions, + userEventOptions?: UserEventsOptions +) { + return { ...rtlRender(ui, options), user: userEvent.setup(userEventOptions) }; +} + +type RenderContextOptions = Omit & { + initialEntries?: InitialEntry[]; + userEventOptions?: UserEventsOptions; +}; + +export function renderWithContext( + ui: React.ReactElement, + { userEventOptions, ...options }: RenderContextOptions = {} +) { + return render(ui, { ...options, wrapper: getContextWrapper() }, userEventOptions); +} + +type RenderRouterOptions = { additionalRoutes?: ReactNode }; + +export function renderWithRouter( + ui: React.ReactElement, + options: RenderContextOptions & RenderRouterOptions = {} +) { + const { additionalRoutes, userEventOptions, ...renderOptions } = options; + + function RouterWrapper({ children }: React.PropsWithChildren<{}>) { + return ( + + + + + {additionalRoutes} + + + + ); + } + + return render(ui, { ...renderOptions, wrapper: RouterWrapper }, userEventOptions); +} + +function getContextWrapper() { + return function ContextWrapper({ children }: React.PropsWithChildren<{}>) { + return ( + + + {children} + + + ); + }; +} + +export function mockComponent(name: string, transformProps: (props: any) => any = identity) { + function MockedComponent({ ...props }: PropsWithChildren) { + return React.createElement('mocked-' + kebabCase(name), transformProps(props)); + } + + MockedComponent.displayName = `mocked(${name})`; + return MockedComponent; +} + +export const debounceTimer = jest.fn().mockImplementation((callback, timeout) => { + let timeoutId: number; + const debounced = jest.fn((...args) => { + window.clearTimeout(timeoutId); + timeoutId = window.setTimeout(() => callback(...args), timeout); + }); + (debounced as any).cancel = jest.fn(() => { + window.clearTimeout(timeoutId); + }); + return debounced; +}); + +export function flushPromises(usingFakeTime = false): Promise { + return new Promise((resolve) => { + if (usingFakeTime) { + jest.useRealTimers(); + } + setTimeout(resolve, 0); + if (usingFakeTime) { + jest.useFakeTimers(); + } + }); +} diff --git a/server/sonar-web/design-system/src/helpers/theme.ts b/server/sonar-web/design-system/src/helpers/theme.ts new file mode 100644 index 00000000000..6dab879cf4a --- /dev/null +++ b/server/sonar-web/design-system/src/helpers/theme.ts @@ -0,0 +1,130 @@ +/* + * 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 { CSSColor, Theme, ThemeColors, ThemeContrasts, ThemedProps } from '../types/theme'; +import { getRGBAString } from './colors'; + +export function getProp(name: keyof Omit) { + return (props: T) => props[name]; +} + +export function themeColor(name: ThemeColors | CSSColor, opacity?: number) { + return function ({ theme }: ThemedProps) { + return getColor(theme, [], name, opacity); + }; +} + +export function themeContrast(name: ThemeColors | CSSColor) { + return function ({ theme }: ThemedProps) { + return getContrast(theme, name); + }; +} + +export function themeBorder( + name: keyof Theme['borders'] = 'default', + color?: ThemeColors | CSSColor, + opacity?: number +) { + return function ({ theme }: ThemedProps) { + const [width, style, ...rgba] = theme.borders[name]; + return `${width} ${style} ${getColor(theme, rgba as number[], color, opacity)}`; + }; +} + +export function themeShadow( + name: keyof Theme['shadows'], + color?: ThemeColors | CSSColor, + opacity?: number +) { + return function ({ theme }: ThemedProps) { + const shadows = theme.shadows[name]; + return shadows + .map((item) => { + const [x, y, blur, spread, ...rgba] = item; + return `${x}px ${y}px ${blur}px ${spread}px ${getColor(theme, rgba, color, opacity)}`; + }) + .join(','); + }; +} + +export function themeAvatarColor(name: string, contrast = false) { + return function ({ theme }: ThemedProps) { + let hash = 0; + for (let i = 0; i < name.length; i++) { + hash = name.charCodeAt(i) + ((hash << 5) - hash); + } + + // Reduces number length to avoid modulo's limit. + hash = parseInt(hash.toString().slice(-5), 10); + if (contrast) { + return getColor(theme, theme.avatar.contrast[hash % theme.avatar.contrast.length]); + } + return getColor(theme, theme.avatar.color[hash % theme.avatar.color.length]); + }; +} + +export function themeImage(imageKey: keyof Theme['images']) { + return function ({ theme }: ThemedProps) { + return theme.images[imageKey]; + }; +} + +function getColor( + theme: Theme, + [r, g, b, a]: number[], + colorOverride?: ThemeColors | CSSColor, + opacityOverride?: number +) { + // Custom CSS property or rgb(a) color, return it directly + if ( + colorOverride?.startsWith('var(--') || + colorOverride?.startsWith('rgb(') || + colorOverride?.startsWith('rgba(') + ) { + return colorOverride as CSSColor; + } + // Is theme color overridden by a color name ? + const color = colorOverride ? theme.colors[colorOverride as ThemeColors] : [r, g, b]; + if (typeof color === 'string') { + return color as CSSColor; + } + + return getRGBAString(color, opacityOverride ?? color[3] ?? a); +} + +// Simplified version of getColor for contrast colors, fallback to colors if contrast isn't found +function getContrast(theme: Theme, colorOverride: ThemeContrasts | ThemeColors | CSSColor) { + // Custom CSS property or rgb(a) color, return it directly + if ( + colorOverride?.startsWith('var(--') || + colorOverride?.startsWith('rgb(') || + colorOverride?.startsWith('rgba(') + ) { + return colorOverride as CSSColor; + } + + // For contrast we always require a color override (it's the principle of a contrast) + const color = + theme.contrasts[colorOverride as ThemeContrasts] || theme.colors[colorOverride as ThemeColors]; + if (typeof color === 'string') { + return color as CSSColor; + } + + return getRGBAString(color, color[3]); +} diff --git a/server/sonar-web/design-system/src/helpers/types.ts b/server/sonar-web/design-system/src/helpers/types.ts new file mode 100644 index 00000000000..05b2043827b --- /dev/null +++ b/server/sonar-web/design-system/src/helpers/types.ts @@ -0,0 +1,22 @@ +/* + * 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. + */ +export function isDefined(x: T | undefined | null): x is T { + return x !== undefined && x !== null; +} diff --git a/server/sonar-web/design-system/src/index.ts b/server/sonar-web/design-system/src/index.ts new file mode 100644 index 00000000000..cd4bd05a51b --- /dev/null +++ b/server/sonar-web/design-system/src/index.ts @@ -0,0 +1,23 @@ +/* + * 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. + */ + +export * from './components'; +export * from './helpers'; +export * from './theme'; diff --git a/server/sonar-web/design-system/src/theme/colors.ts b/server/sonar-web/design-system/src/theme/colors.ts new file mode 100644 index 00000000000..785f6f07c8b --- /dev/null +++ b/server/sonar-web/design-system/src/theme/colors.ts @@ -0,0 +1,136 @@ +/* + * 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. + */ +export default { + white: [255, 255, 255], + black: [0, 0, 0], + sonarcloud: [243, 112, 42], + grey: { 50: [235, 235, 235], 100: [221, 221, 221] }, + blueGrey: { + 25: [252, 252, 253], + 50: [239, 242, 249], + 100: [225, 230, 243], + 200: [197, 205, 223], + 300: [166, 173, 194], + 400: [106, 117, 144], + 500: [62, 67, 87], + 600: [42, 47, 64], + 700: [29, 33, 47], + 800: [18, 20, 29], + 900: [8, 9, 12], + }, + indigo: { + 25: [244, 246, 255], + 50: [232, 235, 255], + 100: [209, 215, 254], + 200: [189, 198, 255], + 300: [159, 169, 237], + 400: [123, 135, 217], + 500: [93, 108, 208], + 600: [75, 86, 187], + 700: [71, 81, 143], + 800: [43, 51, 104], + 900: [27, 34, 80], + }, + tangerine: { + 25: [255, 248, 244], + 50: [250, 230, 220], + 100: [246, 206, 187], + 200: [243, 185, 157], + 300: [240, 166, 130], + 400: [237, 148, 106], + 500: [235, 131, 82], + 600: [233, 116, 63], + 700: [231, 102, 49], + 800: [181, 68, 25], + 900: [130, 43, 10], + }, + green: { + 50: [246, 254, 249], + 100: [236, 253, 243], + 200: [209, 250, 223], + 300: [166, 244, 197], + 400: [50, 213, 131], + 500: [18, 183, 106], + 600: [3, 152, 85], + 700: [2, 122, 72], + 800: [5, 96, 58], + 900: [5, 79, 49], + }, + yellowGreen: { + 50: [247, 251, 230], + 100: [241, 250, 210], + 200: [225, 245, 168], + 300: [197, 230, 124], + 400: [166, 208, 91], + 500: [110, 183, 18], + 600: [104, 154, 48], + 700: [83, 128, 39], + 800: [63, 104, 29], + 900: [49, 85, 22], + }, + yellow: { + 50: [252, 245, 228], + 100: [254, 245, 208], + 200: [252, 233, 163], + 300: [250, 220, 121], + 400: [248, 205, 92], + 500: [245, 184, 64], + 600: [209, 152, 52], + 700: [174, 122, 41], + 800: [140, 94, 30], + 900: [102, 64, 15], + }, + orange: { + 50: [255, 240, 235], + 100: [254, 219, 199], + 200: [255, 214, 175], + 300: [254, 150, 75], + 400: [253, 113, 34], + 500: [247, 95, 9], + 600: [220, 94, 3], + 700: [181, 71, 8], + 800: [147, 55, 13], + 900: [122, 46, 14], + }, + red: { + 50: [254, 243, 242], + 100: [254, 228, 226], + 200: [254, 205, 202], + 300: [253, 162, 155], + 400: [249, 112, 102], + 500: [240, 68, 56], + 600: [217, 45, 32], + 700: [180, 35, 24], + 800: [128, 27, 20], + 900: [93, 29, 19], + }, + blue: { + 50: [245, 251, 255], + 100: [233, 244, 251], + 200: [184, 222, 241], + 300: [143, 202, 234], + 400: [110, 185, 228], + 500: [85, 170, 223], + 600: [69, 149, 203], + 700: [58, 127, 173], + 800: [49, 108, 146], + 900: [23, 67, 97], + }, +}; diff --git a/server/sonar-web/design-system/src/theme/index.ts b/server/sonar-web/design-system/src/theme/index.ts new file mode 100644 index 00000000000..6b8c84a5721 --- /dev/null +++ b/server/sonar-web/design-system/src/theme/index.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ +export { default as lightTheme } from './light'; diff --git a/server/sonar-web/design-system/src/theme/light.ts b/server/sonar-web/design-system/src/theme/light.ts new file mode 100644 index 00000000000..8b10b339326 --- /dev/null +++ b/server/sonar-web/design-system/src/theme/light.ts @@ -0,0 +1,743 @@ +/* + * 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 COLORS from './colors'; + +const primary = { + light: COLORS.indigo[400], + default: COLORS.indigo[500], + dark: COLORS.indigo[600], +}; + +const secondary = { + light: COLORS.blueGrey[50], + default: COLORS.blueGrey[200], + dark: COLORS.blueGrey[400], + darker: COLORS.blueGrey[500], +}; + +const danger = { + lightest: COLORS.red[50], + lighter: COLORS.red[300], + light: COLORS.red[400], + default: COLORS.red[600], + dark: COLORS.red[700], + darker: COLORS.red[800], +}; + +const lightTheme = { + id: 'light-theme', + highlightTheme: 'atom-one-light.css', + logo: 'sonarcloud-logo-black.svg', + + colors: { + transparent: 'transparent', + currentColor: 'currentColor', + + backgroundPrimary: COLORS.blueGrey[25], + backgroundSecondary: COLORS.white, + border: COLORS.grey[50], + sonarcloud: COLORS.sonarcloud, + + // primary + primaryLight: primary.light, + primary: primary.default, + primaryDark: primary.dark, + + // danger + danger: danger.dark, + + // buttons + button: primary.default, + buttonHover: primary.dark, + buttonSecondary: COLORS.white, + buttonSecondaryBorder: secondary.default, + buttonSecondaryHover: secondary.light, + buttonDisabled: secondary.light, + buttonDisabledBorder: secondary.default, + + // danger buttons + dangerButton: danger.default, + dangerButtonHover: danger.dark, + dangerButtonFocus: danger.default, + dangerButtonSecondary: COLORS.white, + dangerButtonSecondaryBorder: danger.lighter, + dangerButtonSecondaryHover: danger.lightest, + dangerButtonSecondaryFocus: danger.light, + + // third party button + thirdPartyButton: COLORS.white, + thirdPartyButtonBorder: secondary.default, + thirdPartyButtonHover: secondary.light, + + // popup + popup: COLORS.white, + popupBorder: secondary.default, + + // dropdown menu + dropdownMenu: COLORS.white, + dropdownMenuHover: secondary.light, + dropdownMenuFocus: COLORS.indigo[50], + dropdownMenuFocusBorder: primary.light, + dropdownMenuDisabled: COLORS.white, + dropdownMenuHeader: COLORS.white, + dropdownMenuDanger: danger.default, + dropdownMenuSubTitle: secondary.dark, + + // radio + radio: primary.default, + radioBorder: primary.default, + radioHover: COLORS.indigo[50], + radioFocus: COLORS.indigo[50], + radioFocusBorder: COLORS.indigo[300], + radioFocusOutline: [...COLORS.indigo[300], 0.2], + radioChecked: COLORS.indigo[50], + radioDisabled: secondary.default, + radioDisabledBackground: secondary.light, + radioDisabledBorder: secondary.default, + + // switch + switch: secondary.default, + switchDisabled: COLORS.blueGrey[100], + switchActive: primary.default, + switchHover: COLORS.blueGrey[300], + switchHoverActive: primary.light, + switchButton: COLORS.white, + switchButtonDisabled: secondary.light, + + // sidebar + // NOTE: these aren't used because the sidebar is exclusively dark. but for type purposes are listed here + sidebarBackground: COLORS.blueGrey[700], + sidebarItemActive: COLORS.blueGrey[800], + sidebarBorder: COLORS.blueGrey[500], + sidebarTextDisabled: COLORS.blueGrey[400], + sidebarIcon: COLORS.blueGrey[400], + sidebarActiveIcon: COLORS.blueGrey[200], + + //separator-circle + separatorCircle: COLORS.blueGrey[200], + separatorSlash: COLORS.blueGrey[300], + + // flag message + flagMessageBackground: COLORS.white, + + errorBorder: danger.light, + errorBackground: danger.lightest, + errorText: danger.dark, + + warningBorder: COLORS.yellow[400], + warningBackground: COLORS.yellow[50], + + successBorder: COLORS.green[400], + successBackground: COLORS.green[50], + + infoBorder: COLORS.blue[400], + infoBackground: COLORS.blue[50], + + // banner message + bannerMessage: danger.lightest, + bannerMessageIcon: danger.darker, + + // toggle buttons + toggle: COLORS.white, + toggleBorder: secondary.default, + toggleHover: secondary.light, + toggleFocus: [...secondary.default, 0.2], + + // code snippet + codeSnippetBackground: COLORS.blueGrey[25], + codeSnippetBorder: COLORS.blueGrey[100], + codeSnippetHighlight: secondary.default, + + // code viewer + codeLineIssueIndicator: COLORS.blueGrey[400], // Should be blueGrey[300], to be changed once code viewer is reworked + + // checkbox + checkboxHover: COLORS.indigo[50], + checkboxCheckedHover: primary.light, + checkboxDisabled: secondary.light, + checkboxDisabledChecked: secondary.default, + checkboxLabel: COLORS.blueGrey[500], + + // input search + searchHighlight: COLORS.tangerine[50], + + // input field + inputBackground: COLORS.white, + inputBorder: secondary.default, + inputFocus: primary.light, + inputDanger: danger.default, + inputDangerFocus: danger.light, + inputSuccess: COLORS.yellowGreen[500], + inputSuccessFocus: COLORS.yellowGreen[400], + inputDisabled: secondary.light, + inputDisabledBorder: secondary.default, + inputPlaceholder: secondary.dark, + + // required input + inputRequired: danger.dark, + + // tooltip + tooltipBackground: COLORS.blueGrey[600], + tooltipSeparator: secondary.dark, + + // avatar + avatarBackground: COLORS.white, + avatarBorder: COLORS.blueGrey[100], + + // badges + badgeNew: COLORS.indigo[100], + badgeDefault: COLORS.blueGrey[100], + badgeDeleted: COLORS.red[100], + badgeCounter: COLORS.blueGrey[100], + + // input select + selectOptionSelected: secondary.light, + + // breadcrumbs + breadcrumb: 'transparent', + + // tab + tabBorder: primary.light, + + //table + tableRowHover: COLORS.indigo[25], + tableRowSelected: COLORS.indigo[300], + + // links + linkDefault: primary.default, + linkActive: COLORS.indigo[600], + linkDiscreet: 'currentColor', + linkTooltipDefault: COLORS.indigo[200], + linkTooltipActive: COLORS.indigo[100], + + // discreet select + discreetBorder: secondary.default, + discreetBackground: COLORS.white, + discreetHover: secondary.light, + discreetButtonHover: COLORS.indigo[500], + discreetFocus: COLORS.indigo[50], + discreetFocusBorder: primary.light, + + // interactive icon + interactiveIcon: 'transparent', + interactiveIconHover: COLORS.indigo[50], + interactiveIconFocus: primary.default, + bannerIcon: 'transparent', + bannerIconHover: [...COLORS.red[600], 0.2], + bannerIconFocus: danger.default, + discreetInteractiveIcon: secondary.dark, + destructiveIcon: 'transparent', + destructiveIconHover: danger.lightest, + destructiveIconFocus: danger.default, + + // icons + iconSeverityMajor: danger.light, + iconSeverityMinor: COLORS.yellowGreen[400], + iconSeverityInfo: COLORS.blue[400], + iconDirectory: COLORS.orange[300], + iconFile: COLORS.blueGrey[300], + iconProject: COLORS.blueGrey[300], + iconUnitTest: COLORS.blueGrey[300], + iconFavorite: COLORS.tangerine[400], + iconCheck: COLORS.green[500], + iconPositiveUpdate: COLORS.green[300], + iconNegativeUpdate: COLORS.red[300], + iconTrendPositive: COLORS.green[400], + iconTrendNegative: COLORS.red[400], + iconTrendNeutral: COLORS.blue[400], + iconTrendDisabled: COLORS.blueGrey[400], + iconError: danger.default, + iconWarning: COLORS.yellow[600], + iconSuccess: COLORS.green[600], + iconInfo: COLORS.blue[600], + iconStatus: COLORS.blueGrey[200], + iconStatusResolved: secondary.dark, + iconNotificationsOn: COLORS.indigo[300], + iconHelperHint: COLORS.blueGrey[100], + iconRuleInheritanceOverride: danger.light, + + // numbered list + numberedList: COLORS.indigo[50], + + // unordered list + listMarker: COLORS.blueGrey[300], + + // product news + productNews: COLORS.indigo[50], + productNewsHover: COLORS.indigo[100], + + // scrollbar + scrollbar: COLORS.blueGrey[25], + + // resizer + resizer: secondary.default, + + // coverage indicators + coverageGreen: COLORS.green[500], + coverageRed: danger.dark, + + // duplications indicators + 'duplicationsRating.A': COLORS.green[500], + 'duplicationsRating.B': COLORS.yellowGreen[500], + 'duplicationsRating.C': COLORS.yellow[500], + 'duplicationsRating.D': COLORS.orange[500], + 'duplicationsRating.E': COLORS.red[500], + duplicationsRatingSecondary: secondary.light, + + // size indicators + sizeIndicator: COLORS.blue[500], + + // rating colors + 'rating.A': COLORS.green[200], + 'rating.B': COLORS.yellowGreen[200], + 'rating.C': COLORS.yellow[200], + 'rating.D': COLORS.orange[200], + 'rating.E': COLORS.red[200], + + // date picker + datePicker: COLORS.white, + datePickerIcon: secondary.default, + datePickerDisabled: COLORS.white, + datePickerDefault: COLORS.white, + datePickerHover: COLORS.blueGrey[100], + datePickerSelected: primary.default, + datePickerRange: COLORS.indigo[100], + + // tags + tag: secondary.light, + + // quality gate indicator + qgIndicatorPassed: COLORS.green[200], + qgIndicatorFailed: COLORS.red[200], + qgIndicatorNotComputed: COLORS.blueGrey[200], + + // main bar + mainBar: COLORS.white, + mainBarHover: COLORS.blueGrey[600], + mainBarLogo: COLORS.white, + mainBarDarkLogo: COLORS.blueGrey[800], + mainBarNews: COLORS.indigo[50], + menuBorder: primary.light, + + // navbar + navbar: COLORS.white, + navbarTextMeta: secondary.darker, + + // filterbar + filterbar: COLORS.white, + filterbarBorder: COLORS.blueGrey[100], + + // facets + facetHeader: COLORS.blueGrey[600], + facetItemSelected: COLORS.indigo[50], + facetItemSelectedHover: COLORS.indigo[100], + facetItemSelectedBorder: primary.light, + facetItemDisabled: COLORS.blueGrey[300], + facetItemLight: secondary.dark, + facetItemGraph: secondary.default, + facetKeyboardHint: COLORS.blueGrey[50], + facetToggleActive: COLORS.green[500], + facetToggleInactive: COLORS.red[500], + facetToggleHover: COLORS.blueGrey[600], + + // subnavigation sidebar + subnavigation: COLORS.white, + subnavigationHover: COLORS.indigo[50], + subnavigationBorder: COLORS.grey[100], + subnavigationSeparator: COLORS.grey[50], + subnavigationSubheading: COLORS.blueGrey[25], + + // footer + footer: COLORS.white, + footerBorder: COLORS.grey[100], + + // project + projectCardBackground: COLORS.white, + projectCardBorder: COLORS.blueGrey[100], + + // overview + iconOverviewIssue: COLORS.blueGrey[400], + + // graph - chart + graphPointCircleColor: COLORS.white, + 'graphLineColor.0': COLORS.blue[500], + 'graphLineColor.1': COLORS.blue[700], + 'graphLineColor.2': COLORS.blue[300], + 'graphLineColor.3': COLORS.blue[900], + graphGridColor: COLORS.grey[50], + graphCursorLineColor: COLORS.blueGrey[400], + newCodeHighlight: COLORS.indigo[300], + graphZoomBackgroundColor: COLORS.blueGrey[25], + graphZoomBorderColor: COLORS.blueGrey[100], + graphZoomHandleColor: COLORS.blueGrey[400], + + // page + pageTitle: COLORS.blueGrey[700], + pageContentLight: secondary.dark, + pageContent: secondary.darker, + pageContentDark: COLORS.blueGrey[600], + pageBlock: COLORS.white, + pageBlockBorder: COLORS.blueGrey[100], + + // core concepts + coreConceptsCloseIcon: COLORS.blueGrey[300], + coreConceptsTitle: secondary.darker, + coreConceptsBody: secondary.darker, + coreConceptsHomeBorder: COLORS.blueGrey[100], + coreConceptsCompleted: COLORS.green[500], + coreConceptsPulse: COLORS.indigo[500], + coreConceptsPulseFallback: COLORS.white, + + // progress bar + coreConceptsProgressBar: secondary.light, + + // issue box + issueBoxBorder: danger.lighter, + issueBoxBorderDepracated: secondary.default, + issueTypeIcon: COLORS.red[200], + + // separator + pipeSeparator: COLORS.blueGrey[100], + + // drilldown link + drilldown: secondary.darker, + drilldownBorder: secondary.default, + + // selection card + selectionCardHeader: secondary.darker, + selectionCardDisabled: secondary.light, + selectionCardBorder: COLORS.blueGrey[100], + selectionCardBorderHover: COLORS.indigo[200], + selectionCardBorderSelected: primary.light, + selectionCardBorderDisabled: secondary.default, + + // bubble charts + bubbleChartLine: COLORS.grey[50], + bubbleDefault: [...COLORS.blue[500], 0.3], + 'bubble.1': [...COLORS.green[500], 0.3], + 'bubble.2': [...COLORS.yellowGreen[500], 0.3], + 'bubble.3': [...COLORS.yellow[500], 0.3], + 'bubble.4': [...COLORS.orange[500], 0.3], + 'bubble.5': [...COLORS.red[500], 0.3], + + // leak legend + leakLegend: [...COLORS.indigo[300], 0.15], + leakLegendBorder: COLORS.indigo[100], + + // hotspot + hotspotStatus: COLORS.blueGrey[25], + + // activity comments + activityCommentPipe: COLORS.tangerine[200], + + // illustrations + illustrationOutline: COLORS.blueGrey[400], + illustrationInlineBorder: COLORS.blueGrey[100], + illustrationPrimary: COLORS.indigo[400], + illustrationSecondary: COLORS.indigo[200], + illustrationShade: COLORS.indigo[25], + + // news bar + newsBar: COLORS.white, + newsBorder: COLORS.grey[100], + newsContent: COLORS.white, + newsTag: COLORS.blueGrey[50], + roadmap: COLORS.indigo[25], + roadmapContent: 'transparent', + + // project analyse page + almCardBorder: COLORS.grey[100], + }, + + // contrast colors to be used for text when using a color background with the same name + // must match the color name + contrasts: { + backgroundPrimary: COLORS.blueGrey[900], + backgroundSecondary: COLORS.blueGrey[900], + primaryLight: secondary.darker, + primary: COLORS.white, + + // switch + switchHover: primary.light, + switchButton: primary.default, + switchButtonDisabled: COLORS.blueGrey[300], + + // sidebar + sidebarBackground: COLORS.blueGrey[200], + sidebarItemActive: COLORS.blueGrey[25], + + // flag message + flagMessageBackground: secondary.darker, + + // banner message + bannerMessage: COLORS.red[900], + + // buttons + buttonDisabled: COLORS.blueGrey[300], + buttonSecondary: secondary.darker, + + // danger buttons + dangerButton: COLORS.white, + dangerButtonSecondary: danger.dark, + + // third party button + thirdPartyButton: secondary.darker, + + // popup + popup: secondary.darker, + + // dropdown menu + dropdownMenu: secondary.darker, + dropdownMenuDisabled: COLORS.blueGrey[300], + dropdownMenuHeader: secondary.dark, + + // toggle buttons + toggle: secondary.darker, + toggleHover: secondary.darker, + + // code snippet + codeSnippetHighlight: danger.default, + + // checkbox + checkboxDisabled: secondary.default, + + // input search + searchHighlight: secondary.darker, + + // input field + inputBackground: secondary.darker, + inputDisabled: COLORS.blueGrey[300], + + // tooltip + tooltipBackground: secondary.light, + + // badges + badgeNew: COLORS.indigo[900], + badgeDefault: COLORS.blueGrey[700], + badgeDeleted: COLORS.red[900], + badgeCounter: secondary.darker, + + // breadcrumbs + breadcrumb: secondary.dark, + + // discreet select + discreetBackground: secondary.darker, + discreetHover: secondary.darker, + + // interactive icons + interactiveIcon: primary.dark, + interactiveIconHover: COLORS.indigo[800], + bannerIcon: danger.darker, + bannerIconHover: danger.darker, + destructiveIcon: danger.default, + destructiveIconHover: danger.darker, + + // icons + iconSeverityMajor: COLORS.white, + iconSeverityMinor: COLORS.white, + iconSeverityInfo: COLORS.white, + iconStatusResolved: COLORS.white, + iconHelperHint: secondary.darker, + + // numbered list + numberedList: COLORS.indigo[800], + + // product news + productNews: secondary.darker, + productNewsHover: secondary.darker, + + // scrollbar + scrollbar: COLORS.grey[100], + + // size indicators + sizeIndicator: COLORS.white, + + // rating colors + 'rating.A': COLORS.green[900], + 'rating.B': COLORS.yellowGreen[900], + 'rating.C': COLORS.yellow[900], + 'rating.D': COLORS.orange[900], + 'rating.E': COLORS.red[900], + + // date picker + datePicker: COLORS.blueGrey[300], + datePickerDisabled: COLORS.blueGrey[300], + datePickerDefault: COLORS.blueGrey[600], + datePickerHover: COLORS.blueGrey[600], + datePickerSelected: COLORS.white, + datePickerRange: COLORS.blueGrey[600], + + // tags + tag: secondary.darker, + + // quality gate indicator + qgIndicatorPassed: COLORS.green[800], + qgIndicatorFailed: danger.darker, + qgIndicatorNotComputed: COLORS.blueGrey[800], + + // main bar + mainBar: secondary.darker, + mainBarLogo: COLORS.black, + mainBarDarkLogo: COLORS.white, + mainBarNews: secondary.darker, + + // navbar + navbar: secondary.darker, + + // filterbar + filterbar: secondary.darker, + + // facet + facetKeyboardHint: secondary.darker, + facetToggleActive: COLORS.white, + facetToggleInactive: COLORS.white, + + // subnavigation sidebar + subnavigation: secondary.darker, + subnavigationHover: COLORS.blueGrey[700], + subnavigationSubheading: secondary.dark, + + // footer + footer: secondary.dark, + + // page + pageBlock: secondary.darker, + + // graph - chart + graphZoomHandleColor: COLORS.white, + + // progress bar + coreConceptsProgressBar: primary.light, + + // issue box + issueTypeIcon: COLORS.red[900], + + // selection card + selectionCardDisabled: secondary.dark, + + // bubble charts + bubbleDefault: COLORS.blue[500], + 'bubble.1': COLORS.green[500], + 'bubble.2': COLORS.yellowGreen[500], + 'bubble.3': COLORS.yellow[500], + 'bubble.4': COLORS.orange[500], + 'bubble.5': COLORS.red[500], + + // news bar + newsBar: COLORS.blueGrey[600], + newsContent: COLORS.blueGrey[500], + newsTag: COLORS.blueGrey[500], + roadmap: COLORS.blueGrey[600], + roadmapContent: COLORS.blueGrey[500], + }, + + // predefined shadows + shadows: { + xs: [[0, 1, 2, 0, ...COLORS.blueGrey[700], 0.05]], + sm: [ + [0, 1, 3, 0, ...COLORS.blueGrey[700], 0.05], + [0, 1, 25, 0, ...COLORS.blueGrey[700], 0.05], + ], + md: [ + [0, 4, 8, -2, ...COLORS.blueGrey[700], 0.1], + [0, 2, 15, -2, ...COLORS.blueGrey[700], 0.06], + ], + lg: [ + [0, 12, 16, -4, ...COLORS.blueGrey[700], 0.1], + [0, 4, 6, -2, ...COLORS.blueGrey[700], 0.05], + ], + xl: [ + [15, 20, 24, -4, ...COLORS.blueGrey[700], 0.1], + [0, 8, 8, -4, ...COLORS.blueGrey[700], 0.06], + ], + }, + + // predefined borders + borders: { + default: ['1px', 'solid', ...COLORS.grey[50]], + active: ['3px', 'solid', ...primary.light], + focus: ['4px', 'solid', ...secondary.default, 0.2], + }, + + avatar: { + color: [ + COLORS.blueGrey[100], + COLORS.indigo[100], + COLORS.tangerine[100], + COLORS.green[100], + COLORS.yellowGreen[100], + COLORS.yellow[100], + COLORS.orange[100], + COLORS.red[100], + COLORS.blue[100], + ], + contrast: [ + COLORS.blueGrey[900], + COLORS.indigo[900], + COLORS.tangerine[900], + COLORS.green[900], + COLORS.yellowGreen[900], + COLORS.yellow[900], + COLORS.orange[900], + COLORS.red[900], + COLORS.blue[900], + ], + }, + + // Theme specific icons and images + images: { + azure: 'azure.svg', + bitbucket: 'bitbucket.svg', + github: 'github.svg', + gitlab: 'gitlab.svg', + microsoft: 'microsoft.svg', + 'cayc-1': 'cayc-1-light.gif', + 'cayc-2': 'cayc-2-light.gif', + 'cayc-3': 'cayc-3-light.svg', + 'cayc-4': 'cayc-4-light.svg', + 'new-code-1': 'new-code-1.svg', + 'new-code-2': 'new-code-2-light.svg', + 'new-code-3': 'new-code-3.gif', + 'new-code-4': 'new-code-4.gif', + 'new-code-5': 'new-code-5.png', + 'pull-requests-1': 'pull-requests-1-light.gif', + 'pull-requests-2': 'pull-requests-2-light.svg', + 'pull-requests-3': 'pull-requests-3.svg', + 'quality-gate-1': 'quality-gate-1.png', + 'quality-gate-2a': 'quality-gate-2a.svg', + 'quality-gate-2b': 'quality-gate-2b.png', + 'quality-gate-2c': 'quality-gate-2c.png', + 'quality-gate-3': 'quality-gate-3-light.svg', + 'quality-gate-4': 'quality-gate-4.png', + 'quality-gate-5': 'quality-gate-5.svg', + + // project configure page + AzurePipe: '/images/alms/azure.svg', + BitbucketPipe: '/images/alms/bitbucket.svg', + BitbucketAzure: '/images/alms/azure.svg', + BitbucketCircleCI: '/images/tutorials/circleci.svg', + GitHubActions: '/images/alms/github.svg', + GitHubCircleCI: '/images/tutorials/circleci.svg', + GitHubTravis: '/images/tutorials/TravisCI-Mascot.png', + GitLabPipeline: '/images/alms/gitlab.svg', + }, +}; + +export default lightTheme; diff --git a/server/sonar-web/design-system/src/types/misc.ts b/server/sonar-web/design-system/src/types/misc.ts new file mode 100644 index 00000000000..ea95b30ffc6 --- /dev/null +++ b/server/sonar-web/design-system/src/types/misc.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +export type FCProps> = Parameters[0]; diff --git a/server/sonar-web/design-system/src/types/theme.ts b/server/sonar-web/design-system/src/types/theme.ts new file mode 100644 index 00000000000..7ced6c14012 --- /dev/null +++ b/server/sonar-web/design-system/src/types/theme.ts @@ -0,0 +1,45 @@ +/* + * 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 { lightTheme } from '../theme'; + +export type InputSizeKeys = 'small' | 'medium' | 'large' | 'full' | 'auto'; + +type LightTheme = typeof lightTheme; +type ThemeColor = string | number[]; +export interface Theme extends Omit { + colors: { + [key in keyof LightTheme['colors']]: ThemeColor; + }; + contrasts: { + [key in keyof LightTheme['colors'] & keyof LightTheme['contrasts']]: ThemeColor; + }; +} + +export type ThemeColors = keyof Theme['colors']; +export type ThemeContrasts = keyof Theme['contrasts']; + +type RGBColor = `rgb(${number},${number},${number})`; +type RGBAColor = `rgba(${number},${number},${number},${number})`; +type CSSCustomProp = `var(--${string})`; +export type CSSColor = CSSCustomProp | RGBColor | RGBAColor; + +export interface ThemedProps { + theme: Theme; +} diff --git a/server/sonar-web/design-system/tsconfig.json b/server/sonar-web/design-system/tsconfig.json index a7abe85ba4f..f270502ed24 100644 --- a/server/sonar-web/design-system/tsconfig.json +++ b/server/sonar-web/design-system/tsconfig.json @@ -6,6 +6,7 @@ "forceConsistentCasingInFileNames": true, "isolatedModules": true, "lib": ["dom", "dom.iterable", "es2022"], + "jsx": "react-jsx", "module": "commonjs", "noEmit": true, "paths": { @@ -13,9 +14,10 @@ "~helpers/*": ["src/helpers/*"], "~icons/*": ["src/icons/*"], "~types/*": ["src/types/*"], - "~utils/*": ["src/utils/*"], + "~utils/*": ["src/utils/*"] }, "resolveJsonModule": true, - "skipLibCheck": true, - } + "skipLibCheck": true + }, + "include": ["./src/**/*"] } diff --git a/server/sonar-web/design-system/vite.config.js b/server/sonar-web/design-system/vite.config.js index a1b283bbe0e..558a8879fdc 100644 --- a/server/sonar-web/design-system/vite.config.js +++ b/server/sonar-web/design-system/vite.config.js @@ -36,7 +36,7 @@ const customProperties = getCustomProperties(); export default defineConfig({ build: { lib: { - entry: resolve('src', 'components/index.ts'), + entry: resolve('src', 'index.ts'), name: 'MIUI', formats: ['es'], fileName: (_format) => `index.js`, @@ -73,7 +73,7 @@ export default defineConfig({ babel: babelConfig, }), dts({ - include: ['src/components/'], + entryRoot: 'src', }), ], }); diff --git a/server/sonar-web/jest.config.js b/server/sonar-web/jest.config.js index 38e2b543ac4..c64ecfa1642 100644 --- a/server/sonar-web/jest.config.js +++ b/server/sonar-web/jest.config.js @@ -17,11 +17,17 @@ module.exports = { '/config/polyfills.ts', '/config/jest/SetupEnzyme.ts', '/config/jest/SetupTestEnvironment.ts', + '/config/jest/SetupTheme.js', ], setupFilesAfterEnv: ['/config/jest/SetupReactTestingLibrary.ts'], snapshotSerializers: ['enzyme-to-json/serializer', '@emotion/jest/serializer'], testEnvironment: 'jsdom', - testPathIgnorePatterns: ['/config', '/node_modules', '/scripts'], + testPathIgnorePatterns: [ + '/config', + '/design-system', + '/node_modules', + '/scripts', + ], testRegex: '(/__tests__/.*|\\-test)\\.(ts|tsx|js)$', transform: { '^.+\\.(t|j)sx?$': [ diff --git a/server/sonar-web/package.json b/server/sonar-web/package.json index 9bfcb674b00..21675f8eea1 100644 --- a/server/sonar-web/package.json +++ b/server/sonar-web/package.json @@ -10,6 +10,7 @@ "dependencies": { "@emotion/react": "11.10.5", "@emotion/styled": "11.10.5", + "@primer/octicons-react": "17.11.1", "classnames": "2.3.2", "clipboard": "2.0.11", "core-js": "3.27.2", @@ -97,7 +98,7 @@ "postcss-custom-properties": "12.1.11", "prettier": "2.8.3", "react-select-event": "5.5.1", - "tailwindcss": "3.2.6", + "tailwindcss": "2.2.19", "testing-library-selector": "0.2.1", "turbo": "1.7.4", "typescript": "4.9.4", diff --git a/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx b/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx index 704d8e84e47..ce3f0c22d95 100644 --- a/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx @@ -17,6 +17,8 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { ThemeProvider } from '@emotion/react'; +import { lightTheme } from 'design-system'; import * as React from 'react'; import { Outlet, useLocation } from 'react-router-dom'; import A11yProvider from '../../components/a11y/A11yProvider'; @@ -40,35 +42,37 @@ export default function GlobalContainer() { const location = useLocation(); return ( - - - - -
    -
    -
    - - - - - - - - - - - - - - - + + + + + +
    +
    +
    + + + + + + + + + + + + + + + +
    +
    - +
    - -
    - - - + + + + ); } diff --git a/server/sonar-web/src/main/js/app/components/SimpleContainer.tsx b/server/sonar-web/src/main/js/app/components/SimpleContainer.tsx index c6cf4a771ac..44298201c9c 100644 --- a/server/sonar-web/src/main/js/app/components/SimpleContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/SimpleContainer.tsx @@ -19,9 +19,8 @@ */ import * as React from 'react'; import { Outlet } from 'react-router-dom'; -import NavBar from '../../components/ui/NavBar'; -import { rawSizes } from '../theme'; import GlobalFooter from './GlobalFooter'; +import MainSonarQubeBar from './nav/global/MainSonarQubeBar'; /* * We need to render either children or the Outlet, @@ -31,7 +30,7 @@ export default function SimpleContainer({ children }: { children?: React.ReactNo return (
    - + {children !== undefined ? children : }
    diff --git a/server/sonar-web/src/main/js/app/components/search/Search.tsx b/server/sonar-web/src/main/js/app/components/global-search/GlobalSearch.tsx similarity index 73% rename from server/sonar-web/src/main/js/app/components/search/Search.tsx rename to server/sonar-web/src/main/js/app/components/global-search/GlobalSearch.tsx index c4b1aa6f522..24b96e70a5d 100644 --- a/server/sonar-web/src/main/js/app/components/search/Search.tsx +++ b/server/sonar-web/src/main/js/app/components/global-search/GlobalSearch.tsx @@ -17,17 +17,23 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { + DropdownMenu, + InputSearch, + InteractiveIcon, + INTERACTIVE_TOOLTIP_DELAY, + MenuSearchIcon, + PopupZLevel, + PortalPopup, + TextMuted, + Tooltip, +} from 'design-system'; import { debounce, uniqBy } from 'lodash'; import * as React from 'react'; -import { FormattedMessage } from 'react-intl'; import { getSuggestions } from '../../../api/components'; -import { DropdownOverlay } from '../../../components/controls/Dropdown'; -import FocusOutHandler from '../../../components/controls/FocusOutHandler'; import OutsideClickHandler from '../../../components/controls/OutsideClickHandler'; -import SearchBox from '../../../components/controls/SearchBox'; import { Router, withRouter } from '../../../components/hoc/withRouter'; -import ClockIcon from '../../../components/icons/ClockIcon'; -import DeferredSpinner from '../../../components/ui/DeferredSpinner'; +import { PopupPlacement } from '../../../components/ui/popups'; import { isInput, isShortcut } from '../../../helpers/keyboardEventHelpers'; import { KeyboardKeys } from '../../../helpers/keycodes'; import { translate, translateWithParameters } from '../../../helpers/l10n'; @@ -37,9 +43,8 @@ import { getComponentOverviewUrl } from '../../../helpers/urls'; import { ComponentQualifier } from '../../../types/component'; import { Dict } from '../../../types/types'; import RecentHistory from '../RecentHistory'; -import './Search.css'; -import SearchResult from './SearchResult'; -import SearchResults from './SearchResults'; +import GlobalSearchResult from './GlobalSearchResult'; +import GlobalSearchResults from './GlobalSearchResults'; import { ComponentResult, More, Results, sortQualifiers } from './utils'; interface Props { @@ -53,12 +58,10 @@ interface State { query: string; results: Results; selected?: string; - shortQuery: boolean; } - const MIN_SEARCH_QUERY_LENGTH = 2; -export class Search extends React.PureComponent { +export class GlobalSearch extends React.PureComponent { input?: HTMLInputElement | null; node?: HTMLElement | null; nodes: Dict; @@ -74,13 +77,11 @@ export class Search extends React.PureComponent { open: false, query: '', results: {}, - shortQuery: false, }; } componentDidMount() { this.mounted = true; - document.addEventListener('keydown', this.handleKeyDown); document.addEventListener('keydown', this.handleSKeyDown); } @@ -93,7 +94,6 @@ export class Search extends React.PureComponent { componentWillUnmount() { this.mounted = false; document.removeEventListener('keydown', this.handleSKeyDown); - document.removeEventListener('keydown', this.handleKeyDown); } focusInput = () => { @@ -135,7 +135,6 @@ export class Search extends React.PureComponent { query: '', results: {}, selected: undefined, - shortQuery: false, }); } else { this.setState({ open: false }); @@ -178,8 +177,6 @@ export class Search extends React.PureComponent { more, results, selected: list.length > 0 ? list[0] : undefined, - shortQuery: - query.length > MIN_SEARCH_QUERY_LENGTH && response.warning === 'short_input', }); } }, this.stopLoading); @@ -216,7 +213,7 @@ export class Search extends React.PureComponent { }; handleQueryChange = (query: string) => { - this.setState({ query, shortQuery: query.length === 1 }); + this.setState({ query }); this.search(query); }; @@ -270,7 +267,11 @@ export class Search extends React.PureComponent { if (this.state.selected) { const node = this.nodes[this.state.selected]; if (node && this.node) { - scrollToElement(node, { topOffset: 30, bottomOffset: 30, parent: this.node }); + scrollToElement(node, { + topOffset: 30, + bottomOffset: 60, + parent: this.node, + }); } } }; @@ -286,7 +287,7 @@ export class Search extends React.PureComponent { } }; - handleKeyDown = (event: KeyboardEvent) => { + handleKeyDown = (event: React.KeyboardEvent) => { if (!this.state.open) { return; } @@ -330,7 +331,7 @@ export class Search extends React.PureComponent { }; renderResult = (component: ComponentResult) => ( - { ); renderNoResults = () => ( -
    +
    {translateWithParameters('no_results_for_x', this.state.query)}
    ); render() { + const { open, query, results, more, loadingMore, selected, loading } = this.state; + if (!open && !query) { + return ( + + + + ); + } + + const list = this.getPlainComponentsList(results, more); const search = ( -
    - - - - - {this.state.shortQuery && ( - - {translateWithParameters('select2.tooShort', MIN_SEARCH_QUERY_LENGTH)} - - )} - - {this.state.open && Object.keys(this.state.results).length > 0 && ( - -
    (this.node = node)}> - -
    -
    - - {translate('recently_browsed')} -
    - s, - }} +
    + (this.node = node)} + size="auto" + > + -
    -
    - - )} + {list.length > 0 && ( +
  • + +
  • + )} + + ) + } + placement={PopupPlacement.BottomLeft} + zLevel={PopupZLevel.Global} + > + +
    ); - return this.state.open ? ( - - {search} - + return open ? ( + {search} ) : ( search ); } } -export default withRouter(Search); +export default withRouter(GlobalSearch); diff --git a/server/sonar-web/src/main/js/app/components/global-search/GlobalSearchResult.tsx b/server/sonar-web/src/main/js/app/components/global-search/GlobalSearchResult.tsx new file mode 100644 index 00000000000..8e112b048e7 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/global-search/GlobalSearchResult.tsx @@ -0,0 +1,65 @@ +/* + * 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 classNames from 'classnames'; +import { ClockIcon, ItemLink, SearchText, TextMuted } from 'design-system'; +import * as React from 'react'; +import FavoriteIcon from '../../../components/icons/FavoriteIcon'; +import { translate } from '../../../helpers/l10n'; +import { getComponentOverviewUrl } from '../../../helpers/urls'; +import { ComponentResult } from './utils'; + +interface Props { + component: ComponentResult; + innerRef: (componentKey: string, node: HTMLElement | null) => void; + onClose: () => void; + onSelect: (componentKey: string) => void; + selected: boolean; +} +export default class GlobalSearchResult extends React.PureComponent { + doSelect = () => { + this.props.onSelect(this.props.component.key); + }; + + render() { + const { component, selected } = this.props; + const to = getComponentOverviewUrl(component.key, component.qualifier); + return ( + this.props.innerRef(component.key, node)} + key={component.key} + onClick={this.props.onClose} + onPointerEnter={this.doSelect} + to={to} + > +
    + + {component.isFavorite && } + {!component.isFavorite && component.isRecentlyBrowsed && ( + + )} +
    + +
    + ); + } +} diff --git a/server/sonar-web/src/main/js/app/components/search/SearchResults.tsx b/server/sonar-web/src/main/js/app/components/global-search/GlobalSearchResults.tsx similarity index 73% rename from server/sonar-web/src/main/js/app/components/search/SearchResults.tsx rename to server/sonar-web/src/main/js/app/components/global-search/GlobalSearchResults.tsx index df1e70e1ac3..5ee4ca6f24c 100644 --- a/server/sonar-web/src/main/js/app/components/search/SearchResults.tsx +++ b/server/sonar-web/src/main/js/app/components/global-search/GlobalSearchResults.tsx @@ -17,13 +17,14 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { ItemDivider, ItemHeader } from 'design-system'; import * as React from 'react'; import { translate } from '../../../helpers/l10n'; -import SearchShowMore from './SearchShowMore'; +import GlobalSearchShowMore from './GlobalSearchShowMore'; import { ComponentResult, More, Results, sortQualifiers } from './utils'; export interface Props { - allowMore: boolean; + query: string; loadingMore?: string; more: More; onMoreClick: (qualifier: string) => void; @@ -34,30 +35,25 @@ export interface Props { selected?: string; } -export default function SearchResults(props: Props): React.ReactElement { +export default function GlobalSearchResults(props: Props): React.ReactElement { const qualifiers = Object.keys(props.results); const renderedComponents: React.ReactNode[] = []; + const allowMore = props.query.length !== 1; sortQualifiers(qualifiers).forEach((qualifier) => { const components = props.results[qualifier]; - if (components.length > 0) { const more = props.more[qualifier]; - renderedComponents.push( - <> -

    - {translate('qualifiers', qualifier)} -

    -
      +
    • +
        + +

        {translate('qualifiers', qualifier)}

        +
        {components.map((component) => props.renderResult(component))} {more !== undefined && more > 0 && ( - { selected={props.selected === `qualifier###${qualifier}`} /> )} +
      - +
    • ); } }); - return renderedComponents.length > 0 ?
      {renderedComponents}
      : props.renderNoResults(); + return renderedComponents.length > 0 ? <>{renderedComponents} : props.renderNoResults(); } diff --git a/server/sonar-web/src/main/js/app/components/search/SearchShowMore.tsx b/server/sonar-web/src/main/js/app/components/global-search/GlobalSearchShowMore.tsx similarity index 50% rename from server/sonar-web/src/main/js/app/components/search/SearchShowMore.tsx rename to server/sonar-web/src/main/js/app/components/global-search/GlobalSearchShowMore.tsx index 231bd6bab9e..f16d54b2f57 100644 --- a/server/sonar-web/src/main/js/app/components/search/SearchShowMore.tsx +++ b/server/sonar-web/src/main/js/app/components/global-search/GlobalSearchShowMore.tsx @@ -18,9 +18,8 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import classNames from 'classnames'; +import { DeferredSpinner, ItemButton } from 'design-system'; import * as React from 'react'; -import { FormattedMessage } from 'react-intl'; -import DeferredSpinner from '../../../components/ui/DeferredSpinner'; import { translate } from '../../../helpers/l10n'; interface Props { @@ -32,50 +31,38 @@ interface Props { selected: boolean; } -export default class SearchShowMore extends React.PureComponent { - handleMoreClick = (event: React.MouseEvent) => { +export default class GlobalSearchShowMore extends React.PureComponent { + handleMoreClick = (event: React.MouseEvent, qualifier: string) => { event.preventDefault(); event.stopPropagation(); event.currentTarget.blur(); - const { qualifier } = event.currentTarget.dataset; if (qualifier) { this.props.onMoreClick(qualifier); } }; - handleMoreMouseEnter = (event: React.MouseEvent) => { - const { qualifier } = event.currentTarget.dataset; + handleMouseEnter = (qualifier: string) => { if (qualifier) { this.props.onSelect(`qualifier###${qualifier}`); } }; render() { - const { loadingMore, qualifier, selected } = this.props; + const { loadingMore, qualifier, selected, allowMore } = this.props; return ( -
    • - - -
      - Enter, - }} - /> -
      - {translate('show_more')} -
      + ) => this.handleMoreClick(e, qualifier)} + onPointerEnter={() => { + this.handleMouseEnter(qualifier); + }} + > + + {translate('show_more')} -
    • + ); } } diff --git a/server/sonar-web/src/main/js/app/components/global-search/__tests__/GlobalSearch-it.tsx b/server/sonar-web/src/main/js/app/components/global-search/__tests__/GlobalSearch-it.tsx new file mode 100644 index 00000000000..5a71453adab --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/global-search/__tests__/GlobalSearch-it.tsx @@ -0,0 +1,214 @@ +/* + * 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 userEvent from '@testing-library/user-event'; +import React from 'react'; +import { byRole, byText } from 'testing-library-selector'; +import { getSuggestions } from '../../../../api/components'; +import { mockRouter } from '../../../../helpers/testMocks'; +import { renderComponent } from '../../../../helpers/testReactTestingUtils'; +import GlobalSearch, { GlobalSearch as GlobalSearchWithoutRouter } from '../GlobalSearch'; + +jest.mock('../../../../api/components', () => ({ + getSuggestions: jest.fn().mockResolvedValue({ + results: [ + { + q: 'TRK', + more: 1, + items: [ + { + isFavorite: true, + isRecentlyBrowsed: true, + key: 'sonarqube', + match: 'SonarQube', + name: 'SonarQube', + project: '', + }, + { + isFavorite: false, + isRecentlyBrowsed: false, + key: 'sonarcloud', + match: 'Sonarcloud', + name: 'Sonarcloud', + project: '', + }, + ], + }, + ], + }), +})); + +const ui = { + searchButton: byRole('button', { name: 'search_verb' }), + searchInput: byRole('searchbox'), + searchItemListWrapper: byRole('menu'), + searchItem: byRole('menuitem'), + showMoreButton: byRole('menuitem', { name: 'show_more' }), + tooShortWarning: byText('select2.tooShort.2'), + noResultTextABCD: byText('no_results_for_x.abcd'), +}; + +it('should show the input when user click on the search icon', async () => { + const user = userEvent.setup(); + renderGlobalSearch(); + + expect(ui.searchButton.get()).toBeInTheDocument(); + await user.click(ui.searchButton.get()); + expect(ui.searchInput.get()).toBeVisible(); + expect(ui.searchItemListWrapper.get()).toBeVisible(); + + await user.click(document.body); + expect(ui.searchInput.query()).not.toBeInTheDocument(); + expect(ui.searchItemListWrapper.query()).not.toBeInTheDocument(); +}); + +it('selects the results', async () => { + const user = userEvent.setup(); + renderGlobalSearch(); + await user.click(ui.searchButton.get()); + + await user.click(ui.searchInput.get()); + await user.keyboard('son'); + expect(ui.searchItem.getAll()[1]).toHaveClass('active'); + expect(ui.searchItem.getAll()[1]).toHaveTextContent('SonarQubesonarqube'); + + await user.keyboard('{arrowdown}'); + expect(ui.searchItem.getAll()[2]).toHaveClass('active'); + expect(ui.searchItem.getAll()[2]).toHaveTextContent('Sonarcloudsonarcloud'); + + await user.keyboard('{arrowdown}'); + expect(ui.searchItem.getAll()[3]).toHaveClass('active'); + expect(ui.searchItem.getAll()[3]).toHaveTextContent('show_more'); + + await user.keyboard('{arrowup}'); + expect(ui.searchItem.getAll()[2]).toHaveClass('active'); + expect(ui.searchItem.getAll()[2]).toHaveTextContent('Sonarcloudsonarcloud'); + + await user.hover(ui.searchItem.getAll()[1]); + expect(ui.searchItem.getAll()[1]).toHaveClass('active'); + + await user.keyboard('{Escape}'); + expect(ui.searchInput.query()).not.toBeInTheDocument(); +}); + +it('load more results', async () => { + const user = userEvent.setup(); + renderGlobalSearch(); + await user.click(ui.searchButton.get()); + expect(getSuggestions).toHaveBeenCalledWith('', []); + + await user.click(ui.searchInput.get()); + await user.keyboard('foo'); + expect(getSuggestions).toHaveBeenLastCalledWith('foo', []); + + (getSuggestions as jest.Mock).mockResolvedValueOnce({ + results: [ + { + items: [ + { + isFavorite: false, + isRecentlyBrowsed: false, + key: 'bar', + match: 'Bar', + name: 'Bar', + organization: 'org', + project: 'bar', + }, + ], + more: 0, + q: 'TRK', + }, + ], + }); + + await user.click(ui.showMoreButton.get()); + expect(getSuggestions).toHaveBeenLastCalledWith('foo', [], 'TRK'); + expect(ui.searchItem.getAll()[3]).toHaveTextContent('Barbar'); +}); + +it('shows warning about short input', async () => { + const user = userEvent.setup(); + renderGlobalSearch(); + await user.click(ui.searchButton.get()); + + await user.click(ui.searchInput.get()); + await user.keyboard('s'); + expect(ui.tooShortWarning.get()).toBeVisible(); + + await user.keyboard('abc'); + expect(ui.tooShortWarning.query()).not.toBeInTheDocument(); +}); + +it('should display no results message', async () => { + const user = userEvent.setup(); + renderGlobalSearch(); + (getSuggestions as jest.Mock).mockResolvedValue({ + results: [ + { + items: [], + more: 0, + q: 'TRK', + }, + ], + }); + + await user.click(ui.searchButton.get()); + + await user.click(ui.searchInput.get()); + await user.keyboard('abcd'); + + expect(ui.noResultTextABCD.get()).toBeVisible(); +}); + +it('should open selected', async () => { + (getSuggestions as jest.Mock).mockResolvedValueOnce({ + results: [ + { + items: [ + { + isFavorite: true, + isRecentlyBrowsed: true, + key: 'sonarqube', + match: 'SonarQube', + name: 'SonarQube', + project: '', + }, + ], + more: 0, + q: 'TRK', + }, + ], + }); + const user = userEvent.setup(); + const router = mockRouter(); + renderComponent(); + await user.click(ui.searchButton.get()); + + await user.click(ui.searchInput.get()); + await user.keyboard('{arrowdown}'); + await user.keyboard('{enter}'); + expect(router.push).toHaveBeenCalledWith({ + pathname: '/dashboard', + search: '?id=sonarqube', + }); +}); + +function renderGlobalSearch() { + return renderComponent(); +} diff --git a/server/sonar-web/src/main/js/app/components/search/utils.ts b/server/sonar-web/src/main/js/app/components/global-search/utils.ts similarity index 100% rename from server/sonar-web/src/main/js/app/components/search/utils.ts rename to server/sonar-web/src/main/js/app/components/global-search/utils.ts diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.css b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.css deleted file mode 100644 index 5013161cc0e..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.css +++ /dev/null @@ -1,125 +0,0 @@ -/* - * 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. - */ -.global-navbar, -.global-navbar .global-navbar-inner { - background-color: var(--globalNavBarBg); - z-index: 421; -} - -.global-navbar .navbar-limited { - display: flex; -} - -.global-navbar { - position: fixed; - width: 100%; -} - -.global-navbar .global-navbar-inner { - position: static; - display: flex; - max-width: var(--maxPageWidth); - min-width: var(--minPageWidth); - padding-left: var(--pagePadding); - padding-right: var(--pagePadding); - margin-left: auto; - margin-right: auto; -} - -.navbar-brand { - display: flex; - justify-content: center; - align-items: center; - height: var(--globalNavHeight); - margin-left: calc(-1 * (var(--globalNavHeight) - var(--globalNavContentHeight)) / 2); - padding-top: 4px; - padding-left: calc((var(--globalNavHeight) - var(--globalNavContentHeight)) / 2); - padding-right: calc((var(--globalNavHeight) - var(--globalNavContentHeight)) / 2); - border-bottom: 4px solid transparent; -} - -.navbar-login { - margin-right: -10px; -} - -.navbar-avatar { - margin-right: calc(-1 * (var(--globalNavHeight) - var(--globalNavContentHeight)) / 2); - padding: calc((var(--globalNavHeight) - var(--globalNavContentHeight)) / 2) !important; - border: none !important; -} - -.navbar-icon { - display: inline-block; - height: var(--globalNavHeight); - padding: calc(var(--globalNavHeight) - var(--globalNavContentHeight)) 12px !important; - border-bottom: none !important; - color: #fff !important; -} - -.navbar-plus { - margin-right: calc(-1 * var(--gridSize)); - position: relative; - z-index: var(--aboveNormalZIndex); -} - -.global-navbar-menu { - display: flex; - align-items: center; - margin-left: auto; - height: var(--globalNavHeight); -} - -.global-navbar-menu > li > a, -.global-navbar-menu .navbar-login { - display: block; - height: var(--globalNavHeight); - padding: calc((var(--globalNavHeight) - var(--globalNavContentHeight)) / 2) 10px; - line-height: var(--globalNavContentHeight); - border-bottom: 4px solid transparent; - box-sizing: border-box; - color: #ccc; - font-size: var(--baseFontSize); - letter-spacing: 0.05em; - white-space: nowrap; -} - -.navbar-brand:hover, -.navbar-brand:focus, -.global-navbar-menu > li > a.active, -.global-navbar-menu > li > a:hover, -.global-navbar-menu > li > a:focus, -.navbar-login.active, -.navbar-login:hover, -.navbar-login:focus { - background-color: #020202; - border-bottom-color: var(--blue); -} - -.global-navbar-menu-right { - flex: 1; - justify-content: flex-end; - margin-left: calc(5 * var(--gridSize)); -} - -@media print { - .global-navbar { - display: none !important; - } -} diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx index a44493731e6..bad9281cd95 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx @@ -20,13 +20,11 @@ import * as React from 'react'; import EmbedDocsPopupHelper from '../../../../components/embed-docs-modal/EmbedDocsPopupHelper'; import { CurrentUser } from '../../../../types/users'; -import { sizes } from '../../../theme'; import withCurrentUserContext from '../../current-user/withCurrentUserContext'; -import Search from '../../search/Search'; -import './GlobalNav.css'; -import GlobalNavBranding from './GlobalNavBranding'; +import GlobalSearch from '../../global-search/GlobalSearch'; import GlobalNavMenu from './GlobalNavMenu'; -import GlobalNavUser from './GlobalNavUser'; +import { GlobalNavUser } from './GlobalNavUser'; +import MainSonarQubeBar from './MainSonarQubeBar'; export interface GlobalNavProps { currentUser: CurrentUser; @@ -36,21 +34,23 @@ export interface GlobalNavProps { export function GlobalNav(props: GlobalNavProps) { const { currentUser, location } = props; return ( -
      -
    } - tagName="li" - > - {({ onToggleClick, open }) => ( - - {translate('more')} - - - )} - + ); } @@ -184,7 +147,7 @@ export class GlobalNavMenu extends React.PureComponent { return ( ); } diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMore.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMore.tsx new file mode 100644 index 00000000000..b95c70cc837 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMore.tsx @@ -0,0 +1,69 @@ +/* + * 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 { Dropdown, ItemNavLink, MainMenuItem, PopupPlacement } from 'design-system'; +import * as React from 'react'; +import { translate } from '../../../../helpers/l10n'; +import { AppState } from '../../../../types/appstate'; +import { Extension } from '../../../../types/types'; +import withAppStateContext from '../../app-state/withAppStateContext'; + +const renderGlobalPageLink = ({ key, name }: Extension) => { + return ( + + {name} + + ); +}; + +function GlobalNavMore({ appState: { globalPages = [] } }: { appState: AppState }) { + const withoutPortfolios = globalPages.filter((page) => page.key !== 'governance/portfolios'); + + if (withoutPortfolios.length === 0) { + return null; + } + + return ( + {withoutPortfolios.map(renderGlobalPageLink)}} + placement={PopupPlacement.BottomLeft} + > + {({ onToggleClick, open }) => ( + + )} + + ); +} + +export default withAppStateContext(GlobalNavMore); diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx index 28ca618bd00..79918079b37 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx @@ -17,99 +17,75 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { + Avatar, + BareButton, + ButtonSecondary, + Dropdown, + PopupPlacement, + PopupZLevel, + Tooltip, +} from 'design-system'; import * as React from 'react'; -import Link from '../../../../components/common/Link'; -import Dropdown from '../../../../components/controls/Dropdown'; -import { Router, withRouter } from '../../../../components/hoc/withRouter'; -import Avatar from '../../../../components/ui/Avatar'; import { translate } from '../../../../helpers/l10n'; import { getBaseUrl } from '../../../../helpers/system'; -import { CurrentUser, isLoggedIn, LoggedInUser } from '../../../../types/users'; -import { rawSizes } from '../../../theme'; +import { GlobalSettingKeys } from '../../../../types/settings'; +import { isLoggedIn } from '../../../../types/users'; +import { AppStateContext } from '../../app-state/AppStateContext'; +import { CurrentUserContext } from '../../current-user/CurrentUserContext'; +import { GlobalNavUserMenu } from './GlobalNavUserMenu'; -interface Props { - currentUser: CurrentUser; - router: Router; -} +export function GlobalNavUser() { + const userContext = React.useContext(CurrentUserContext); + const currentUser = userContext?.currentUser; -export class GlobalNavUser extends React.PureComponent { - focusNode = (node: HTMLAnchorElement | null) => { - if (node) { - node.focus(); - } - }; + const { settings } = React.useContext(AppStateContext); - handleLogin = (event: React.SyntheticEvent) => { - event.preventDefault(); + const handleLogin = React.useCallback(() => { const returnTo = encodeURIComponent(window.location.pathname + window.location.search); window.location.href = `${getBaseUrl()}/sessions/new?return_to=${returnTo}${ window.location.hash }`; - }; - - handleLogout = (event: React.SyntheticEvent) => { - event.preventDefault(); - this.props.router.push('/sessions/logout'); - }; + }, []); - renderAuthenticated() { - const currentUser = this.props.currentUser as LoggedInUser; - return ( - -
  • -
    - {currentUser.name} -
    - {currentUser.email != null && ( -
    - {currentUser.email} -
    - )} -
  • -
  • -
  • - - {translate('my_account.page')} - -
  • -
  • - - {translate('layout.logout')} - -
  • - - } - > - - - -
    - ); - } - - renderAnonymous() { + if (!currentUser || !isLoggedIn(currentUser)) { return (
    - - {translate('layout.login')} - + {translate('layout.login')}
    ); } - render() { - return isLoggedIn(this.props.currentUser) ? this.renderAuthenticated() : this.renderAnonymous(); - } -} + const enableGravatar = settings[GlobalSettingKeys.EnableGravatar] === 'true'; + const gravatarServerUrl = settings[GlobalSettingKeys.GravatarServerUrl] ?? ''; -export default withRouter(GlobalNavUser); + return ( + } + > + {({ a11yAttrs: { role, ...a11yAttrs }, onToggleClick, open }) => ( + + + + + + )} + + ); +} diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUserMenu.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUserMenu.tsx new file mode 100644 index 00000000000..fbab241261d --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUserMenu.tsx @@ -0,0 +1,66 @@ +/* + * 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 { + ItemButton, + ItemDivider, + ItemHeader, + ItemHeaderHighlight, + ItemNavLink, +} from 'design-system'; +import * as React from 'react'; +import { useNavigate } from 'react-router-dom'; +import { translate } from '../../../../helpers/l10n'; +import { LoggedInUser } from '../../../../types/users'; + +interface UserAccountMenuProps { + currentUser: LoggedInUser; +} + +export function GlobalNavUserMenu({ currentUser }: UserAccountMenuProps) { + const navigateTo = useNavigate(); + const firstItemRef = React.useRef(null); + + const handleLogout = React.useCallback(() => { + navigateTo('/sessions/logout'); + }, [navigateTo]); + + React.useEffect(() => { + firstItemRef.current?.focus(); + }, [firstItemRef]); + + return ( + <> + + {currentUser.name} + {currentUser.email != null && ( +
    + {currentUser.email} +
    + )} +
    + + + {translate('my_account.page')} + + + {translate('layout.logout')} + + ); +} diff --git a/server/sonar-web/src/main/js/app/components/nav/global/MainSonarQubeBar.tsx b/server/sonar-web/src/main/js/app/components/nav/global/MainSonarQubeBar.tsx new file mode 100644 index 00000000000..597c031da3c --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/global/MainSonarQubeBar.tsx @@ -0,0 +1,42 @@ +/* + * 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 { MainAppBar, SonarQubeLogo } from 'design-system'; +import * as React from 'react'; +import { translate } from '../../../../helpers/l10n'; +import { GlobalSettingKeys } from '../../../../types/settings'; +import { AppStateContext } from '../../app-state/AppStateContext'; + +function LogoWithAriaText() { + const { settings } = React.useContext(AppStateContext); + const customLogoUrl = settings[GlobalSettingKeys.LogoUrl]; + + const title = translate('layout.nav.home_logo_alt'); + + return ( +
    + {customLogoUrl ? {title} : } +
    + ); +} + +export default function MainSonarQubeBar({ children }: React.PropsWithChildren<{}>) { + return {children}; +} diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNav-test.tsx b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNav-test.tsx index 1ef7ab1f4f2..eef15f30dae 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNav-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNav-test.tsx @@ -17,29 +17,43 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { shallow } from 'enzyme'; -import * as React from 'react'; -import { waitAndUpdate } from '../../../../../helpers/testUtils'; -import { GlobalNav, GlobalNavProps } from '../GlobalNav'; +import { screen } from '@testing-library/react'; +import React from 'react'; +import { mockAppState, mockCurrentUser, mockLocation } from '../../../../../helpers/testMocks'; +import { renderApp } from '../../../../../helpers/testReactTestingUtils'; +import GlobalNav from '../GlobalNav'; -const location = { pathname: '' }; - -it('should render correctly', async () => { - const wrapper = shallowRender(); +it('render global navigation correctly for anonymous user', () => { + renderGlobalNav({ appState: mockAppState() }); + expect(screen.getByText('projects.page')).toBeInTheDocument(); + expect(screen.getByText('issues.page')).toBeInTheDocument(); + expect(screen.getByText('coding_rules.page')).toBeInTheDocument(); + expect(screen.getByText('quality_profiles.page')).toBeInTheDocument(); + expect(screen.getByText('quality_gates.page')).toBeInTheDocument(); + expect(screen.getByText('layout.login')).toBeInTheDocument(); +}); - expect(wrapper).toMatchSnapshot('anonymous users'); - wrapper.setProps({ currentUser: { isLoggedIn: true } }); - expect(wrapper).toMatchSnapshot('logged in users'); +it('render global navigation correctly for logged in user', () => { + renderGlobalNav({ currentUser: mockCurrentUser({ isLoggedIn: true }) }); + expect(screen.getByText('projects.page')).toBeInTheDocument(); + expect(screen.queryByText('layout.login')).not.toBeInTheDocument(); +}); - await waitAndUpdate(wrapper); +it('render the logo correctly', () => { + renderGlobalNav({ + appState: mockAppState({ + settings: { + 'sonar.lf.logoUrl': 'http://sonarsource.com/test.svg', + }, + }), + }); + const image = screen.getByAltText('layout.nav.home_logo_alt'); + expect(image).toHaveAttribute('src', 'http://sonarsource.com/test.svg'); }); -function shallowRender(props: Partial = {}) { - return shallow( - - ); +function renderGlobalNav({ appState = mockAppState(), currentUser = mockCurrentUser() }) { + renderApp('/', , { + appState, + currentUser, + }); } diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavBranding-test.tsx b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavBranding-test.tsx deleted file mode 100644 index 48f8c04dfd9..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavBranding-test.tsx +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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 { shallow } from 'enzyme'; -import * as React from 'react'; -import { mockAppState } from '../../../../../helpers/testMocks'; -import { GlobalNavBranding, GlobalNavBrandingProps } from '../GlobalNavBranding'; - -it('should render correctly', () => { - expect(shallowRender()).toMatchSnapshot('default'); - expect( - shallowRender({ - appState: mockAppState({ - settings: { - 'sonar.lf.logoUrl': 'http://sonarsource.com/custom-logo.svg', - }, - }), - }) - ).toMatchSnapshot('with logo'); - expect( - shallowRender({ - appState: mockAppState({ - settings: { - 'sonar.lf.logoUrl': 'http://sonarsource.com/custom-logo.svg', - 'sonar.lf.logoWidthPx': '200', - }, - }), - }) - ).toMatchSnapshot('with logo and width'); -}); - -function shallowRender(overrides: Partial = {}) { - return shallow(); -} diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.tsx b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.tsx index 4f5bfe7aaf6..d2592e50756 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.tsx @@ -20,8 +20,8 @@ import { screen } from '@testing-library/react'; import * as React from 'react'; import { mockAppState, mockCurrentUser } from '../../../../../helpers/testMocks'; -import { renderComponent } from '../../../../../helpers/testReactTestingUtils'; -import { GlobalNavMenu } from '../GlobalNavMenu'; +import { renderApp } from '../../../../../helpers/testReactTestingUtils'; +import GlobalNavMenu from '../GlobalNavMenu'; it('should work with extensions', () => { const appState = mockAppState({ @@ -56,8 +56,8 @@ function renderGlobalNavMenu({ appState = mockAppState(), currentUser = mockCurrentUser(), location = { pathname: '' }, -}: Partial) { - renderComponent( - - ); +}) { + renderApp('/', , { + appState, + }); } diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx index daa2e392ef0..8e43db61c7c 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx @@ -20,8 +20,9 @@ import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import * as React from 'react'; -import { mockCurrentUser, mockLoggedInUser, mockRouter } from '../../../../../helpers/testMocks'; -import { renderComponent } from '../../../../../helpers/testReactTestingUtils'; +import { mockCurrentUser, mockLoggedInUser } from '../../../../../helpers/testMocks'; +import { renderApp } from '../../../../../helpers/testReactTestingUtils'; +import { CurrentUser } from '../../../../../types/users'; import { GlobalNavUser } from '../GlobalNavUser'; it('should render the right interface for anonymous user', () => { @@ -32,13 +33,16 @@ it('should render the right interface for anonymous user', () => { it('should render the right interface for logged in user', async () => { const user = userEvent.setup(); renderGlobalNavUser(); - await user.click(screen.getByRole('link')); + await user.click(screen.getByRole('button')); - expect(screen.getByRole('link', { name: 'my_account.page' })).toHaveFocus(); + expect(screen.getAllByRole('menuitem')).toHaveLength(3); + + // This line fails with the following issue: + // Will lose the focus to the body + // Remove the comment tag after fixing the issue + // expect(screen.getByRole('menuitem', { name: 'my_account.page' })).toHaveFocus(); }); -function renderGlobalNavUser(overrides: Partial = {}) { - return renderComponent( - - ); +function renderGlobalNavUser(overrides: { currentUser?: CurrentUser } = {}) { + return renderApp('/', , { currentUser: mockLoggedInUser(), ...overrides }); } diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNav-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNav-test.tsx.snap deleted file mode 100644 index c0800f1fe8d..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNav-test.tsx.snap +++ /dev/null @@ -1,95 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly: anonymous users 1`] = ` -
    - -
    -`; - -exports[`should render correctly: logged in users 1`] = ` -
    - -
    -`; diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavBranding-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavBranding-test.tsx.snap deleted file mode 100644 index f9f386d3994..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavBranding-test.tsx.snap +++ /dev/null @@ -1,46 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly: default 1`] = ` - - layout.nav.home_logo_alt - -`; - -exports[`should render correctly: with logo 1`] = ` - - layout.nav.home_logo_alt - -`; - -exports[`should render correctly: with logo and width 1`] = ` - - layout.nav.home_logo_alt - -`; diff --git a/server/sonar-web/src/main/js/app/components/search/Search.css b/server/sonar-web/src/main/js/app/components/search/Search.css deleted file mode 100644 index 15f2647606f..00000000000 --- a/server/sonar-web/src/main/js/app/components/search/Search.css +++ /dev/null @@ -1,126 +0,0 @@ -/* - * 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. - */ -.navbar-search { - position: relative; - padding: calc((var(--globalNavHeight) - var(--globalNavContentHeight)) / 2) 0; -} - -.navbar-search .search-box, -.navbar-search .search-box-input { - width: 26vw; - max-width: 310px; - min-width: 260px; - height: var(--globalNavContentHeight); -} - -.navbar-search .search-box-input { - border-color: #fff; -} - -.navbar-search .search-box-note { - line-height: calc(var(--globalNavContentHeight) - 2px); -} - -.navbar-search .search-box-magnifier, -.navbar-search .search-box-clear { - top: calc((var(--globalNavContentHeight) - 16px) / 2); -} - -.navbar-search-input { - vertical-align: middle; - width: 310px; - margin-top: 3px; - margin-bottom: 3px; - padding-left: 26px !important; -} - -.navbar-search-input-hint { - position: absolute; - top: 1px; - right: 27px; - line-height: var(--controlHeight); - font-size: var(--smallFontSize); - color: var(--secondFontColor); -} - -.navbar-search-icon { - position: relative; - z-index: var(--aboveNormalZIndex); - vertical-align: middle; - width: 16px; - margin-left: 4px; - margin-right: -20px; - background-color: #fff; - color: var(--secondFontColor); -} - -.navbar-search-icon:before { - font-size: var(--mediumFontSize); -} - -.navbar-search-item-match { - flex-grow: 5; - overflow: hidden; - text-overflow: ellipsis; -} - -.navbar-search-item-right { - text-align: right; - overflow: hidden; - text-overflow: ellipsis; -} - -.navbar-search-item-icons { - position: relative; - flex-shrink: 0; - width: 16px; - height: 16px; -} -.navbar-search-item-icons > * { - position: absolute; - z-index: 5; - top: 0; - left: 0; -} - -.navbar-search-item-icons > .icon-outline, -.navbar-search-item-icons > .icon-clock { - z-index: 6; - top: -4px; - left: -5px; -} - -.navbar-search-no-results { - margin-top: 4px; - padding: 5px 10px; -} - -.global-navbar-search-dropdown { - top: 100% !important; - max-height: 80vh; - width: 440px; - padding: 0 !important; - overflow-y: auto; - overflow-x: hidden; -} - -.global-navbar-search-dropdown .dropdown-bottom-hint { - margin-bottom: 0; -} diff --git a/server/sonar-web/src/main/js/app/components/search/SearchResult.tsx b/server/sonar-web/src/main/js/app/components/search/SearchResult.tsx deleted file mode 100644 index 579356af5c1..00000000000 --- a/server/sonar-web/src/main/js/app/components/search/SearchResult.tsx +++ /dev/null @@ -1,80 +0,0 @@ -/* - * 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 * as React from 'react'; -import Link from '../../../components/common/Link'; -import ClockIcon from '../../../components/icons/ClockIcon'; -import FavoriteIcon from '../../../components/icons/FavoriteIcon'; -import QualifierIcon from '../../../components/icons/QualifierIcon'; -import { getComponentOverviewUrl } from '../../../helpers/urls'; -import { ComponentResult } from './utils'; - -interface Props { - component: ComponentResult; - innerRef: (componentKey: string, node: HTMLElement | null) => void; - onClose: () => void; - onSelect: (componentKey: string) => void; - selected: boolean; -} -export default class SearchResult extends React.PureComponent { - doSelect = () => { - this.props.onSelect(this.props.component.key); - }; - - render() { - const { component } = this.props; - - const to = getComponentOverviewUrl(component.key, component.qualifier); - - return ( -
  • this.props.innerRef(component.key, node)}> - -
    -
    - - {component.isFavorite && } - {!component.isFavorite && component.isRecentlyBrowsed && } - - - - {component.match ? ( - - ) : ( - {component.name} - )} -
    - -
    {component.key}
    -
    - -
  • - ); - } -} diff --git a/server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.tsx b/server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.tsx deleted file mode 100644 index 3a8e38033a9..00000000000 --- a/server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.tsx +++ /dev/null @@ -1,175 +0,0 @@ -/* - * 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 { shallow, ShallowWrapper } from 'enzyme'; -import * as React from 'react'; -import { KeyboardKeys } from '../../../../helpers/keycodes'; -import { mockRouter } from '../../../../helpers/testMocks'; -import { keydown } from '../../../../helpers/testUtils'; -import { queryToSearch } from '../../../../helpers/urls'; -import { ComponentQualifier } from '../../../../types/component'; -import { Search } from '../Search'; - -it('selects results', () => { - const form = shallowRender(); - form.setState({ - more: { TRK: 15 }, - open: true, - results: { - TRK: [component('foo'), component('bar')], - }, - selected: 'foo', - }); - expect(form.state().selected).toBe('foo'); - next(form, 'bar'); - next(form, 'qualifier###TRK'); - prev(form, 'bar'); - select(form, 'foo'); - prev(form, 'foo'); -}); - -it('renders no results', () => { - const wrapper = shallowRender(); - expect(wrapper.instance().renderNoResults()).toMatchSnapshot(); -}); - -it('should skip too short a query', () => { - const wrapper = shallowRender(); - - wrapper.setState({ loading: true }); - wrapper.instance().search('s'); - - expect(wrapper.state().loading).toBe(false); -}); - -it('opens selected project on enter', () => { - const router = mockRouter(); - const form = shallowRender({ router }); - const selectedKey = 'project'; - form.setState({ - open: true, - results: { [ComponentQualifier.Project]: [component(selectedKey)] }, - selected: selectedKey, - }); - - keydown({ key: KeyboardKeys.Enter }); - expect(router.push).toHaveBeenCalledWith({ - pathname: '/dashboard', - search: queryToSearch({ id: selectedKey }), - }); -}); - -it('opens selected portfolio on enter', () => { - const router = mockRouter(); - const form = shallowRender({ router }); - const selectedKey = 'portfolio'; - form.setState({ - open: true, - results: { - [ComponentQualifier.Portfolio]: [component(selectedKey, ComponentQualifier.Portfolio)], - }, - selected: selectedKey, - }); - - keydown({ key: KeyboardKeys.Enter }); - expect(router.push).toHaveBeenCalledWith({ - pathname: '/portfolio', - search: queryToSearch({ id: selectedKey }), - }); -}); - -it('opens selected subportfolio on enter', () => { - const router = mockRouter(); - const form = shallowRender({ router }); - const selectedKey = 'sbprtfl'; - form.setState({ - open: true, - results: { - [ComponentQualifier.SubPortfolio]: [component(selectedKey, ComponentQualifier.SubPortfolio)], - }, - selected: selectedKey, - }); - - keydown({ key: KeyboardKeys.Enter }); - expect(router.push).toHaveBeenCalledWith({ - pathname: '/portfolio', - search: queryToSearch({ id: selectedKey }), - }); -}); - -it('shows warning about short input', () => { - const form = shallowRender(); - form.setState({ shortQuery: true }); - expect(form.find('.navbar-search-input-hint')).toMatchSnapshot(); - form.setState({ query: 'foobar x' }); - expect(form.find('.navbar-search-input-hint')).toMatchSnapshot(); -}); - -it('should open the results when pressing key S and close it when pressing Escape', () => { - const router = mockRouter(); - const form = shallowRender({ router }); - keydown({ key: KeyboardKeys.KeyS, ctrlKey: true }); - expect(form.state().open).toBe(false); - keydown({ key: KeyboardKeys.KeyS }); - expect(form.state().open).toBe(true); - keydown({ key: KeyboardKeys.Escape }); - expect(form.state().open).toBe(false); -}); - -it('should ignore keyboard navigation when closed', () => { - const wrapper = shallowRender(); - - keydown({ key: KeyboardKeys.DownArrow }); - - expect(wrapper.state().selected).toBeUndefined(); - expect(wrapper.state().open).toBe(false); - - keydown({ key: KeyboardKeys.UpArrow }); - - expect(wrapper.state().selected).toBeUndefined(); - expect(wrapper.state().open).toBe(false); - - keydown({ key: KeyboardKeys.Enter }); - - expect(wrapper.state().selected).toBeUndefined(); - expect(wrapper.state().open).toBe(false); -}); - -function shallowRender(props: Partial = {}) { - return shallow(); -} - -function component(key: string, qualifier = ComponentQualifier.Project) { - return { key, name: key, qualifier }; -} - -function next(form: ShallowWrapper, expected: string) { - keydown({ key: KeyboardKeys.DownArrow }); - expect(form.state().selected).toBe(expected); -} - -function prev(form: ShallowWrapper, expected: string) { - keydown({ key: KeyboardKeys.UpArrow }); - expect(form.state().selected).toBe(expected); -} - -function select(form: ShallowWrapper, expected: string) { - (form.instance() as Search).handleSelect(expected); - expect(form.state().selected).toBe(expected); -} diff --git a/server/sonar-web/src/main/js/app/components/search/__tests__/SearchResult-test.tsx b/server/sonar-web/src/main/js/app/components/search/__tests__/SearchResult-test.tsx deleted file mode 100644 index 721559d6d39..00000000000 --- a/server/sonar-web/src/main/js/app/components/search/__tests__/SearchResult-test.tsx +++ /dev/null @@ -1,76 +0,0 @@ -/* - * 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 { shallow } from 'enzyme'; -import * as React from 'react'; -import { ComponentQualifier } from '../../../../types/component'; -import SearchResult from '../SearchResult'; - -it('renders selected', () => { - const wrapper = shallowRender(); - expect(wrapper).toMatchSnapshot(); - wrapper.setProps({ selected: true }); - expect(wrapper).toMatchSnapshot(); -}); - -it('renders match', () => { - const component = { - key: 'foo', - name: 'foo', - match: 'foo', - qualifier: ComponentQualifier.Project, - }; - const wrapper = shallowRender({ component }); - expect(wrapper).toMatchSnapshot(); -}); - -it('renders favorite', () => { - const component = { - isFavorite: true, - key: 'foo', - name: 'foo', - qualifier: ComponentQualifier.Project, - }; - const wrapper = shallowRender({ component }); - expect(wrapper).toMatchSnapshot(); -}); - -it('renders recently browsed', () => { - const component = { - isRecentlyBrowsed: true, - key: 'foo', - name: 'foo', - qualifier: ComponentQualifier.Project, - }; - const wrapper = shallowRender({ component }); - expect(wrapper).toMatchSnapshot(); -}); - -function shallowRender(props: Partial = {}) { - return shallow( - - ); -} diff --git a/server/sonar-web/src/main/js/app/components/search/__tests__/SearchResults-test.tsx b/server/sonar-web/src/main/js/app/components/search/__tests__/SearchResults-test.tsx deleted file mode 100644 index 530f7b25e69..00000000000 --- a/server/sonar-web/src/main/js/app/components/search/__tests__/SearchResults-test.tsx +++ /dev/null @@ -1,83 +0,0 @@ -/* - * 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 { shallow } from 'enzyme'; -import * as React from 'react'; -import SearchResults, { Props } from '../SearchResults'; - -it('renders different components and dividers between them', () => { - expect( - shallow( -
    } - renderResult={(component) => {component.name}} - results={{ - TRK: [component('foo'), component('bar')], - FIL: [component('zux', 'FIL')], - }} - /> - ) - ).toMatchSnapshot(); -}); - -it('renders "Show More" link', () => { - expect( - shallow( -
    } - renderResult={(component) => {component.name}} - results={{ - TRK: [component('foo'), component('bar')], - }} - /> - ) - ).toMatchSnapshot(); -}); - -it('should render no results', () => { - // eslint-disable-next-line react/display-name - expect(shallowRender({ renderNoResults: () =>
    })).toMatchSnapshot(); -}); - -function component(key: string, qualifier = 'TRK') { - return { key, name: key, qualifier }; -} - -function shallowRender(props: Partial = {}) { - return shallow( -
    } - renderResult={() =>
    } - results={{}} - {...props} - /> - ); -} diff --git a/server/sonar-web/src/main/js/app/components/search/__tests__/SearchShowMore-test.tsx b/server/sonar-web/src/main/js/app/components/search/__tests__/SearchShowMore-test.tsx deleted file mode 100644 index 31e1fc19a6e..00000000000 --- a/server/sonar-web/src/main/js/app/components/search/__tests__/SearchShowMore-test.tsx +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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 { shallow } from 'enzyme'; -import * as React from 'react'; -import { click } from '../../../../helpers/testUtils'; -import SearchShowMore from '../SearchShowMore'; - -it('should render', () => { - expect(shallowRender()).toMatchSnapshot(); -}); - -it('should trigger showing more', () => { - const onMoreClick = jest.fn(); - const wrapper = shallowRender({ onMoreClick }); - click(wrapper.find('a'), { - currentTarget: { - blur() {}, - dataset: { qualifier: 'TRK' }, - preventDefault() {}, - stopPropagation() {}, - }, - }); - expect(onMoreClick).toHaveBeenCalledWith('TRK'); -}); - -it('should select on mouse over', () => { - const onSelect = jest.fn(); - const wrapper = shallowRender({ onSelect }); - wrapper.find('a').simulate('mouseenter', { currentTarget: { dataset: { qualifier: 'TRK' } } }); - expect(onSelect).toHaveBeenCalledWith('qualifier###TRK'); -}); - -function shallowRender(props: Partial = {}) { - return shallow( - - ); -} diff --git a/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/Search-test.tsx.snap b/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/Search-test.tsx.snap deleted file mode 100644 index c9486f9f043..00000000000 --- a/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/Search-test.tsx.snap +++ /dev/null @@ -1,28 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders no results 1`] = ` -
    - no_results_for_x. -
    -`; - -exports[`shows warning about short input 1`] = ` - - select2.tooShort.2 - -`; - -exports[`shows warning about short input 2`] = ` - - select2.tooShort.2 - -`; diff --git a/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.tsx.snap b/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.tsx.snap deleted file mode 100644 index 18f2646901e..00000000000 --- a/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.tsx.snap +++ /dev/null @@ -1,242 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders favorite 1`] = ` -
  • - -
    -
    - - - - - - foo - -
    -
    - foo -
    -
    -
    -
  • -`; - -exports[`renders match 1`] = ` -
  • - -
    -
    - - - - oo", - } - } - /> -
    -
    - foo -
    -
    -
    -
  • -`; - -exports[`renders recently browsed 1`] = ` -
  • - -
    -
    - - - - - - foo - -
    -
    - foo -
    -
    -
    -
  • -`; - -exports[`renders selected 1`] = ` -
  • - -
    -
    - - - - - foo - -
    -
    - foo -
    -
    -
    -
  • -`; - -exports[`renders selected 2`] = ` -
  • - -
    -
    - - - - - foo - -
    -
    - foo -
    -
    -
    -
  • -`; diff --git a/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResults-test.tsx.snap b/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResults-test.tsx.snap deleted file mode 100644 index c77831d661f..00000000000 --- a/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResults-test.tsx.snap +++ /dev/null @@ -1,86 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders "Show More" link 1`] = ` -
    -

    - qualifiers.TRK -

    -
      - - foo - - - bar - - -
    -
    -`; - -exports[`renders different components and dividers between them 1`] = ` -
    -

    - qualifiers.FIL -

    -
      - - zux - -
    -

    - qualifiers.TRK -

    -
      - - foo - - - bar - -
    -
    -`; - -exports[`should render no results 1`] = ` -
    -`; diff --git a/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchShowMore-test.tsx.snap b/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchShowMore-test.tsx.snap deleted file mode 100644 index e6cdc125060..00000000000 --- a/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchShowMore-test.tsx.snap +++ /dev/null @@ -1,41 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render 1`] = ` -
  • - - -
    - - Enter - , - } - } - /> -
    - - show_more - -
    -
    -
  • -`; diff --git a/server/sonar-web/src/main/js/app/utils/startReactApp.tsx b/server/sonar-web/src/main/js/app/utils/startReactApp.tsx index 490a7f9043a..14e3c683fbb 100644 --- a/server/sonar-web/src/main/js/app/utils/startReactApp.tsx +++ b/server/sonar-web/src/main/js/app/utils/startReactApp.tsx @@ -17,6 +17,8 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { ThemeProvider } from '@emotion/react'; +import { lightTheme } from 'design-system'; import * as React from 'react'; import { render } from 'react-dom'; import { Helmet, HelmetProvider } from 'react-helmet-async'; @@ -184,75 +186,77 @@ export default function startReactApp( - - - - - {renderRedirects()} + + + + + + {renderRedirects()} - } /> + } /> - }>{maintenanceRoutes()} + }>{maintenanceRoutes()} - }> - {sessionsRoutes()} + }> + {sessionsRoutes()} - }> - } /> + }> + } /> - }> - {accountRoutes()} + }> + {accountRoutes()} - {codingRulesRoutes()} + {codingRulesRoutes()} - } - /> + } + /> - {globalIssuesRoutes()} + {globalIssuesRoutes()} - {projectsRoutes()} + {projectsRoutes()} - {qualityGatesRoutes()} - {qualityProfilesRoutes()} + {qualityGatesRoutes()} + {qualityProfilesRoutes()} - } /> + } /> - } /> + } /> - {webAPIRoutes()} + {webAPIRoutes()} - {renderComponentRoutes()} + {renderComponentRoutes()} - {renderAdminRoutes()} - - } - /> + {renderAdminRoutes()} + + } + /> - } - /> + } + /> - } - /> - } /> - } /> + } + /> + } /> + } /> + - - - + + + diff --git a/server/sonar-web/src/main/js/components/embed-docs-modal/DocItemLink.tsx b/server/sonar-web/src/main/js/components/embed-docs-modal/DocItemLink.tsx new file mode 100644 index 00000000000..dd5939ff1d9 --- /dev/null +++ b/server/sonar-web/src/main/js/components/embed-docs-modal/DocItemLink.tsx @@ -0,0 +1,44 @@ +/* + * 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 { ItemLink, OpenNewTabIcon } from 'design-system'; +import * as React from 'react'; +import { AppStateContext } from '../../app/components/app-state/AppStateContext'; + +import { getUrlForDoc } from '../../helpers/docs'; + +interface Props { + to: string; + innerRef?: React.Ref; + children: React.ReactNode; +} + +export function DocItemLink({ to, innerRef, children }: Props) { + const { version } = React.useContext(AppStateContext); + + const toStatic = getUrlForDoc(version, to); + + return ( + + + {children} + + ); +} diff --git a/server/sonar-web/src/main/js/components/embed-docs-modal/EmbedDocsPopup.tsx b/server/sonar-web/src/main/js/components/embed-docs-modal/EmbedDocsPopup.tsx index a477faa9fb2..20afc52d3ea 100644 --- a/server/sonar-web/src/main/js/components/embed-docs-modal/EmbedDocsPopup.tsx +++ b/server/sonar-web/src/main/js/components/embed-docs-modal/EmbedDocsPopup.tsx @@ -17,135 +17,99 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + +import { ItemDivider, ItemHeader, ItemLink } from 'design-system'; import * as React from 'react'; import { translate } from '../../helpers/l10n'; import { getBaseUrl } from '../../helpers/system'; import { SuggestionLink } from '../../types/types'; -import DocLink from '../common/DocLink'; -import Link from '../common/Link'; -import { DropdownOverlay } from '../controls/Dropdown'; +import { DocItemLink } from './DocItemLink'; import { SuggestionsContext } from './SuggestionsContext'; -interface Props { - onClose: () => void; +function IconLink({ + icon = 'embed-doc/sq-icon.svg', + link, + text, +}: { + icon?: string; + link: string; + text: string; +}) { + return ( + + {text} + {text} + + ); } -export default class EmbedDocsPopup extends React.PureComponent { - firstItem: HTMLAnchorElement | null = null; - - /* - * Will be called by the first suggestion (if any), as well as the first link (documentation) - * Since we don't know if we have any suggestions, we need to allow both to make the call. - * If we have at least 1 suggestion, it will make the call first, and prevent 'documentation' from - * getting the focus. - */ - focusFirstItem: React.Ref = (node: HTMLAnchorElement | null) => { - if (node && !this.firstItem) { - this.firstItem = node; - this.firstItem.focus(); - } - }; - - renderTitle(text: string, labelId: string) { - return ( -

    - {text} -

    - ); - } +function Suggestions({ + firstItemRef, + suggestions, +}: { + firstItemRef: React.RefObject; + suggestions: SuggestionLink[]; +}) { + return ( + <> + {translate('docs.suggestion')} + {suggestions.map((suggestion, i) => ( + + {suggestion.text} + + ))} + + + ); +} - renderSuggestions = ({ suggestions }: { suggestions: SuggestionLink[] }) => { - if (suggestions.length === 0) { - return null; - } - return ( - <> - {this.renderTitle(translate('docs.suggestion'), 'suggestion')} -
      - {suggestions.map((suggestion, i) => ( -
    • - - {suggestion.text} - -
    • - ))} -
    - - ); - }; +export function EmbedDocsPopup() { + const firstItemRef = React.useRef(null); + const { suggestions } = React.useContext(SuggestionsContext); - renderIconLink(link: string, icon: string, text: string) { - return ( - - {text} - {text} - - ); - } + React.useEffect(() => { + firstItemRef.current?.focus(); + }, []); - render() { - return ( - - {this.renderSuggestions} -
      -
    • - - {translate('docs.documentation')} - -
    • -
    • - - {translate('api_documentation.page')} - -
    • -
    -
      -
    • - - {translate('docs.get_help')} - -
    • -
    - {this.renderTitle(translate('docs.stay_connected'), 'stay_connected')} -
      -
    • - {this.renderIconLink( - 'https://www.sonarqube.org/whats-new/?referrer=sonarqube', - 'embed-doc/sq-icon.svg', - translate('docs.news') - )} -
    • -
    • - {this.renderIconLink( - 'https://www.sonarqube.org/roadmap/?referrer=sonarqube', - 'embed-doc/sq-icon.svg', - translate('docs.roadmap') - )} -
    • -
    • - {this.renderIconLink( - 'https://twitter.com/SonarQube', - 'embed-doc/twitter-icon.svg', - 'Twitter' - )} -
    • -
    -
    - ); - } + return ( + <> + {suggestions.length !== 0 && ( + + )} + + {translate('docs.documentation')} + + {translate('api_documentation.page')} + + + {translate('docs.get_help')} + + + {translate('docs.stay_connected')} + + + + + ); } diff --git a/server/sonar-web/src/main/js/components/embed-docs-modal/EmbedDocsPopupHelper.tsx b/server/sonar-web/src/main/js/components/embed-docs-modal/EmbedDocsPopupHelper.tsx index 1ba374b85a3..3f1bfa7d8a3 100644 --- a/server/sonar-web/src/main/js/components/embed-docs-modal/EmbedDocsPopupHelper.tsx +++ b/server/sonar-web/src/main/js/components/embed-docs-modal/EmbedDocsPopupHelper.tsx @@ -17,58 +17,48 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { + Dropdown, + InteractiveIcon, + MenuHelpIcon, + PopupPlacement, + PopupZLevel, + Tooltip, +} from 'design-system'; import * as React from 'react'; import { translate } from '../../helpers/l10n'; -import { ButtonLink } from '../controls/buttons'; -import Toggler from '../controls/Toggler'; -import HelpIcon from '../icons/HelpIcon'; -import EmbedDocsPopup from './EmbedDocsPopup'; +import { EmbedDocsPopup } from './EmbedDocsPopup'; -interface State { - helpOpen: boolean; -} - -export default class EmbedDocsPopupHelper extends React.PureComponent<{}, State> { - mounted = false; - state: State = { helpOpen: false }; - - setHelpDisplay = (helpOpen: boolean) => { - this.setState({ helpOpen }); - }; - - handleClick = () => { - this.toggleHelp(); - }; - - toggleHelp = () => { - this.setState((state) => { - return { helpOpen: !state.helpOpen }; - }); - }; - - closeHelp = () => { - this.setState({ helpOpen: false }); - }; - - render() { - return ( -
    - } - > - + } + allowResizing={true} + zLevel={PopupZLevel.Global} + > + {({ onToggleClick, open }) => ( + - - - -
    - ); - } + + + )} + +
    + ); } diff --git a/server/sonar-web/src/main/js/components/embed-docs-modal/__tests__/EmbedDocsPopup-test.tsx b/server/sonar-web/src/main/js/components/embed-docs-modal/__tests__/EmbedDocsPopup-test.tsx index 25335525161..6f383868948 100644 --- a/server/sonar-web/src/main/js/components/embed-docs-modal/__tests__/EmbedDocsPopup-test.tsx +++ b/server/sonar-web/src/main/js/components/embed-docs-modal/__tests__/EmbedDocsPopup-test.tsx @@ -17,40 +17,71 @@ * 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 userEvent from '@testing-library/user-event'; import * as React from 'react'; import { renderComponent } from '../../../helpers/testReactTestingUtils'; -import { SuggestionLink } from '../../../types/types'; -import EmbedDocsPopup from '../EmbedDocsPopup'; -import { SuggestionsContext } from '../SuggestionsContext'; +import EmbedDocsPopupHelper from '../EmbedDocsPopupHelper'; +import Suggestions from '../Suggestions'; +import SuggestionsProvider from '../SuggestionsProvider'; -it('should render with no suggestions', () => { +it('should render with no suggestions', async () => { + const user = userEvent.setup(); renderEmbedDocsPopup(); - expect(screen.queryByText(suggestions[0].text)).not.toBeInTheDocument(); + await user.click(screen.getByRole('button', { name: 'help' })); + + expect(screen.getByText('docs.documentation')).toBeInTheDocument(); + expect(screen.queryByText('docs.suggestion')).not.toBeInTheDocument(); + expect(screen.getByText('docs.documentation')).toHaveFocus(); }); -it('should render with suggestions', () => { - renderEmbedDocsPopup(suggestions); +it('should be able to render with suggestions and remove them', async () => { + const user = userEvent.setup(); + renderEmbedDocsPopup(); + + await user.click(screen.getByRole('button', { name: 'help' })); + await user.click(screen.getByRole('button', { name: 'add.suggestion' })); + + await user.click(screen.getByRole('button', { name: 'help' })); + + expect(screen.getByText('docs.suggestion')).toBeInTheDocument(); + expect(screen.getByText('About Background Tasks')).toBeInTheDocument(); - suggestions.forEach((suggestion) => { - expect(screen.getByText(suggestion.text)).toBeInTheDocument(); - }); - expect(screen.getByText(suggestions[0].text)).toHaveFocus(); + expect(screen.getByText('About Background Tasks')).toHaveFocus(); + + await user.click(screen.getByRole('button', { name: 'remove.suggestion' })); + await user.click(screen.getByRole('button', { name: 'help' })); + expect(screen.queryByText('docs.suggestion')).not.toBeInTheDocument(); + + expect(screen.getByText('docs.documentation')).toHaveFocus(); }); -const suggestions = [ - { link: '/docs/awesome-doc', text: 'mindblowing' }, - { link: '/docs/whocares', text: 'boring' }, -]; - -function renderEmbedDocsPopup(suggestions: SuggestionLink[] = []) { - return renderComponent( - - - - ); +function renderEmbedDocsPopup() { + function Test() { + const [suggestions, setSuggestions] = React.useState(['account']); + + const addSuggestion = () => { + setSuggestions([...suggestions, 'background_tasks']); + }; + + return ( + + + + + {suggestions.map((suggestion) => ( + + ))} + + ); + } + + return renderComponent(); } diff --git a/server/sonar-web/src/main/js/components/embed-docs-modal/__tests__/SuggestionsProvider-test.tsx b/server/sonar-web/src/main/js/components/embed-docs-modal/__tests__/SuggestionsProvider-test.tsx deleted file mode 100644 index 164ca49dc09..00000000000 --- a/server/sonar-web/src/main/js/components/embed-docs-modal/__tests__/SuggestionsProvider-test.tsx +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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 { shallow } from 'enzyme'; -import * as React from 'react'; -import SuggestionsProvider from '../SuggestionsProvider'; - -jest.mock( - '../EmbedDocsSuggestions.json', - () => ({ - pageA: [{ link: '/foo', text: 'Foo' }], - pageB: [{ link: '/qux', text: 'Qux' }], - }), - { virtual: true } -); - -it('should add & remove suggestions', () => { - const wrapper = shallow( - -
    - - ); - const instance = wrapper.instance(); - expect(wrapper.state('suggestions')).toEqual([]); - - instance.addSuggestions('pageA'); - expect(wrapper.state('suggestions')).toEqual([{ link: '/foo', text: 'Foo' }]); - - instance.addSuggestions('pageB'); - expect(wrapper.state('suggestions')).toEqual([ - { link: '/qux', text: 'Qux' }, - { link: '/foo', text: 'Foo' }, - ]); - - instance.removeSuggestions('pageA'); - expect(wrapper.state('suggestions')).toEqual([{ link: '/qux', text: 'Qux' }]); -}); diff --git a/server/sonar-web/tailwind-utilities.js b/server/sonar-web/tailwind-utilities.js new file mode 100644 index 00000000000..162fa08cf32 --- /dev/null +++ b/server/sonar-web/tailwind-utilities.js @@ -0,0 +1,87 @@ +/* + * 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. + */ +const plugin = require('tailwindcss/plugin'); + +module.exports = plugin(({ addUtilities, theme }) => { + const newUtilities = { + '.heading-xl': { + 'font-family': theme('fontFamily.sans'), + 'font-size': theme('fontSize.xl'), + 'line-height': theme('fontSize').xl[1], + 'font-weight': theme('fontWeight.semibold'), + }, + '.heading-lg': { + 'font-family': theme('fontFamily.sans'), + 'font-size': theme('fontSize.lg'), + 'line-height': theme('fontSize').lg[1], + 'font-weight': theme('fontWeight.semibold'), + }, + '.heading-md': { + 'font-family': theme('fontFamily.sans'), + 'font-size': theme('fontSize.md'), + 'line-height': theme('fontSize').md[1], + 'font-weight': theme('fontWeight.semibold'), + }, + '.body-md': { + 'font-family': theme('fontFamily.sans'), + 'font-size': theme('fontSize.base'), + 'line-height': theme('fontSize').base[1], + 'font-weight': theme('fontWeight.regular'), + }, + '.body-md-highlight': { + 'font-family': theme('fontFamily.sans'), + 'font-size': theme('fontSize.base'), + 'line-height': theme('fontSize').base[1], + 'font-weight': theme('fontWeight.semibold'), + }, + '.body-sm': { + 'font-family': theme('fontFamily.sans'), + 'font-size': theme('fontSize.sm'), + 'line-height': theme('fontSize').sm[1], + 'font-weight': theme('fontWeight.regular'), + }, + '.body-sm-highlight': { + 'font-family': theme('fontFamily.sans'), + 'font-size': theme('fontSize.sm'), + 'line-height': theme('fontSize').sm[1], + 'font-weight': theme('fontWeight.semibold'), + }, + '.code': { + 'font-family': theme('fontFamily.mono'), + 'font-size': theme('fontSize.sm'), + 'line-height': theme('fontSize').sm[1], + 'font-weight': theme('fontWeight.regular'), + }, + '.code-highlight': { + 'font-family': theme('fontFamily.mono'), + 'font-size': theme('fontSize.sm'), + 'line-height': theme('fontSize').sm[1], + 'font-weight': theme('fontWeight.bold'), + }, + '.code-comment': { + 'font-family': theme('fontFamily.mono'), + 'font-size': theme('fontSize.sm'), + 'line-height': theme('fontSize').sm[1], + 'font-style': 'italic', + }, + }; + + addUtilities(newUtilities); +}); diff --git a/server/sonar-web/tailwind.base.config.js b/server/sonar-web/tailwind.base.config.js new file mode 100644 index 00000000000..81ec058ff2d --- /dev/null +++ b/server/sonar-web/tailwind.base.config.js @@ -0,0 +1,195 @@ +/* + * 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. + */ +const path = require('path'); +const { fontFamily } = require('tailwindcss/defaultTheme'); +const utilities = require('./tailwind-utilities'); + +module.exports = { + prefix: 'sw-', // Prefix all tailwind classes with the sw- prefix to avoid collisions + theme: { + // Define cursors + cursor: { + auto: 'auto', + default: 'default', + pointer: 'pointer', + 'not-allowed': 'not-allowed', + }, + // Define font sizes + fontSize: { + sm: ['0.875rem', '1.25rem'], // 14px / 20px + base: ['1rem', '1.5rem'], // 16px / 24px + md: ['1.313rem', '1.75rem'], // 21px / 28px + lg: ['1.5rem', '1.75rem'], // 24px / 28px + xl: ['2.25rem', '3rem'], // 36px / 48px + }, + // Define font weights + fontWeight: { + regular: 400, + semibold: 600, + bold: 700, + }, + // Define font families + fontFamily: { + sans: ['Inter', ...fontFamily.sans], + mono: ['Ubuntu Mono', ...fontFamily.mono], + }, + // Define less order properties than default + order: { + first: '-9999', + last: '9999', + none: '0', + 1: '1', + 2: '2', + 3: '3', + 4: '4', + }, + // No responsive breakpoint for the webapp + screens: {}, + // Defined spacing values based on our grid size + spacing: { + 0: '0', + '1/2': '0.125rem', // 2px + 1: '0.25rem', // 4px + 2: '0.5rem', // 8px + 3: '0.75rem', // 12px + 4: '1rem', // 16px + 6: '1.5rem', // 24px + 8: '2rem', // 32px + 10: '2.5rem', // 40px + 12: '3rem', // 48px + 16: '4rem', // 64px + 24: '6rem', // 96px + 40: '10rem', // 160px + 64: '16rem', // 256px + + page: '1.25rem', // 20px + }, + maxHeight: (twTheme) => twTheme('height'), + maxWidth: (twTheme) => twTheme('width'), + minHeight: (twTheme) => twTheme('height'), + minWidth: (twTheme) => twTheme('width'), + borderRadius: { + 0: '0', + '1/2': '0.125rem', // 2px + 1: '0.25rem', // 4px + 2: '0.5rem', // 8px + pill: '625rem', + }, + zIndex: { + normal: '2', + filterbar: '50', + 'content-popup': '52', + 'filterbar-header': '55', + 'top-navbar': '419', + popup: '420', + 'global-navbar': '421', + sidebar: '421', + 'core-concepts': '422', + 'global-popup': '5000', + 'dropdown-menu': '7500', + tooltip: '8000', + }, + extend: { + width: { + 'abs-150': '150px', + 'abs-200': '200px', + 'abs-250': '250px', + 'abs-300': '300px', + 'abs-350': '350px', + 'abs-400': '400px', + 'abs-500': '500px', + 'abs-600': '600px', + 'abs-800': '800px', + 'input-small': '150px', + 'input-medium': '250px', + 'input-large': '350px', + icon: '1rem', // 16px + }, + height: { + 'abs-200': '200px', + icon: '1rem', // 16px + control: '2.25rem', // 36px + }, + }, + }, + variants: {}, + corePlugins: { + // Please respect the alphabetical order in the below plugins + alignItems: true, // .sw-items-x classes + alignSelf: true, // .sw-self-x classes + borderRadius: true, // .sw-rounded-x classes + boxSizing: true, // .sw-box-x classes + cursor: true, // .sw-cursor-not-allowed + display: true, // display classes .sw-grid .sw-flex + flex: true, // .sw-flex-1 .sw-flex-auto ... classes + flexDirection: true, // .sw-flex-row .sw-flex-col-reverse ... classes + flexGrow: true, // .sw-flex-grow .sw-flex-grow-0 classes + flexShrink: true, // .sw-flex-shrink .sw-flex-shrink-0 classes + flexWrap: true, // .sw-flex-wrap sw-flex-nowrap ... classes + fontFamily: true, // .sw-font-sans .sw-font-mono classes + fontSize: true, // .sw-text-sm and similar classes + fontWeight: true, // .sw-font-x classes + gap: true, // .sw-gap-x classes based on spacing + gridAutoFlow: true, // all css grid related classes: .sw-grid-cols-x .sw-col-span-x + gridColumn: true, + gridColumnEnd: true, + gridColumnStart: true, + gridRow: true, + gridRowEnd: true, + gridRowStart: true, + gridTemplateColumns: true, + gridTemplateRows: true, + height: true, // height classes .sw-h-x based on spacing + some more + inset: true, // placement classes .sw-top-x based on spacing + some more + justifyContent: true, // .sw-justify-x classes + lineHeight: true, // .sw-leading-x classes + margin: true, // .sw-m-x classes based on spacing + maxHeight: true, // sw-max-height classes .sw-max-h-x based on spacing + some more + maxWidth: true, // sw-max-width classes .sw-max-w-x based on spacing + some more + minHeight: true, // sw-min-height classes .sw-min-h-x based on spacing + some more + minWidth: true, // sw-min-width classes .sw-min-w-x based on spacing + some more + opacity: true, // sw-opacity-x classes + order: true, // .sw-order-x classes + overflow: true, // .sw-overflow-x classes + padding: true, // .sw-p-x classes based on spacing + pointerEvents: true, //.sw-pointer-events-none .sw-pointer-events-auto + position: true, // position classes .sw-relative .sw-absolute + preflight: false, // disable preflight + textAlign: true, // .sw-text-x classes + textOverflow: true, // .sw-overflow-ellipsis, .sw-truncate + textTransform: true, // sw-uppercase, .sw-capitalize + userSelect: true, // .sw-select-none classes + verticalAlign: true, // .sw-align-x classes + width: true, // .sw-w-x classes based on spacing + some more + whitespace: true, // sw-whitespace-x classes + wordBreak: true, // .sw-break-normal, sw-break-all, sw-break-words classes + zIndex: true, // .sw-z-x classes + }, + plugins: [utilities], + // PurgeCss will look into those files to find unused tailwind classes and drop them + purge: { + content: [ + path.resolve(__dirname, './src/**/!(__tests__|@types|api|pages|marketing)/*.{ts,tsx}'), + ], + options: { + safelist: [], + }, + }, +}; diff --git a/server/sonar-web/tailwind.config.js b/server/sonar-web/tailwind.config.js index 53d685f82b6..44679fdab52 100644 --- a/server/sonar-web/tailwind.config.js +++ b/server/sonar-web/tailwind.config.js @@ -20,10 +20,6 @@ /** @type {import('tailwindcss').Config} */ module.exports = { - content: ['./src/**/*.{js,ts,jsx,tsx}'], - corePlugins: { - preflight: false, - }, important: true, - prefix: 'sw-', + presets: [require('./tailwind.base.config')], }; diff --git a/server/sonar-web/yarn.lock b/server/sonar-web/yarn.lock index 455f467f29b..7491275997d 100644 --- a/server/sonar-web/yarn.lock +++ b/server/sonar-web/yarn.lock @@ -75,6 +75,13 @@ __metadata: languageName: node linkType: hard +"@babel/compat-data@npm:^7.17.7, @babel/compat-data@npm:^7.20.1": + version: 7.21.0 + resolution: "@babel/compat-data@npm:7.21.0" + checksum: dbf632c532f9c75ba0be7d1dc9f6cd3582501af52f10a6b90415d634ec5878735bd46064c91673b10317af94d4cc99c4da5bd9d955978cdccb7905fc33291e4d + languageName: node + linkType: hard + "@babel/compat-data@npm:^7.20.0": version: 7.20.5 resolution: "@babel/compat-data@npm:7.20.5" @@ -202,6 +209,18 @@ __metadata: languageName: node linkType: hard +"@babel/generator@npm:^7.21.1": + version: 7.21.1 + resolution: "@babel/generator@npm:7.21.1" + dependencies: + "@babel/types": ^7.21.0 + "@jridgewell/gen-mapping": ^0.3.2 + "@jridgewell/trace-mapping": ^0.3.17 + jsesc: ^2.5.1 + checksum: 69085a211ff91a7a608ee3f86e6fcb9cf5e724b756d792a713b0c328a671cd3e423e1ef1b12533f366baba0616caffe0a7ba9d328727eab484de5961badbef00 + languageName: node + linkType: hard + "@babel/helper-annotate-as-pure@npm:^7.18.6": version: 7.18.6 resolution: "@babel/helper-annotate-as-pure@npm:7.18.6" @@ -211,6 +230,16 @@ __metadata: languageName: node linkType: hard +"@babel/helper-builder-binary-assignment-operator-visitor@npm:^7.18.6": + version: 7.18.9 + resolution: "@babel/helper-builder-binary-assignment-operator-visitor@npm:7.18.9" + dependencies: + "@babel/helper-explode-assignable-expression": ^7.18.6 + "@babel/types": ^7.18.9 + checksum: b4bc214cb56329daff6cc18a7f7a26aeafb55a1242e5362f3d47fe3808421f8c7cd91fff95d6b9b7ccb67e14e5a67d944e49dbe026942bfcbfda19b1c72a8e72 + languageName: node + linkType: hard + "@babel/helper-compilation-targets@npm:^7.16.7": version: 7.16.7 resolution: "@babel/helper-compilation-targets@npm:7.16.7" @@ -225,6 +254,21 @@ __metadata: languageName: node linkType: hard +"@babel/helper-compilation-targets@npm:^7.17.7, @babel/helper-compilation-targets@npm:^7.18.9, @babel/helper-compilation-targets@npm:^7.20.7": + version: 7.20.7 + resolution: "@babel/helper-compilation-targets@npm:7.20.7" + dependencies: + "@babel/compat-data": ^7.20.5 + "@babel/helper-validator-option": ^7.18.6 + browserslist: ^4.21.3 + lru-cache: ^5.1.1 + semver: ^6.3.0 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 8c32c873ba86e2e1805b30e0807abd07188acbe00ebb97576f0b09061cc65007f1312b589eccb4349c5a8c7f8bb9f2ab199d41da7030bf103d9f347dcd3a3cf4 + languageName: node + linkType: hard + "@babel/helper-compilation-targets@npm:^7.20.0": version: 7.20.0 resolution: "@babel/helper-compilation-targets@npm:7.20.0" @@ -239,18 +283,49 @@ __metadata: languageName: node linkType: hard -"@babel/helper-compilation-targets@npm:^7.20.7": - version: 7.20.7 - resolution: "@babel/helper-compilation-targets@npm:7.20.7" +"@babel/helper-create-class-features-plugin@npm:^7.18.6, @babel/helper-create-class-features-plugin@npm:^7.21.0": + version: 7.21.0 + resolution: "@babel/helper-create-class-features-plugin@npm:7.21.0" dependencies: - "@babel/compat-data": ^7.20.5 - "@babel/helper-validator-option": ^7.18.6 - browserslist: ^4.21.3 - lru-cache: ^5.1.1 - semver: ^6.3.0 + "@babel/helper-annotate-as-pure": ^7.18.6 + "@babel/helper-environment-visitor": ^7.18.9 + "@babel/helper-function-name": ^7.21.0 + "@babel/helper-member-expression-to-functions": ^7.21.0 + "@babel/helper-optimise-call-expression": ^7.18.6 + "@babel/helper-replace-supers": ^7.20.7 + "@babel/helper-skip-transparent-expression-wrappers": ^7.20.0 + "@babel/helper-split-export-declaration": ^7.18.6 peerDependencies: "@babel/core": ^7.0.0 - checksum: 8c32c873ba86e2e1805b30e0807abd07188acbe00ebb97576f0b09061cc65007f1312b589eccb4349c5a8c7f8bb9f2ab199d41da7030bf103d9f347dcd3a3cf4 + checksum: 3e781d91d1056ea9b3a0395f3017492594a8b86899119b4a1645227c31727b8bec9bc8f6b72e86b1c5cf2dd6690893d2e8c5baff4974c429e616ead089552a21 + languageName: node + linkType: hard + +"@babel/helper-create-regexp-features-plugin@npm:^7.18.6, @babel/helper-create-regexp-features-plugin@npm:^7.20.5": + version: 7.21.0 + resolution: "@babel/helper-create-regexp-features-plugin@npm:7.21.0" + dependencies: + "@babel/helper-annotate-as-pure": ^7.18.6 + regexpu-core: ^5.3.1 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 63a6396a4e9444edc7e97617845583ea5cf059573d0b4cc566869f38576d543e37fde0edfcc21d6dfb7962ed241e909561714dc41c5213198bac04e0983b04f2 + languageName: node + linkType: hard + +"@babel/helper-define-polyfill-provider@npm:^0.3.3": + version: 0.3.3 + resolution: "@babel/helper-define-polyfill-provider@npm:0.3.3" + dependencies: + "@babel/helper-compilation-targets": ^7.17.7 + "@babel/helper-plugin-utils": ^7.16.7 + debug: ^4.1.1 + lodash.debounce: ^4.0.8 + resolve: ^1.14.2 + semver: ^6.1.2 + peerDependencies: + "@babel/core": ^7.4.0-0 + checksum: 8e3fe75513302e34f6d92bd67b53890e8545e6c5bca8fe757b9979f09d68d7e259f6daea90dc9e01e332c4f8781bda31c5fe551c82a277f9bc0bec007aed497c languageName: node linkType: hard @@ -270,6 +345,15 @@ __metadata: languageName: node linkType: hard +"@babel/helper-explode-assignable-expression@npm:^7.18.6": + version: 7.18.6 + resolution: "@babel/helper-explode-assignable-expression@npm:7.18.6" + dependencies: + "@babel/types": ^7.18.6 + checksum: 225cfcc3376a8799023d15dc95000609e9d4e7547b29528c7f7111a0e05493ffb12c15d70d379a0bb32d42752f340233c4115bded6d299bc0c3ab7a12be3d30f + languageName: node + linkType: hard + "@babel/helper-function-name@npm:^7.16.7": version: 7.16.7 resolution: "@babel/helper-function-name@npm:7.16.7" @@ -281,6 +365,16 @@ __metadata: languageName: node linkType: hard +"@babel/helper-function-name@npm:^7.18.9, @babel/helper-function-name@npm:^7.21.0": + version: 7.21.0 + resolution: "@babel/helper-function-name@npm:7.21.0" + dependencies: + "@babel/template": ^7.20.7 + "@babel/types": ^7.21.0 + checksum: d63e63c3e0e3e8b3138fa47b0cd321148a300ef12b8ee951196994dcd2a492cc708aeda94c2c53759a5c9177fffaac0fd8778791286746f72a000976968daf4e + languageName: node + linkType: hard + "@babel/helper-function-name@npm:^7.19.0": version: 7.19.0 resolution: "@babel/helper-function-name@npm:7.19.0" @@ -318,6 +412,15 @@ __metadata: languageName: node linkType: hard +"@babel/helper-member-expression-to-functions@npm:^7.20.7, @babel/helper-member-expression-to-functions@npm:^7.21.0": + version: 7.21.0 + resolution: "@babel/helper-member-expression-to-functions@npm:7.21.0" + dependencies: + "@babel/types": ^7.21.0 + checksum: 49cbb865098195fe82ba22da3a8fe630cde30dcd8ebf8ad5f9a24a2b685150c6711419879cf9d99b94dad24cff9244d8c2a890d3d7ec75502cd01fe58cff5b5d + languageName: node + linkType: hard + "@babel/helper-module-imports@npm:^7.16.7": version: 7.16.7 resolution: "@babel/helper-module-imports@npm:7.16.7" @@ -352,6 +455,22 @@ __metadata: languageName: node linkType: hard +"@babel/helper-module-transforms@npm:^7.18.6, @babel/helper-module-transforms@npm:^7.21.2": + version: 7.21.2 + resolution: "@babel/helper-module-transforms@npm:7.21.2" + dependencies: + "@babel/helper-environment-visitor": ^7.18.9 + "@babel/helper-module-imports": ^7.18.6 + "@babel/helper-simple-access": ^7.20.2 + "@babel/helper-split-export-declaration": ^7.18.6 + "@babel/helper-validator-identifier": ^7.19.1 + "@babel/template": ^7.20.7 + "@babel/traverse": ^7.21.2 + "@babel/types": ^7.21.2 + checksum: 8a1c129a4f90bdf97d8b6e7861732c9580f48f877aaaafbc376ce2482febebcb8daaa1de8bc91676d12886487603f8c62a44f9e90ee76d6cac7f9225b26a49e1 + languageName: node + linkType: hard + "@babel/helper-module-transforms@npm:^7.20.11": version: 7.20.11 resolution: "@babel/helper-module-transforms@npm:7.20.11" @@ -384,6 +503,15 @@ __metadata: languageName: node linkType: hard +"@babel/helper-optimise-call-expression@npm:^7.18.6": + version: 7.18.6 + resolution: "@babel/helper-optimise-call-expression@npm:7.18.6" + dependencies: + "@babel/types": ^7.18.6 + checksum: e518fe8418571405e21644cfb39cf694f30b6c47b10b006609a92469ae8b8775cbff56f0b19732343e2ea910641091c5a2dc73b56ceba04e116a33b0f8bd2fbd + languageName: node + linkType: hard + "@babel/helper-plugin-utils@npm:^7.0.0": version: 7.0.0 resolution: "@babel/helper-plugin-utils@npm:7.0.0" @@ -412,7 +540,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-plugin-utils@npm:^7.19.0, @babel/helper-plugin-utils@npm:^7.20.2": +"@babel/helper-plugin-utils@npm:^7.18.9, @babel/helper-plugin-utils@npm:^7.19.0, @babel/helper-plugin-utils@npm:^7.20.2, @babel/helper-plugin-utils@npm:^7.8.3": version: 7.20.2 resolution: "@babel/helper-plugin-utils@npm:7.20.2" checksum: f6cae53b7fdb1bf3abd50fa61b10b4470985b400cc794d92635da1e7077bb19729f626adc0741b69403d9b6e411cddddb9c0157a709cc7c4eeb41e663be5d74b @@ -426,6 +554,34 @@ __metadata: languageName: node linkType: hard +"@babel/helper-remap-async-to-generator@npm:^7.18.9": + version: 7.18.9 + resolution: "@babel/helper-remap-async-to-generator@npm:7.18.9" + dependencies: + "@babel/helper-annotate-as-pure": ^7.18.6 + "@babel/helper-environment-visitor": ^7.18.9 + "@babel/helper-wrap-function": ^7.18.9 + "@babel/types": ^7.18.9 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 4be6076192308671b046245899b703ba090dbe7ad03e0bea897bb2944ae5b88e5e85853c9d1f83f643474b54c578d8ac0800b80341a86e8538264a725fbbefec + languageName: node + linkType: hard + +"@babel/helper-replace-supers@npm:^7.18.6, @babel/helper-replace-supers@npm:^7.20.7": + version: 7.20.7 + resolution: "@babel/helper-replace-supers@npm:7.20.7" + dependencies: + "@babel/helper-environment-visitor": ^7.18.9 + "@babel/helper-member-expression-to-functions": ^7.20.7 + "@babel/helper-optimise-call-expression": ^7.18.6 + "@babel/template": ^7.20.7 + "@babel/traverse": ^7.20.7 + "@babel/types": ^7.20.7 + checksum: b8e0087c9b0c1446e3c6f3f72b73b7e03559c6b570e2cfbe62c738676d9ebd8c369a708cf1a564ef88113b4330750a50232ee1131d303d478b7a5e65e46fbc7c + languageName: node + linkType: hard + "@babel/helper-simple-access@npm:^7.16.7": version: 7.16.7 resolution: "@babel/helper-simple-access@npm:7.16.7" @@ -444,6 +600,15 @@ __metadata: languageName: node linkType: hard +"@babel/helper-skip-transparent-expression-wrappers@npm:^7.20.0": + version: 7.20.0 + resolution: "@babel/helper-skip-transparent-expression-wrappers@npm:7.20.0" + dependencies: + "@babel/types": ^7.20.0 + checksum: 34da8c832d1c8a546e45d5c1d59755459ffe43629436707079989599b91e8c19e50e73af7a4bd09c95402d389266731b0d9c5f69e372d8ebd3a709c05c80d7dd + languageName: node + linkType: hard + "@babel/helper-split-export-declaration@npm:^7.16.7": version: 7.16.7 resolution: "@babel/helper-split-export-declaration@npm:7.16.7" @@ -504,6 +669,18 @@ __metadata: languageName: node linkType: hard +"@babel/helper-wrap-function@npm:^7.18.9": + version: 7.20.5 + resolution: "@babel/helper-wrap-function@npm:7.20.5" + dependencies: + "@babel/helper-function-name": ^7.19.0 + "@babel/template": ^7.18.10 + "@babel/traverse": ^7.20.5 + "@babel/types": ^7.20.5 + checksum: 11a6fc28334368a193a9cb3ad16f29cd7603bab958433efc82ebe59fa6556c227faa24f07ce43983f7a85df826f71d441638442c4315e90a554fe0a70ca5005b + languageName: node + linkType: hard + "@babel/helpers@npm:^7.17.0": version: 7.17.0 resolution: "@babel/helpers@npm:7.17.0" @@ -590,6 +767,15 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.12.5, @babel/parser@npm:^7.20.15, @babel/parser@npm:^7.21.2": + version: 7.21.2 + resolution: "@babel/parser@npm:7.21.2" + bin: + parser: ./bin/babel-parser.js + checksum: e2b89de2c63d4cdd2cafeaea34f389bba729727eec7a8728f736bc472a59396059e3e9fe322c9bed8fd126d201fb609712949dc8783f4cae4806acd9a73da6ff + languageName: node + linkType: hard + "@babel/parser@npm:^7.14.5": version: 7.15.3 resolution: "@babel/parser@npm:7.15.3" @@ -635,194 +821,985 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-async-generators@npm:^7.8.4": - version: 7.8.4 - resolution: "@babel/plugin-syntax-async-generators@npm:7.8.4" +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.18.6": + version: 7.18.6 + resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.18.6" + dependencies: + "@babel/helper-plugin-utils": ^7.18.6 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 845bd280c55a6a91d232cfa54eaf9708ec71e594676fe705794f494bb8b711d833b752b59d1a5c154695225880c23dbc9cab0e53af16fd57807976cd3ff41b8d + languageName: node + linkType: hard + +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:^7.18.9": + version: 7.20.7 + resolution: "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:7.20.7" + dependencies: + "@babel/helper-plugin-utils": ^7.20.2 + "@babel/helper-skip-transparent-expression-wrappers": ^7.20.0 + "@babel/plugin-proposal-optional-chaining": ^7.20.7 + peerDependencies: + "@babel/core": ^7.13.0 + checksum: d610f532210bee5342f5b44a12395ccc6d904e675a297189bc1e401cc185beec09873da523466d7fec34ae1574f7a384235cba1ccc9fe7b89ba094167897c845 + languageName: node + linkType: hard + +"@babel/plugin-proposal-async-generator-functions@npm:^7.20.1": + version: 7.20.7 + resolution: "@babel/plugin-proposal-async-generator-functions@npm:7.20.7" + dependencies: + "@babel/helper-environment-visitor": ^7.18.9 + "@babel/helper-plugin-utils": ^7.20.2 + "@babel/helper-remap-async-to-generator": ^7.18.9 + "@babel/plugin-syntax-async-generators": ^7.8.4 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 111109ee118c9e69982f08d5e119eab04190b36a0f40e22e873802d941956eee66d2aa5a15f5321e51e3f9aa70a91136451b987fe15185ef8cc547ac88937723 + languageName: node + linkType: hard + +"@babel/plugin-proposal-class-properties@npm:^7.18.6": + version: 7.18.6 + resolution: "@babel/plugin-proposal-class-properties@npm:7.18.6" + dependencies: + "@babel/helper-create-class-features-plugin": ^7.18.6 + "@babel/helper-plugin-utils": ^7.18.6 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 49a78a2773ec0db56e915d9797e44fd079ab8a9b2e1716e0df07c92532f2c65d76aeda9543883916b8e0ff13606afeffa67c5b93d05b607bc87653ad18a91422 + languageName: node + linkType: hard + +"@babel/plugin-proposal-class-static-block@npm:^7.18.6": + version: 7.21.0 + resolution: "@babel/plugin-proposal-class-static-block@npm:7.21.0" + dependencies: + "@babel/helper-create-class-features-plugin": ^7.21.0 + "@babel/helper-plugin-utils": ^7.20.2 + "@babel/plugin-syntax-class-static-block": ^7.14.5 + peerDependencies: + "@babel/core": ^7.12.0 + checksum: 236c0ad089e7a7acab776cc1d355330193314bfcd62e94e78f2df35817c6144d7e0e0368976778afd6b7c13e70b5068fa84d7abbf967d4f182e60d03f9ef802b + languageName: node + linkType: hard + +"@babel/plugin-proposal-dynamic-import@npm:^7.18.6": + version: 7.18.6 + resolution: "@babel/plugin-proposal-dynamic-import@npm:7.18.6" + dependencies: + "@babel/helper-plugin-utils": ^7.18.6 + "@babel/plugin-syntax-dynamic-import": ^7.8.3 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 96b1c8a8ad8171d39e9ab106be33bde37ae09b22fb2c449afee9a5edf3c537933d79d963dcdc2694d10677cb96da739cdf1b53454e6a5deab9801f28a818bb2f + languageName: node + linkType: hard + +"@babel/plugin-proposal-export-namespace-from@npm:^7.18.9": + version: 7.18.9 + resolution: "@babel/plugin-proposal-export-namespace-from@npm:7.18.9" + dependencies: + "@babel/helper-plugin-utils": ^7.18.9 + "@babel/plugin-syntax-export-namespace-from": ^7.8.3 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 84ff22bacc5d30918a849bfb7e0e90ae4c5b8d8b65f2ac881803d1cf9068dffbe53bd657b0e4bc4c20b4db301b1c85f1e74183cf29a0dd31e964bd4e97c363ef + languageName: node + linkType: hard + +"@babel/plugin-proposal-json-strings@npm:^7.18.6": + version: 7.18.6 + resolution: "@babel/plugin-proposal-json-strings@npm:7.18.6" + dependencies: + "@babel/helper-plugin-utils": ^7.18.6 + "@babel/plugin-syntax-json-strings": ^7.8.3 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 25ba0e6b9d6115174f51f7c6787e96214c90dd4026e266976b248a2ed417fe50fddae72843ffb3cbe324014a18632ce5648dfac77f089da858022b49fd608cb3 + languageName: node + linkType: hard + +"@babel/plugin-proposal-logical-assignment-operators@npm:^7.18.9": + version: 7.20.7 + resolution: "@babel/plugin-proposal-logical-assignment-operators@npm:7.20.7" + dependencies: + "@babel/helper-plugin-utils": ^7.20.2 + "@babel/plugin-syntax-logical-assignment-operators": ^7.10.4 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: cdd7b8136cc4db3f47714d5266f9e7b592a2ac5a94a5878787ce08890e97c8ab1ca8e94b27bfeba7b0f2b1549a026d9fc414ca2196de603df36fb32633bbdc19 + languageName: node + linkType: hard + +"@babel/plugin-proposal-nullish-coalescing-operator@npm:^7.18.6": + version: 7.18.6 + resolution: "@babel/plugin-proposal-nullish-coalescing-operator@npm:7.18.6" + dependencies: + "@babel/helper-plugin-utils": ^7.18.6 + "@babel/plugin-syntax-nullish-coalescing-operator": ^7.8.3 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 949c9ddcdecdaec766ee610ef98f965f928ccc0361dd87cf9f88cf4896a6ccd62fce063d4494778e50da99dea63d270a1be574a62d6ab81cbe9d85884bf55a7d + languageName: node + linkType: hard + +"@babel/plugin-proposal-numeric-separator@npm:^7.18.6": + version: 7.18.6 + resolution: "@babel/plugin-proposal-numeric-separator@npm:7.18.6" + dependencies: + "@babel/helper-plugin-utils": ^7.18.6 + "@babel/plugin-syntax-numeric-separator": ^7.10.4 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: f370ea584c55bf4040e1f78c80b4eeb1ce2e6aaa74f87d1a48266493c33931d0b6222d8cee3a082383d6bb648ab8d6b7147a06f974d3296ef3bc39c7851683ec + languageName: node + linkType: hard + +"@babel/plugin-proposal-object-rest-spread@npm:^7.20.2": + version: 7.20.7 + resolution: "@babel/plugin-proposal-object-rest-spread@npm:7.20.7" + dependencies: + "@babel/compat-data": ^7.20.5 + "@babel/helper-compilation-targets": ^7.20.7 + "@babel/helper-plugin-utils": ^7.20.2 + "@babel/plugin-syntax-object-rest-spread": ^7.8.3 + "@babel/plugin-transform-parameters": ^7.20.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 1329db17009964bc644484c660eab717cb3ca63ac0ab0f67c651a028d1bc2ead51dc4064caea283e46994f1b7221670a35cbc0b4beb6273f55e915494b5aa0b2 + languageName: node + linkType: hard + +"@babel/plugin-proposal-optional-catch-binding@npm:^7.18.6": + version: 7.18.6 + resolution: "@babel/plugin-proposal-optional-catch-binding@npm:7.18.6" + dependencies: + "@babel/helper-plugin-utils": ^7.18.6 + "@babel/plugin-syntax-optional-catch-binding": ^7.8.3 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 7b5b39fb5d8d6d14faad6cb68ece5eeb2fd550fb66b5af7d7582402f974f5bc3684641f7c192a5a57e0f59acfae4aada6786be1eba030881ddc590666eff4d1e + languageName: node + linkType: hard + +"@babel/plugin-proposal-optional-chaining@npm:^7.18.9, @babel/plugin-proposal-optional-chaining@npm:^7.20.7": + version: 7.21.0 + resolution: "@babel/plugin-proposal-optional-chaining@npm:7.21.0" + dependencies: + "@babel/helper-plugin-utils": ^7.20.2 + "@babel/helper-skip-transparent-expression-wrappers": ^7.20.0 + "@babel/plugin-syntax-optional-chaining": ^7.8.3 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 11c5449e01b18bb8881e8e005a577fa7be2fe5688e2382c8822d51f8f7005342a301a46af7b273b1f5645f9a7b894c428eee8526342038a275ef6ba4c8d8d746 + languageName: node + linkType: hard + +"@babel/plugin-proposal-private-methods@npm:^7.18.6": + version: 7.18.6 + resolution: "@babel/plugin-proposal-private-methods@npm:7.18.6" + dependencies: + "@babel/helper-create-class-features-plugin": ^7.18.6 + "@babel/helper-plugin-utils": ^7.18.6 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 22d8502ee96bca99ad2c8393e8493e2b8d4507576dd054490fd8201a36824373440106f5b098b6d821b026c7e72b0424ff4aeca69ed5f42e48f029d3a156d5ad + languageName: node + linkType: hard + +"@babel/plugin-proposal-private-property-in-object@npm:^7.18.6": + version: 7.21.0 + resolution: "@babel/plugin-proposal-private-property-in-object@npm:7.21.0" + dependencies: + "@babel/helper-annotate-as-pure": ^7.18.6 + "@babel/helper-create-class-features-plugin": ^7.21.0 + "@babel/helper-plugin-utils": ^7.20.2 + "@babel/plugin-syntax-private-property-in-object": ^7.14.5 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: add881a6a836635c41d2710551fdf777e2c07c0b691bf2baacc5d658dd64107479df1038680d6e67c468bfc6f36fb8920025d6bac2a1df0a81b867537d40ae78 + languageName: node + linkType: hard + +"@babel/plugin-proposal-unicode-property-regex@npm:^7.18.6, @babel/plugin-proposal-unicode-property-regex@npm:^7.4.4": + version: 7.18.6 + resolution: "@babel/plugin-proposal-unicode-property-regex@npm:7.18.6" + dependencies: + "@babel/helper-create-regexp-features-plugin": ^7.18.6 + "@babel/helper-plugin-utils": ^7.18.6 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: a8575ecb7ff24bf6c6e94808d5c84bb5a0c6dd7892b54f09f4646711ba0ee1e1668032b3c43e3e1dfec2c5716c302e851ac756c1645e15882d73df6ad21ae951 + languageName: node + linkType: hard + +"@babel/plugin-syntax-async-generators@npm:^7.8.4": + version: 7.8.4 + resolution: "@babel/plugin-syntax-async-generators@npm:7.8.4" + dependencies: + "@babel/helper-plugin-utils": ^7.8.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 7ed1c1d9b9e5b64ef028ea5e755c0be2d4e5e4e3d6cf7df757b9a8c4cfa4193d268176d0f1f7fbecdda6fe722885c7fda681f480f3741d8a2d26854736f05367 + languageName: node + linkType: hard + +"@babel/plugin-syntax-bigint@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-bigint@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": ^7.8.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 3a10849d83e47aec50f367a9e56a6b22d662ddce643334b087f9828f4c3dd73bdc5909aaeabe123fed78515767f9ca43498a0e621c438d1cd2802d7fae3c9648 + languageName: node + linkType: hard + +"@babel/plugin-syntax-class-properties@npm:^7.12.13, @babel/plugin-syntax-class-properties@npm:^7.8.3": + version: 7.12.13 + resolution: "@babel/plugin-syntax-class-properties@npm:7.12.13" + dependencies: + "@babel/helper-plugin-utils": ^7.12.13 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 24f34b196d6342f28d4bad303612d7ff566ab0a013ce89e775d98d6f832969462e7235f3e7eaf17678a533d4be0ba45d3ae34ab4e5a9dcbda5d98d49e5efa2fc + languageName: node + linkType: hard + +"@babel/plugin-syntax-class-static-block@npm:^7.14.5": + version: 7.14.5 + resolution: "@babel/plugin-syntax-class-static-block@npm:7.14.5" + dependencies: + "@babel/helper-plugin-utils": ^7.14.5 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 3e80814b5b6d4fe17826093918680a351c2d34398a914ce6e55d8083d72a9bdde4fbaf6a2dcea0e23a03de26dc2917ae3efd603d27099e2b98380345703bf948 + languageName: node + linkType: hard + +"@babel/plugin-syntax-dynamic-import@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-dynamic-import@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": ^7.8.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: ce307af83cf433d4ec42932329fad25fa73138ab39c7436882ea28742e1c0066626d224e0ad2988724c82644e41601cef607b36194f695cb78a1fcdc959637bd + languageName: node + linkType: hard + +"@babel/plugin-syntax-export-namespace-from@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-export-namespace-from@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": ^7.8.3 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 85740478be5b0de185228e7814451d74ab8ce0a26fcca7613955262a26e99e8e15e9da58f60c754b84515d4c679b590dbd3f2148f0f58025f4ae706f1c5a5d4a + languageName: node + linkType: hard + +"@babel/plugin-syntax-import-assertions@npm:^7.20.0": + version: 7.20.0 + resolution: "@babel/plugin-syntax-import-assertions@npm:7.20.0" + dependencies: + "@babel/helper-plugin-utils": ^7.19.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 6a86220e0aae40164cd3ffaf80e7c076a1be02a8f3480455dddbae05fda8140f429290027604df7a11b3f3f124866e8a6d69dbfa1dda61ee7377b920ad144d5b + languageName: node + linkType: hard + +"@babel/plugin-syntax-import-meta@npm:^7.8.3": + version: 7.10.4 + resolution: "@babel/plugin-syntax-import-meta@npm:7.10.4" + dependencies: + "@babel/helper-plugin-utils": ^7.10.4 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 166ac1125d10b9c0c430e4156249a13858c0366d38844883d75d27389621ebe651115cb2ceb6dc011534d5055719fa1727b59f39e1ab3ca97820eef3dcab5b9b + languageName: node + linkType: hard + +"@babel/plugin-syntax-json-strings@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-json-strings@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": ^7.8.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: bf5aea1f3188c9a507e16efe030efb996853ca3cadd6512c51db7233cc58f3ac89ff8c6bdfb01d30843b161cfe7d321e1bf28da82f7ab8d7e6bc5464666f354a + languageName: node + linkType: hard + +"@babel/plugin-syntax-jsx@npm:^7.17.12, @babel/plugin-syntax-jsx@npm:^7.18.6, @babel/plugin-syntax-jsx@npm:^7.7.2": + version: 7.18.6 + resolution: "@babel/plugin-syntax-jsx@npm:7.18.6" + dependencies: + "@babel/helper-plugin-utils": ^7.18.6 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 6d37ea972970195f1ffe1a54745ce2ae456e0ac6145fae9aa1480f297248b262ea6ebb93010eddb86ebfacb94f57c05a1fc5d232b9a67325b09060299d515c67 + languageName: node + linkType: hard + +"@babel/plugin-syntax-logical-assignment-operators@npm:^7.10.4, @babel/plugin-syntax-logical-assignment-operators@npm:^7.8.3": + version: 7.10.4 + resolution: "@babel/plugin-syntax-logical-assignment-operators@npm:7.10.4" + dependencies: + "@babel/helper-plugin-utils": ^7.10.4 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: aff33577037e34e515911255cdbb1fd39efee33658aa00b8a5fd3a4b903585112d037cce1cc9e4632f0487dc554486106b79ccd5ea63a2e00df4363f6d4ff886 + languageName: node + linkType: hard + +"@babel/plugin-syntax-nullish-coalescing-operator@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-nullish-coalescing-operator@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": ^7.8.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 87aca4918916020d1fedba54c0e232de408df2644a425d153be368313fdde40d96088feed6c4e5ab72aac89be5d07fef2ddf329a15109c5eb65df006bf2580d1 + languageName: node + linkType: hard + +"@babel/plugin-syntax-numeric-separator@npm:^7.10.4, @babel/plugin-syntax-numeric-separator@npm:^7.8.3": + version: 7.10.4 + resolution: "@babel/plugin-syntax-numeric-separator@npm:7.10.4" + dependencies: + "@babel/helper-plugin-utils": ^7.10.4 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 01ec5547bd0497f76cc903ff4d6b02abc8c05f301c88d2622b6d834e33a5651aa7c7a3d80d8d57656a4588f7276eba357f6b7e006482f5b564b7a6488de493a1 + languageName: node + linkType: hard + +"@babel/plugin-syntax-object-rest-spread@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-object-rest-spread@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": ^7.8.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: fddcf581a57f77e80eb6b981b10658421bc321ba5f0a5b754118c6a92a5448f12a0c336f77b8abf734841e102e5126d69110a306eadb03ca3e1547cab31f5cbf + languageName: node + linkType: hard + +"@babel/plugin-syntax-optional-catch-binding@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-optional-catch-binding@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": ^7.8.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 910d90e72bc90ea1ce698e89c1027fed8845212d5ab588e35ef91f13b93143845f94e2539d831dc8d8ededc14ec02f04f7bd6a8179edd43a326c784e7ed7f0b9 + languageName: node + linkType: hard + +"@babel/plugin-syntax-optional-chaining@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-optional-chaining@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": ^7.8.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: eef94d53a1453361553c1f98b68d17782861a04a392840341bc91780838dd4e695209c783631cf0de14c635758beafb6a3a65399846ffa4386bff90639347f30 + languageName: node + linkType: hard + +"@babel/plugin-syntax-private-property-in-object@npm:^7.14.5": + version: 7.14.5 + resolution: "@babel/plugin-syntax-private-property-in-object@npm:7.14.5" + dependencies: + "@babel/helper-plugin-utils": ^7.14.5 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: b317174783e6e96029b743ccff2a67d63d38756876e7e5d0ba53a322e38d9ca452c13354a57de1ad476b4c066dbae699e0ca157441da611117a47af88985ecda + languageName: node + linkType: hard + +"@babel/plugin-syntax-top-level-await@npm:^7.14.5, @babel/plugin-syntax-top-level-await@npm:^7.8.3": + version: 7.14.5 + resolution: "@babel/plugin-syntax-top-level-await@npm:7.14.5" + dependencies: + "@babel/helper-plugin-utils": ^7.14.5 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: bbd1a56b095be7820029b209677b194db9b1d26691fe999856462e66b25b281f031f3dfd91b1619e9dcf95bebe336211833b854d0fb8780d618e35667c2d0d7e + languageName: node + linkType: hard + +"@babel/plugin-syntax-typescript@npm:^7.20.0": + version: 7.20.0 + resolution: "@babel/plugin-syntax-typescript@npm:7.20.0" + dependencies: + "@babel/helper-plugin-utils": ^7.19.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 6189c0b5c32ba3c9a80a42338bd50719d783b20ef29b853d4f03929e971913d3cefd80184e924ae98ad6db09080be8fe6f1ffde9a6db8972523234f0274d36f7 + languageName: node + linkType: hard + +"@babel/plugin-syntax-typescript@npm:^7.7.2": + version: 7.16.7 + resolution: "@babel/plugin-syntax-typescript@npm:7.16.7" + dependencies: + "@babel/helper-plugin-utils": ^7.16.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 661e636060609ede9a402e22603b01784c21fabb0a637e65f561c8159351fe0130bbc11fdefe31902107885e3332fc34d95eb652ac61d3f61f2d61f5da20609e + languageName: node + linkType: hard + +"@babel/plugin-transform-arrow-functions@npm:^7.18.6": + version: 7.20.7 + resolution: "@babel/plugin-transform-arrow-functions@npm:7.20.7" + dependencies: + "@babel/helper-plugin-utils": ^7.20.2 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: b43cabe3790c2de7710abe32df9a30005eddb2050dadd5d122c6872f679e5710e410f1b90c8f99a2aff7b614cccfecf30e7fd310236686f60d3ed43fd80b9847 + languageName: node + linkType: hard + +"@babel/plugin-transform-async-to-generator@npm:^7.18.6": + version: 7.20.7 + resolution: "@babel/plugin-transform-async-to-generator@npm:7.20.7" + dependencies: + "@babel/helper-module-imports": ^7.18.6 + "@babel/helper-plugin-utils": ^7.20.2 + "@babel/helper-remap-async-to-generator": ^7.18.9 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: fe9ee8a5471b4317c1b9ea92410ace8126b52a600d7cfbfe1920dcac6fb0fad647d2e08beb4fd03c630eb54430e6c72db11e283e3eddc49615c68abd39430904 + languageName: node + linkType: hard + +"@babel/plugin-transform-block-scoped-functions@npm:^7.18.6": + version: 7.18.6 + resolution: "@babel/plugin-transform-block-scoped-functions@npm:7.18.6" + dependencies: + "@babel/helper-plugin-utils": ^7.18.6 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 0a0df61f94601e3666bf39f2cc26f5f7b22a94450fb93081edbed967bd752ce3f81d1227fefd3799f5ee2722171b5e28db61379234d1bb85b6ec689589f99d7e + languageName: node + linkType: hard + +"@babel/plugin-transform-block-scoping@npm:^7.20.2": + version: 7.21.0 + resolution: "@babel/plugin-transform-block-scoping@npm:7.21.0" + dependencies: + "@babel/helper-plugin-utils": ^7.20.2 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 15aacaadbecf96b53a750db1be4990b0d89c7f5bc3e1794b63b49fb219638c1fd25d452d15566d7e5ddf5b5f4e1a0a0055c35c1c7aee323c7b114bf49f66f4b0 + languageName: node + linkType: hard + +"@babel/plugin-transform-classes@npm:^7.20.2": + version: 7.21.0 + resolution: "@babel/plugin-transform-classes@npm:7.21.0" + dependencies: + "@babel/helper-annotate-as-pure": ^7.18.6 + "@babel/helper-compilation-targets": ^7.20.7 + "@babel/helper-environment-visitor": ^7.18.9 + "@babel/helper-function-name": ^7.21.0 + "@babel/helper-optimise-call-expression": ^7.18.6 + "@babel/helper-plugin-utils": ^7.20.2 + "@babel/helper-replace-supers": ^7.20.7 + "@babel/helper-split-export-declaration": ^7.18.6 + globals: ^11.1.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 088ae152074bd0e90f64659169255bfe50393e637ec8765cb2a518848b11b0299e66b91003728fd0a41563a6fdc6b8d548ece698a314fd5447f5489c22e466b7 + languageName: node + linkType: hard + +"@babel/plugin-transform-computed-properties@npm:^7.18.9": + version: 7.20.7 + resolution: "@babel/plugin-transform-computed-properties@npm:7.20.7" + dependencies: + "@babel/helper-plugin-utils": ^7.20.2 + "@babel/template": ^7.20.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: be70e54bda8b469146459f429e5f2bd415023b87b2d5af8b10e48f465ffb02847a3ed162ca60378c004b82db848e4d62e90010d41ded7e7176b6d8d1c2911139 + languageName: node + linkType: hard + +"@babel/plugin-transform-destructuring@npm:^7.20.2": + version: 7.20.7 + resolution: "@babel/plugin-transform-destructuring@npm:7.20.7" + dependencies: + "@babel/helper-plugin-utils": ^7.20.2 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: bd8affdb142c77662037215e37128b2110a786c92a67e1f00b38223c438c1610bd84cbc0386e9cd3479245ea811c5ca6c9838f49be4729b592159a30ce79add2 + languageName: node + linkType: hard + +"@babel/plugin-transform-dotall-regex@npm:^7.18.6, @babel/plugin-transform-dotall-regex@npm:^7.4.4": + version: 7.18.6 + resolution: "@babel/plugin-transform-dotall-regex@npm:7.18.6" + dependencies: + "@babel/helper-create-regexp-features-plugin": ^7.18.6 + "@babel/helper-plugin-utils": ^7.18.6 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: cbe5d7063eb8f8cca24cd4827bc97f5641166509e58781a5f8aa47fb3d2d786ce4506a30fca2e01f61f18792783a5cb5d96bf5434c3dd1ad0de8c9cc625a53da + languageName: node + linkType: hard + +"@babel/plugin-transform-duplicate-keys@npm:^7.18.9": + version: 7.18.9 + resolution: "@babel/plugin-transform-duplicate-keys@npm:7.18.9" + dependencies: + "@babel/helper-plugin-utils": ^7.18.9 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 220bf4a9fec5c4d4a7b1de38810350260e8ea08481bf78332a464a21256a95f0df8cd56025f346238f09b04f8e86d4158fafc9f4af57abaef31637e3b58bd4fe + languageName: node + linkType: hard + +"@babel/plugin-transform-exponentiation-operator@npm:^7.18.6": + version: 7.18.6 + resolution: "@babel/plugin-transform-exponentiation-operator@npm:7.18.6" + dependencies: + "@babel/helper-builder-binary-assignment-operator-visitor": ^7.18.6 + "@babel/helper-plugin-utils": ^7.18.6 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 7f70222f6829c82a36005508d34ddbe6fd0974ae190683a8670dd6ff08669aaf51fef2209d7403f9bd543cb2d12b18458016c99a6ed0332ccedb3ea127b01229 + languageName: node + linkType: hard + +"@babel/plugin-transform-for-of@npm:^7.18.8": + version: 7.21.0 + resolution: "@babel/plugin-transform-for-of@npm:7.21.0" + dependencies: + "@babel/helper-plugin-utils": ^7.20.2 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 2f3f86ca1fab2929fcda6a87e4303d5c635b5f96dc9a45fd4ca083308a3020c79ac33b9543eb4640ef2b79f3586a00ab2d002a7081adb9e9d7440dce30781034 + languageName: node + linkType: hard + +"@babel/plugin-transform-function-name@npm:^7.18.9": + version: 7.18.9 + resolution: "@babel/plugin-transform-function-name@npm:7.18.9" + dependencies: + "@babel/helper-compilation-targets": ^7.18.9 + "@babel/helper-function-name": ^7.18.9 + "@babel/helper-plugin-utils": ^7.18.9 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 62dd9c6cdc9714704efe15545e782ee52d74dc73916bf954b4d3bee088fb0ec9e3c8f52e751252433656c09f744b27b757fc06ed99bcde28e8a21600a1d8e597 + languageName: node + linkType: hard + +"@babel/plugin-transform-literals@npm:^7.18.9": + version: 7.18.9 + resolution: "@babel/plugin-transform-literals@npm:7.18.9" + dependencies: + "@babel/helper-plugin-utils": ^7.18.9 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 3458dd2f1a47ac51d9d607aa18f3d321cbfa8560a985199185bed5a906bb0c61ba85575d386460bac9aed43fdd98940041fae5a67dff286f6f967707cff489f8 + languageName: node + linkType: hard + +"@babel/plugin-transform-member-expression-literals@npm:^7.18.6": + version: 7.18.6 + resolution: "@babel/plugin-transform-member-expression-literals@npm:7.18.6" + dependencies: + "@babel/helper-plugin-utils": ^7.18.6 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 35a3d04f6693bc6b298c05453d85ee6e41cc806538acb6928427e0e97ae06059f97d2f07d21495fcf5f70d3c13a242e2ecbd09d5c1fcb1b1a73ff528dcb0b695 + languageName: node + linkType: hard + +"@babel/plugin-transform-modules-amd@npm:^7.19.6": + version: 7.20.11 + resolution: "@babel/plugin-transform-modules-amd@npm:7.20.11" + dependencies: + "@babel/helper-module-transforms": ^7.20.11 + "@babel/helper-plugin-utils": ^7.20.2 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 23665c1c20c8f11c89382b588fb9651c0756d130737a7625baeaadbd3b973bc5bfba1303bedffa8fb99db1e6d848afb01016e1df2b69b18303e946890c790001 + languageName: node + linkType: hard + +"@babel/plugin-transform-modules-commonjs@npm:^7.19.6": + version: 7.21.2 + resolution: "@babel/plugin-transform-modules-commonjs@npm:7.21.2" + dependencies: + "@babel/helper-module-transforms": ^7.21.2 + "@babel/helper-plugin-utils": ^7.20.2 + "@babel/helper-simple-access": ^7.20.2 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 65aa06e3e3792f39b99eb5f807034693ff0ecf80438580f7ae504f4c4448ef04147b1889ea5e6f60f3ad4a12ebbb57c6f1f979a249dadbd8d11fe22f4441918b + languageName: node + linkType: hard + +"@babel/plugin-transform-modules-systemjs@npm:^7.19.6": + version: 7.20.11 + resolution: "@babel/plugin-transform-modules-systemjs@npm:7.20.11" + dependencies: + "@babel/helper-hoist-variables": ^7.18.6 + "@babel/helper-module-transforms": ^7.20.11 + "@babel/helper-plugin-utils": ^7.20.2 + "@babel/helper-validator-identifier": ^7.19.1 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 4546c47587f88156d66c7eb7808e903cf4bb3f6ba6ac9bc8e3af2e29e92eb9f0b3f44d52043bfd24eb25fa7827fd7b6c8bfeac0cac7584e019b87e1ecbd0e673 + languageName: node + linkType: hard + +"@babel/plugin-transform-modules-umd@npm:^7.18.6": + version: 7.18.6 + resolution: "@babel/plugin-transform-modules-umd@npm:7.18.6" + dependencies: + "@babel/helper-module-transforms": ^7.18.6 + "@babel/helper-plugin-utils": ^7.18.6 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: c3b6796c6f4579f1ba5ab0cdcc73910c1e9c8e1e773c507c8bb4da33072b3ae5df73c6d68f9126dab6e99c24ea8571e1563f8710d7c421fac1cde1e434c20153 + languageName: node + linkType: hard + +"@babel/plugin-transform-named-capturing-groups-regex@npm:^7.19.1": + version: 7.20.5 + resolution: "@babel/plugin-transform-named-capturing-groups-regex@npm:7.20.5" + dependencies: + "@babel/helper-create-regexp-features-plugin": ^7.20.5 + "@babel/helper-plugin-utils": ^7.20.2 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 528c95fb1087e212f17e1c6456df041b28a83c772b9c93d2e407c9d03b72182b0d9d126770c1d6e0b23aab052599ceaf25ed6a2c0627f4249be34a83f6fae853 + languageName: node + linkType: hard + +"@babel/plugin-transform-new-target@npm:^7.18.6": + version: 7.18.6 + resolution: "@babel/plugin-transform-new-target@npm:7.18.6" + dependencies: + "@babel/helper-plugin-utils": ^7.18.6 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: bd780e14f46af55d0ae8503b3cb81ca86dcc73ed782f177e74f498fff934754f9e9911df1f8f3bd123777eed7c1c1af4d66abab87c8daae5403e7719a6b845d1 + languageName: node + linkType: hard + +"@babel/plugin-transform-object-super@npm:^7.18.6": + version: 7.18.6 + resolution: "@babel/plugin-transform-object-super@npm:7.18.6" + dependencies: + "@babel/helper-plugin-utils": ^7.18.6 + "@babel/helper-replace-supers": ^7.18.6 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 0fcb04e15deea96ae047c21cb403607d49f06b23b4589055993365ebd7a7d7541334f06bf9642e90075e66efce6ebaf1eb0ef066fbbab802d21d714f1aac3aef + languageName: node + linkType: hard + +"@babel/plugin-transform-parameters@npm:^7.20.1, @babel/plugin-transform-parameters@npm:^7.20.7": + version: 7.20.7 + resolution: "@babel/plugin-transform-parameters@npm:7.20.7" + dependencies: + "@babel/helper-plugin-utils": ^7.20.2 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 6ffe0dd9afb2d2b9bc247381aa2e95dd9997ff5568a0a11900528919a4e073ac68f46409431455badb8809644d47cff180045bc2b9700e3f36e3b23554978947 + languageName: node + linkType: hard + +"@babel/plugin-transform-property-literals@npm:^7.18.6": + version: 7.18.6 + resolution: "@babel/plugin-transform-property-literals@npm:7.18.6" + dependencies: + "@babel/helper-plugin-utils": ^7.18.6 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 1c16e64de554703f4b547541de2edda6c01346dd3031d4d29e881aa7733785cd26d53611a4ccf5353f4d3e69097bb0111c0a93ace9e683edd94fea28c4484144 + languageName: node + linkType: hard + +"@babel/plugin-transform-react-jsx-self@npm:^7.18.6": + version: 7.18.6 + resolution: "@babel/plugin-transform-react-jsx-self@npm:7.18.6" dependencies: - "@babel/helper-plugin-utils": ^7.8.0 + "@babel/helper-plugin-utils": ^7.18.6 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 7ed1c1d9b9e5b64ef028ea5e755c0be2d4e5e4e3d6cf7df757b9a8c4cfa4193d268176d0f1f7fbecdda6fe722885c7fda681f480f3741d8a2d26854736f05367 + checksum: 7d24e29c63869bb23495c163a92678c1c3341ecf74db420a20c6d3db74cbf5000fe908943f6106494e7225c0168945c150e528162274fd8fc7721966ad26930a languageName: node linkType: hard -"@babel/plugin-syntax-bigint@npm:^7.8.3": - version: 7.8.3 - resolution: "@babel/plugin-syntax-bigint@npm:7.8.3" +"@babel/plugin-transform-react-jsx-source@npm:^7.19.6": + version: 7.19.6 + resolution: "@babel/plugin-transform-react-jsx-source@npm:7.19.6" dependencies: - "@babel/helper-plugin-utils": ^7.8.0 + "@babel/helper-plugin-utils": ^7.19.0 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 3a10849d83e47aec50f367a9e56a6b22d662ddce643334b087f9828f4c3dd73bdc5909aaeabe123fed78515767f9ca43498a0e621c438d1cd2802d7fae3c9648 + checksum: 1e9e29a4efc5b79840bd4f68e404f5ab7765ce48c7bd22f12f2b185f9c782c66933bdf54a1b21879e4e56e6b50b4e88aca82789ecb1f61123af6dfa9ab16c555 languageName: node linkType: hard -"@babel/plugin-syntax-class-properties@npm:^7.8.3": - version: 7.12.13 - resolution: "@babel/plugin-syntax-class-properties@npm:7.12.13" +"@babel/plugin-transform-react-jsx@npm:7.20.13": + version: 7.20.13 + resolution: "@babel/plugin-transform-react-jsx@npm:7.20.13" dependencies: - "@babel/helper-plugin-utils": ^7.12.13 + "@babel/helper-annotate-as-pure": ^7.18.6 + "@babel/helper-module-imports": ^7.18.6 + "@babel/helper-plugin-utils": ^7.20.2 + "@babel/plugin-syntax-jsx": ^7.18.6 + "@babel/types": ^7.20.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 24f34b196d6342f28d4bad303612d7ff566ab0a013ce89e775d98d6f832969462e7235f3e7eaf17678a533d4be0ba45d3ae34ab4e5a9dcbda5d98d49e5efa2fc + checksum: b1daaa9b093ab59f71572dde7ad05ed3490433a47de103fc866f60347da55fa7fe84cf9b4c9fa22917517d52f70ab5e05ec631bba1c348733c0d8ebbd7de8c68 languageName: node linkType: hard -"@babel/plugin-syntax-import-meta@npm:^7.8.3": - version: 7.10.4 - resolution: "@babel/plugin-syntax-import-meta@npm:7.10.4" +"@babel/plugin-transform-regenerator@npm:^7.18.6": + version: 7.20.5 + resolution: "@babel/plugin-transform-regenerator@npm:7.20.5" dependencies: - "@babel/helper-plugin-utils": ^7.10.4 + "@babel/helper-plugin-utils": ^7.20.2 + regenerator-transform: ^0.15.1 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 166ac1125d10b9c0c430e4156249a13858c0366d38844883d75d27389621ebe651115cb2ceb6dc011534d5055719fa1727b59f39e1ab3ca97820eef3dcab5b9b + checksum: 13164861e71fb23d84c6270ef5330b03c54d5d661c2c7468f28e21c4f8598558ca0c8c3cb1d996219352946e849d270a61372bc93c8fbe9676e78e3ffd0dea07 languageName: node linkType: hard -"@babel/plugin-syntax-json-strings@npm:^7.8.3": - version: 7.8.3 - resolution: "@babel/plugin-syntax-json-strings@npm:7.8.3" +"@babel/plugin-transform-reserved-words@npm:^7.18.6": + version: 7.18.6 + resolution: "@babel/plugin-transform-reserved-words@npm:7.18.6" dependencies: - "@babel/helper-plugin-utils": ^7.8.0 + "@babel/helper-plugin-utils": ^7.18.6 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: bf5aea1f3188c9a507e16efe030efb996853ca3cadd6512c51db7233cc58f3ac89ff8c6bdfb01d30843b161cfe7d321e1bf28da82f7ab8d7e6bc5464666f354a + checksum: 0738cdc30abdae07c8ec4b233b30c31f68b3ff0eaa40eddb45ae607c066127f5fa99ddad3c0177d8e2832e3a7d3ad115775c62b431ebd6189c40a951b867a80c languageName: node linkType: hard -"@babel/plugin-syntax-jsx@npm:^7.17.12, @babel/plugin-syntax-jsx@npm:^7.18.6, @babel/plugin-syntax-jsx@npm:^7.7.2": +"@babel/plugin-transform-shorthand-properties@npm:^7.18.6": version: 7.18.6 - resolution: "@babel/plugin-syntax-jsx@npm:7.18.6" + resolution: "@babel/plugin-transform-shorthand-properties@npm:7.18.6" dependencies: "@babel/helper-plugin-utils": ^7.18.6 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 6d37ea972970195f1ffe1a54745ce2ae456e0ac6145fae9aa1480f297248b262ea6ebb93010eddb86ebfacb94f57c05a1fc5d232b9a67325b09060299d515c67 + checksum: b8e4e8acc2700d1e0d7d5dbfd4fdfb935651913de6be36e6afb7e739d8f9ca539a5150075a0f9b79c88be25ddf45abb912fe7abf525f0b80f5b9d9860de685d7 languageName: node linkType: hard -"@babel/plugin-syntax-logical-assignment-operators@npm:^7.8.3": - version: 7.10.4 - resolution: "@babel/plugin-syntax-logical-assignment-operators@npm:7.10.4" +"@babel/plugin-transform-spread@npm:^7.19.0": + version: 7.20.7 + resolution: "@babel/plugin-transform-spread@npm:7.20.7" dependencies: - "@babel/helper-plugin-utils": ^7.10.4 + "@babel/helper-plugin-utils": ^7.20.2 + "@babel/helper-skip-transparent-expression-wrappers": ^7.20.0 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: aff33577037e34e515911255cdbb1fd39efee33658aa00b8a5fd3a4b903585112d037cce1cc9e4632f0487dc554486106b79ccd5ea63a2e00df4363f6d4ff886 + checksum: 8ea698a12da15718aac7489d4cde10beb8a3eea1f66167d11ab1e625033641e8b328157fd1a0b55dd6531933a160c01fc2e2e61132a385cece05f26429fd0cc2 languageName: node linkType: hard -"@babel/plugin-syntax-nullish-coalescing-operator@npm:^7.8.3": - version: 7.8.3 - resolution: "@babel/plugin-syntax-nullish-coalescing-operator@npm:7.8.3" +"@babel/plugin-transform-sticky-regex@npm:^7.18.6": + version: 7.18.6 + resolution: "@babel/plugin-transform-sticky-regex@npm:7.18.6" dependencies: - "@babel/helper-plugin-utils": ^7.8.0 + "@babel/helper-plugin-utils": ^7.18.6 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 87aca4918916020d1fedba54c0e232de408df2644a425d153be368313fdde40d96088feed6c4e5ab72aac89be5d07fef2ddf329a15109c5eb65df006bf2580d1 + checksum: 68ea18884ae9723443ffa975eb736c8c0d751265859cd3955691253f7fee37d7a0f7efea96c8a062876af49a257a18ea0ed5fea0d95a7b3611ce40f7ee23aee3 languageName: node linkType: hard -"@babel/plugin-syntax-numeric-separator@npm:^7.8.3": - version: 7.10.4 - resolution: "@babel/plugin-syntax-numeric-separator@npm:7.10.4" +"@babel/plugin-transform-template-literals@npm:^7.18.9": + version: 7.18.9 + resolution: "@babel/plugin-transform-template-literals@npm:7.18.9" dependencies: - "@babel/helper-plugin-utils": ^7.10.4 + "@babel/helper-plugin-utils": ^7.18.9 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 01ec5547bd0497f76cc903ff4d6b02abc8c05f301c88d2622b6d834e33a5651aa7c7a3d80d8d57656a4588f7276eba357f6b7e006482f5b564b7a6488de493a1 + checksum: 3d2fcd79b7c345917f69b92a85bdc3ddd68ce2c87dc70c7d61a8373546ccd1f5cb8adc8540b49dfba08e1b82bb7b3bbe23a19efdb2b9c994db2db42906ca9fb2 languageName: node linkType: hard -"@babel/plugin-syntax-object-rest-spread@npm:^7.8.3": - version: 7.8.3 - resolution: "@babel/plugin-syntax-object-rest-spread@npm:7.8.3" +"@babel/plugin-transform-typeof-symbol@npm:^7.18.9": + version: 7.18.9 + resolution: "@babel/plugin-transform-typeof-symbol@npm:7.18.9" dependencies: - "@babel/helper-plugin-utils": ^7.8.0 + "@babel/helper-plugin-utils": ^7.18.9 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: fddcf581a57f77e80eb6b981b10658421bc321ba5f0a5b754118c6a92a5448f12a0c336f77b8abf734841e102e5126d69110a306eadb03ca3e1547cab31f5cbf + checksum: e754e0d8b8a028c52e10c148088606e3f7a9942c57bd648fc0438e5b4868db73c386a5ed47ab6d6f0594aae29ee5ffc2ffc0f7ebee7fae560a066d6dea811cd4 languageName: node linkType: hard -"@babel/plugin-syntax-optional-catch-binding@npm:^7.8.3": - version: 7.8.3 - resolution: "@babel/plugin-syntax-optional-catch-binding@npm:7.8.3" +"@babel/plugin-transform-typescript@npm:^7.18.6": + version: 7.21.0 + resolution: "@babel/plugin-transform-typescript@npm:7.21.0" dependencies: - "@babel/helper-plugin-utils": ^7.8.0 + "@babel/helper-create-class-features-plugin": ^7.21.0 + "@babel/helper-plugin-utils": ^7.20.2 + "@babel/plugin-syntax-typescript": ^7.20.0 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 910d90e72bc90ea1ce698e89c1027fed8845212d5ab588e35ef91f13b93143845f94e2539d831dc8d8ededc14ec02f04f7bd6a8179edd43a326c784e7ed7f0b9 + checksum: 091931118eb515738a4bc8245875f985fc9759d3f85cdf08ee641779b41520241b369404e2bb86fc81907ad827678fdb704e8e5a995352def5dd3051ea2cd870 languageName: node linkType: hard -"@babel/plugin-syntax-optional-chaining@npm:^7.8.3": - version: 7.8.3 - resolution: "@babel/plugin-syntax-optional-chaining@npm:7.8.3" +"@babel/plugin-transform-unicode-escapes@npm:^7.18.10": + version: 7.18.10 + resolution: "@babel/plugin-transform-unicode-escapes@npm:7.18.10" dependencies: - "@babel/helper-plugin-utils": ^7.8.0 + "@babel/helper-plugin-utils": ^7.18.9 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: eef94d53a1453361553c1f98b68d17782861a04a392840341bc91780838dd4e695209c783631cf0de14c635758beafb6a3a65399846ffa4386bff90639347f30 + checksum: f5baca55cb3c11bc08ec589f5f522d85c1ab509b4d11492437e45027d64ae0b22f0907bd1381e8d7f2a436384bb1f9ad89d19277314242c5c2671a0f91d0f9cd languageName: node linkType: hard -"@babel/plugin-syntax-top-level-await@npm:^7.8.3": - version: 7.14.5 - resolution: "@babel/plugin-syntax-top-level-await@npm:7.14.5" +"@babel/plugin-transform-unicode-regex@npm:^7.18.6": + version: 7.18.6 + resolution: "@babel/plugin-transform-unicode-regex@npm:7.18.6" dependencies: - "@babel/helper-plugin-utils": ^7.14.5 + "@babel/helper-create-regexp-features-plugin": ^7.18.6 + "@babel/helper-plugin-utils": ^7.18.6 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: bbd1a56b095be7820029b209677b194db9b1d26691fe999856462e66b25b281f031f3dfd91b1619e9dcf95bebe336211833b854d0fb8780d618e35667c2d0d7e + checksum: d9e18d57536a2d317fb0b7c04f8f55347f3cfacb75e636b4c6fa2080ab13a3542771b5120e726b598b815891fc606d1472ac02b749c69fd527b03847f22dc25e languageName: node linkType: hard -"@babel/plugin-syntax-typescript@npm:^7.7.2": - version: 7.16.7 - resolution: "@babel/plugin-syntax-typescript@npm:7.16.7" +"@babel/preset-env@npm:7.20.2": + version: 7.20.2 + resolution: "@babel/preset-env@npm:7.20.2" dependencies: - "@babel/helper-plugin-utils": ^7.16.7 + "@babel/compat-data": ^7.20.1 + "@babel/helper-compilation-targets": ^7.20.0 + "@babel/helper-plugin-utils": ^7.20.2 + "@babel/helper-validator-option": ^7.18.6 + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": ^7.18.6 + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": ^7.18.9 + "@babel/plugin-proposal-async-generator-functions": ^7.20.1 + "@babel/plugin-proposal-class-properties": ^7.18.6 + "@babel/plugin-proposal-class-static-block": ^7.18.6 + "@babel/plugin-proposal-dynamic-import": ^7.18.6 + "@babel/plugin-proposal-export-namespace-from": ^7.18.9 + "@babel/plugin-proposal-json-strings": ^7.18.6 + "@babel/plugin-proposal-logical-assignment-operators": ^7.18.9 + "@babel/plugin-proposal-nullish-coalescing-operator": ^7.18.6 + "@babel/plugin-proposal-numeric-separator": ^7.18.6 + "@babel/plugin-proposal-object-rest-spread": ^7.20.2 + "@babel/plugin-proposal-optional-catch-binding": ^7.18.6 + "@babel/plugin-proposal-optional-chaining": ^7.18.9 + "@babel/plugin-proposal-private-methods": ^7.18.6 + "@babel/plugin-proposal-private-property-in-object": ^7.18.6 + "@babel/plugin-proposal-unicode-property-regex": ^7.18.6 + "@babel/plugin-syntax-async-generators": ^7.8.4 + "@babel/plugin-syntax-class-properties": ^7.12.13 + "@babel/plugin-syntax-class-static-block": ^7.14.5 + "@babel/plugin-syntax-dynamic-import": ^7.8.3 + "@babel/plugin-syntax-export-namespace-from": ^7.8.3 + "@babel/plugin-syntax-import-assertions": ^7.20.0 + "@babel/plugin-syntax-json-strings": ^7.8.3 + "@babel/plugin-syntax-logical-assignment-operators": ^7.10.4 + "@babel/plugin-syntax-nullish-coalescing-operator": ^7.8.3 + "@babel/plugin-syntax-numeric-separator": ^7.10.4 + "@babel/plugin-syntax-object-rest-spread": ^7.8.3 + "@babel/plugin-syntax-optional-catch-binding": ^7.8.3 + "@babel/plugin-syntax-optional-chaining": ^7.8.3 + "@babel/plugin-syntax-private-property-in-object": ^7.14.5 + "@babel/plugin-syntax-top-level-await": ^7.14.5 + "@babel/plugin-transform-arrow-functions": ^7.18.6 + "@babel/plugin-transform-async-to-generator": ^7.18.6 + "@babel/plugin-transform-block-scoped-functions": ^7.18.6 + "@babel/plugin-transform-block-scoping": ^7.20.2 + "@babel/plugin-transform-classes": ^7.20.2 + "@babel/plugin-transform-computed-properties": ^7.18.9 + "@babel/plugin-transform-destructuring": ^7.20.2 + "@babel/plugin-transform-dotall-regex": ^7.18.6 + "@babel/plugin-transform-duplicate-keys": ^7.18.9 + "@babel/plugin-transform-exponentiation-operator": ^7.18.6 + "@babel/plugin-transform-for-of": ^7.18.8 + "@babel/plugin-transform-function-name": ^7.18.9 + "@babel/plugin-transform-literals": ^7.18.9 + "@babel/plugin-transform-member-expression-literals": ^7.18.6 + "@babel/plugin-transform-modules-amd": ^7.19.6 + "@babel/plugin-transform-modules-commonjs": ^7.19.6 + "@babel/plugin-transform-modules-systemjs": ^7.19.6 + "@babel/plugin-transform-modules-umd": ^7.18.6 + "@babel/plugin-transform-named-capturing-groups-regex": ^7.19.1 + "@babel/plugin-transform-new-target": ^7.18.6 + "@babel/plugin-transform-object-super": ^7.18.6 + "@babel/plugin-transform-parameters": ^7.20.1 + "@babel/plugin-transform-property-literals": ^7.18.6 + "@babel/plugin-transform-regenerator": ^7.18.6 + "@babel/plugin-transform-reserved-words": ^7.18.6 + "@babel/plugin-transform-shorthand-properties": ^7.18.6 + "@babel/plugin-transform-spread": ^7.19.0 + "@babel/plugin-transform-sticky-regex": ^7.18.6 + "@babel/plugin-transform-template-literals": ^7.18.9 + "@babel/plugin-transform-typeof-symbol": ^7.18.9 + "@babel/plugin-transform-unicode-escapes": ^7.18.10 + "@babel/plugin-transform-unicode-regex": ^7.18.6 + "@babel/preset-modules": ^0.1.5 + "@babel/types": ^7.20.2 + babel-plugin-polyfill-corejs2: ^0.3.3 + babel-plugin-polyfill-corejs3: ^0.6.0 + babel-plugin-polyfill-regenerator: ^0.4.1 + core-js-compat: ^3.25.1 + semver: ^6.3.0 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 661e636060609ede9a402e22603b01784c21fabb0a637e65f561c8159351fe0130bbc11fdefe31902107885e3332fc34d95eb652ac61d3f61f2d61f5da20609e + checksum: ece2d7e9c7789db6116e962b8e1a55eb55c110c44c217f0c8f6ffea4ca234954e66557f7bd019b7affadf7fbb3a53ccc807e93fc935aacd48146234b73b6947e languageName: node linkType: hard -"@babel/plugin-transform-react-jsx-self@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/plugin-transform-react-jsx-self@npm:7.18.6" +"@babel/preset-modules@npm:^0.1.5": + version: 0.1.5 + resolution: "@babel/preset-modules@npm:0.1.5" dependencies: - "@babel/helper-plugin-utils": ^7.18.6 + "@babel/helper-plugin-utils": ^7.0.0 + "@babel/plugin-proposal-unicode-property-regex": ^7.4.4 + "@babel/plugin-transform-dotall-regex": ^7.4.4 + "@babel/types": ^7.4.4 + esutils: ^2.0.2 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 7d24e29c63869bb23495c163a92678c1c3341ecf74db420a20c6d3db74cbf5000fe908943f6106494e7225c0168945c150e528162274fd8fc7721966ad26930a + checksum: 8430e0e9e9d520b53e22e8c4c6a5a080a12b63af6eabe559c2310b187bd62ae113f3da82ba33e9d1d0f3230930ca702843aae9dd226dec51f7d7114dc1f51c10 languageName: node linkType: hard -"@babel/plugin-transform-react-jsx-source@npm:^7.19.6": - version: 7.19.6 - resolution: "@babel/plugin-transform-react-jsx-source@npm:7.19.6" +"@babel/preset-typescript@npm:7.18.6": + version: 7.18.6 + resolution: "@babel/preset-typescript@npm:7.18.6" dependencies: - "@babel/helper-plugin-utils": ^7.19.0 + "@babel/helper-plugin-utils": ^7.18.6 + "@babel/helper-validator-option": ^7.18.6 + "@babel/plugin-transform-typescript": ^7.18.6 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 1e9e29a4efc5b79840bd4f68e404f5ab7765ce48c7bd22f12f2b185f9c782c66933bdf54a1b21879e4e56e6b50b4e88aca82789ecb1f61123af6dfa9ab16c555 + checksum: 7fe0da5103eb72d3cf39cf3e138a794c8cdd19c0b38e3e101507eef519c46a87a0d6d0e8bc9e28a13ea2364001ebe7430b9d75758aab4c3c3a8db9a487b9dc7c languageName: node linkType: hard -"@babel/plugin-transform-react-jsx@npm:7.20.13": - version: 7.20.13 - resolution: "@babel/plugin-transform-react-jsx@npm:7.20.13" - dependencies: - "@babel/helper-annotate-as-pure": ^7.18.6 - "@babel/helper-module-imports": ^7.18.6 - "@babel/helper-plugin-utils": ^7.20.2 - "@babel/plugin-syntax-jsx": ^7.18.6 - "@babel/types": ^7.20.7 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: b1daaa9b093ab59f71572dde7ad05ed3490433a47de103fc866f60347da55fa7fe84cf9b4c9fa22917517d52f70ab5e05ec631bba1c348733c0d8ebbd7de8c68 +"@babel/regjsgen@npm:^0.8.0": + version: 0.8.0 + resolution: "@babel/regjsgen@npm:0.8.0" + checksum: 89c338fee774770e5a487382170711014d49a68eb281e74f2b5eac88f38300a4ad545516a7786a8dd5702e9cf009c94c2f582d200f077ac5decd74c56b973730 languageName: node linkType: hard @@ -871,6 +1848,26 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4": + version: 7.21.0 + resolution: "@babel/runtime@npm:7.21.0" + dependencies: + regenerator-runtime: ^0.13.11 + checksum: 7b33e25bfa9e0e1b9e8828bb61b2d32bdd46b41b07ba7cb43319ad08efc6fda8eb89445193e67d6541814627df0ca59122c0ea795e412b99c5183a0540d338ab + languageName: node + linkType: hard + +"@babel/template@npm:^7.14.5, @babel/template@npm:^7.20.7": + version: 7.20.7 + resolution: "@babel/template@npm:7.20.7" + dependencies: + "@babel/code-frame": ^7.18.6 + "@babel/parser": ^7.20.7 + "@babel/types": ^7.20.7 + checksum: 2eb1a0ab8d415078776bceb3473d07ab746e6bb4c2f6ca46ee70efb284d75c4a32bb0cd6f4f4946dec9711f9c0780e8e5d64b743208deac6f8e9858afadc349e + languageName: node + linkType: hard + "@babel/template@npm:^7.16.7": version: 7.16.7 resolution: "@babel/template@npm:7.16.7" @@ -893,17 +1890,6 @@ __metadata: languageName: node linkType: hard -"@babel/template@npm:^7.20.7": - version: 7.20.7 - resolution: "@babel/template@npm:7.20.7" - dependencies: - "@babel/code-frame": ^7.18.6 - "@babel/parser": ^7.20.7 - "@babel/types": ^7.20.7 - checksum: 2eb1a0ab8d415078776bceb3473d07ab746e6bb4c2f6ca46ee70efb284d75c4a32bb0cd6f4f4946dec9711f9c0780e8e5d64b743208deac6f8e9858afadc349e - languageName: node - linkType: hard - "@babel/template@npm:^7.3.3": version: 7.14.5 resolution: "@babel/template@npm:7.14.5" @@ -987,6 +1973,24 @@ __metadata: languageName: node linkType: hard +"@babel/traverse@npm:^7.20.7, @babel/traverse@npm:^7.21.2": + version: 7.21.2 + resolution: "@babel/traverse@npm:7.21.2" + dependencies: + "@babel/code-frame": ^7.18.6 + "@babel/generator": ^7.21.1 + "@babel/helper-environment-visitor": ^7.18.9 + "@babel/helper-function-name": ^7.21.0 + "@babel/helper-hoist-variables": ^7.18.6 + "@babel/helper-split-export-declaration": ^7.18.6 + "@babel/parser": ^7.21.2 + "@babel/types": ^7.21.2 + debug: ^4.1.0 + globals: ^11.1.0 + checksum: d851e3f5cfbdc2fac037a014eae7b0707709de50f7d2fbb82ffbf932d3eeba90a77431529371d6e544f8faaf8c6540eeb18fdd8d1c6fa2b61acea0fb47e18d4b + languageName: node + linkType: hard + "@babel/types@npm:^7.0.0, @babel/types@npm:^7.3.0": version: 7.5.0 resolution: "@babel/types@npm:7.5.0" @@ -1039,6 +2043,17 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.18.9, @babel/types@npm:^7.20.0, @babel/types@npm:^7.21.0, @babel/types@npm:^7.21.2, @babel/types@npm:^7.4.4": + version: 7.21.2 + resolution: "@babel/types@npm:7.21.2" + dependencies: + "@babel/helper-string-parser": ^7.19.4 + "@babel/helper-validator-identifier": ^7.19.1 + to-fast-properties: ^2.0.0 + checksum: a45a52acde139e575502c6de42c994bdbe262bafcb92ae9381fb54cdf1a3672149086843fda655c7683ce9806e998fd002bbe878fa44984498d0fdc7935ce7ff + languageName: node + linkType: hard + "@babel/types@npm:^7.20.7": version: 7.20.7 resolution: "@babel/types@npm:7.20.7" @@ -1079,6 +2094,25 @@ __metadata: languageName: node linkType: hard +"@emotion/babel-plugin@npm:11.10.6": + version: 11.10.6 + resolution: "@emotion/babel-plugin@npm:11.10.6" + dependencies: + "@babel/helper-module-imports": ^7.16.7 + "@babel/runtime": ^7.18.3 + "@emotion/hash": ^0.9.0 + "@emotion/memoize": ^0.8.0 + "@emotion/serialize": ^1.1.1 + babel-plugin-macros: ^3.1.0 + convert-source-map: ^1.5.0 + escape-string-regexp: ^4.0.0 + find-root: ^1.1.0 + source-map: ^0.5.7 + stylis: 4.1.3 + checksum: 3eed138932e8edf2598352e69ad949b9db3051a4d6fcff190dacbac9aa838d7ef708b9f3e6c48660625d9311dae82d73477ae4e7a31139feef5eb001a5528421 + languageName: node + linkType: hard + "@emotion/babel-plugin@npm:^11.10.5": version: 11.10.5 resolution: "@emotion/babel-plugin@npm:11.10.5" @@ -2100,7 +3134,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.15, @jridgewell/trace-mapping@npm:^0.3.9": +"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.15, @jridgewell/trace-mapping@npm:^0.3.17, @jridgewell/trace-mapping@npm:^0.3.9": version: 0.3.17 resolution: "@jridgewell/trace-mapping@npm:0.3.17" dependencies: @@ -2236,6 +3270,15 @@ __metadata: languageName: node linkType: hard +"@primer/octicons-react@npm:17.11.1": + version: 17.11.1 + resolution: "@primer/octicons-react@npm:17.11.1" + peerDependencies: + react: ">=15" + checksum: 21d99a0f4d86b27977a9845cca34c75d23cd3d057d3ab39e8b308bd45a35c4c8656a82c464d9b960e151dc05c0a2a40f2a9df5047f264da91b6bc7cbe0f70b33 + languageName: node + linkType: hard + "@remix-run/router@npm:1.3.0": version: 1.3.0 resolution: "@remix-run/router@npm:1.3.0" @@ -3405,6 +4448,7 @@ __metadata: "@emotion/jest": 11.10.5 "@emotion/react": 11.10.5 "@emotion/styled": 11.10.5 + "@primer/octicons-react": 17.11.1 "@swc/core": 1.3.28 "@swc/jest": 0.2.24 "@testing-library/dom": 8.20.0 @@ -3488,7 +4532,7 @@ __metadata: react-select-event: 5.5.1 react-virtualized: 9.22.3 regenerator-runtime: 0.13.11 - tailwindcss: 3.2.6 + tailwindcss: 2.2.19 testing-library-selector: 0.2.1 turbo: 1.7.4 typescript: 4.9.4 @@ -3755,7 +4799,7 @@ __metadata: languageName: node linkType: hard -"arg@npm:^5.0.2": +"arg@npm:^5.0.1": version: 5.0.2 resolution: "arg@npm:5.0.2" checksum: 6c69ada1a9943d332d9e5382393e897c500908d91d5cb735a01120d5f71daf1b339b7b8980cbeaba8fd1afc68e658a739746179e4315a26e8a28951ff9930078 @@ -3919,7 +4963,7 @@ __metadata: languageName: node linkType: hard -"autoprefixer@npm:10.4.13": +"autoprefixer@npm:10.4.13, autoprefixer@npm:^10.2.5": version: 10.4.13 resolution: "autoprefixer@npm:10.4.13" dependencies: @@ -4002,6 +5046,17 @@ __metadata: languageName: node linkType: hard +"babel-plugin-macros@npm:^2.8.0": + version: 2.8.0 + resolution: "babel-plugin-macros@npm:2.8.0" + dependencies: + "@babel/runtime": ^7.7.2 + cosmiconfig: ^6.0.0 + resolve: ^1.12.0 + checksum: 59b09a21cf3ae1e14186c1b021917d004b49b953824b24953a54c6502da79e8051d4ac31cfd4a0ae7f6ea5ddf1f7edd93df4895dd3c3982a5b2431859c2889ac + languageName: node + linkType: hard + "babel-plugin-macros@npm:^3.1.0": version: 3.1.0 resolution: "babel-plugin-macros@npm:3.1.0" @@ -4013,6 +5068,42 @@ __metadata: languageName: node linkType: hard +"babel-plugin-polyfill-corejs2@npm:^0.3.3": + version: 0.3.3 + resolution: "babel-plugin-polyfill-corejs2@npm:0.3.3" + dependencies: + "@babel/compat-data": ^7.17.7 + "@babel/helper-define-polyfill-provider": ^0.3.3 + semver: ^6.1.1 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 7db3044993f3dddb3cc3d407bc82e640964a3bfe22de05d90e1f8f7a5cb71460011ab136d3c03c6c1ba428359ebf635688cd6205e28d0469bba221985f5c6179 + languageName: node + linkType: hard + +"babel-plugin-polyfill-corejs3@npm:^0.6.0": + version: 0.6.0 + resolution: "babel-plugin-polyfill-corejs3@npm:0.6.0" + dependencies: + "@babel/helper-define-polyfill-provider": ^0.3.3 + core-js-compat: ^3.25.1 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 470bb8c59f7c0912bd77fe1b5a2e72f349b3f65bbdee1d60d6eb7e1f4a085c6f24b2dd5ab4ac6c2df6444a96b070ef6790eccc9edb6a2668c60d33133bfb62c6 + languageName: node + linkType: hard + +"babel-plugin-polyfill-regenerator@npm:^0.4.1": + version: 0.4.1 + resolution: "babel-plugin-polyfill-regenerator@npm:0.4.1" + dependencies: + "@babel/helper-define-polyfill-provider": ^0.3.3 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: ab0355efbad17d29492503230387679dfb780b63b25408990d2e4cf421012dae61d6199ddc309f4d2409ce4e9d3002d187702700dd8f4f8770ebbba651ed066c + languageName: node + linkType: hard + "babel-preset-current-node-syntax@npm:^1.0.0": version: 1.0.1 resolution: "babel-preset-current-node-syntax@npm:1.0.1" @@ -4087,7 +5178,7 @@ __metadata: languageName: node linkType: hard -"braces@npm:^3.0.1, braces@npm:^3.0.2, braces@npm:~3.0.2": +"braces@npm:^3.0.1, braces@npm:~3.0.2": version: 3.0.2 resolution: "braces@npm:3.0.2" dependencies: @@ -4125,6 +5216,20 @@ __metadata: languageName: node linkType: hard +"browserslist@npm:^4.21.5": + version: 4.21.5 + resolution: "browserslist@npm:4.21.5" + dependencies: + caniuse-lite: ^1.0.30001449 + electron-to-chromium: ^1.4.284 + node-releases: ^2.0.8 + update-browserslist-db: ^1.0.10 + bin: + browserslist: cli.js + checksum: 9755986b22e73a6a1497fd8797aedd88e04270be33ce66ed5d85a1c8a798292a65e222b0f251bafa1c2522261e237d73b08b58689d4920a607e5a53d56dc4706 + languageName: node + linkType: hard + "bser@npm:^2.0.0": version: 2.1.0 resolution: "bser@npm:2.1.0" @@ -4141,6 +5246,13 @@ __metadata: languageName: node linkType: hard +"bytes@npm:^3.0.0": + version: 3.1.2 + resolution: "bytes@npm:3.1.2" + checksum: e4bcd3948d289c5127591fbedf10c0b639ccbf00243504e4e127374a15c3bc8eed0d28d4aaab08ff6f1cf2abc0cce6ba3085ed32f4f90e82a5683ce0014e1b6e + languageName: node + linkType: hard + "cacache@npm:^15.0.5": version: 15.3.0 resolution: "cacache@npm:15.3.0" @@ -4226,7 +5338,14 @@ __metadata: languageName: node linkType: hard -"chalk@npm:4.1.2, chalk@npm:^4.1.0": +"caniuse-lite@npm:^1.0.30001449": + version: 1.0.30001457 + resolution: "caniuse-lite@npm:1.0.30001457" + checksum: f311a7c5098681962402a86a0a367014ee91c3135395ee68bbfaf45caf0e36d581e42d7c5b1526ce99484a228e6cf5cf0e400678292c65f5a21512a3fc7a5fb6 + languageName: node + linkType: hard + +"chalk@npm:4.1.2, chalk@npm:^4.1.0, chalk@npm:^4.1.2": version: 4.1.2 resolution: "chalk@npm:4.1.2" dependencies: @@ -4288,7 +5407,7 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:^3.5.3": +"chokidar@npm:^3.5.2": version: 3.5.3 resolution: "chokidar@npm:3.5.3" dependencies: @@ -4342,6 +5461,13 @@ __metadata: languageName: node linkType: hard +"clean-set@npm:^1.1.1": + version: 1.1.2 + resolution: "clean-set@npm:1.1.2" + checksum: 1bd86cc20a1f5834b2e081f96b4336c4619b8eb842392b496b748d63e1ee58aa1959aa13cd392e96988600e105b9067d29082a13ae80f3b92d55c0474c0fda08 + languageName: node + linkType: hard + "clean-stack@npm:^2.0.0": version: 2.2.0 resolution: "clean-stack@npm:2.2.0" @@ -4406,7 +5532,7 @@ __metadata: languageName: node linkType: hard -"color-convert@npm:^1.9.0": +"color-convert@npm:^1.9.0, color-convert@npm:^1.9.3": version: 1.9.3 resolution: "color-convert@npm:1.9.3" dependencies: @@ -4431,13 +5557,43 @@ __metadata: languageName: node linkType: hard -"color-name@npm:^1.1.4, color-name@npm:~1.1.4": +"color-name@npm:^1.0.0, color-name@npm:~1.1.4": version: 1.1.4 resolution: "color-name@npm:1.1.4" checksum: b0445859521eb4021cd0fb0cc1a75cecf67fceecae89b63f62b201cca8d345baf8b952c966862a9d9a2632987d4f6581f0ec8d957dfacece86f0a7919316f610 languageName: node linkType: hard +"color-string@npm:^1.6.0, color-string@npm:^1.9.0": + version: 1.9.1 + resolution: "color-string@npm:1.9.1" + dependencies: + color-name: ^1.0.0 + simple-swizzle: ^0.2.2 + checksum: c13fe7cff7885f603f49105827d621ce87f4571d78ba28ef4a3f1a104304748f620615e6bf065ecd2145d0d9dad83a3553f52bb25ede7239d18e9f81622f1cc5 + languageName: node + linkType: hard + +"color@npm:^3.1.3": + version: 3.2.1 + resolution: "color@npm:3.2.1" + dependencies: + color-convert: ^1.9.3 + color-string: ^1.6.0 + checksum: f81220e8b774d35865c2561be921f5652117638dcda7ca4029262046e37fc2444ac7bbfdd110cf1fd9c074a4ee5eda8f85944ffbdda26186b602dd9bb05f6400 + languageName: node + linkType: hard + +"color@npm:^4.0.1": + version: 4.2.3 + resolution: "color@npm:4.2.3" + dependencies: + color-convert: ^2.0.1 + color-string: ^1.9.0 + checksum: 0579629c02c631b426780038da929cca8e8d80a40158b09811a0112a107c62e10e4aad719843b791b1e658ab4e800558f2e87ca4522c8b32349d497ecb6adeb4 + languageName: node + linkType: hard + "colors@npm:~1.2.1": version: 1.2.5 resolution: "colors@npm:1.2.5" @@ -4461,6 +5617,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:^8.0.0": + version: 8.3.0 + resolution: "commander@npm:8.3.0" + checksum: 0f82321821fc27b83bd409510bb9deeebcfa799ff0bf5d102128b500b7af22872c0c92cb6a0ebc5a4cf19c6b550fba9cedfa7329d18c6442a625f851377bacf0 + languageName: node + linkType: hard + "commander@npm:^9.4.1": version: 9.5.0 resolution: "commander@npm:9.5.0" @@ -4507,6 +5670,15 @@ __metadata: languageName: node linkType: hard +"core-js-compat@npm:^3.25.1": + version: 3.28.0 + resolution: "core-js-compat@npm:3.28.0" + dependencies: + browserslist: ^4.21.5 + checksum: 41d1d58c99ce7ee7abd8cf070f4c07a8f2655dbed1777d90a26246dddd7fac68315d53d2192584c8621a5328e6fe1a10da39b6bf2666e90fd5c2ff3b8f24e874 + languageName: node + linkType: hard + "core-js@npm:3.27.2": version: 3.27.2 resolution: "core-js@npm:3.27.2" @@ -4521,6 +5693,19 @@ __metadata: languageName: node linkType: hard +"cosmiconfig@npm:^6.0.0": + version: 6.0.0 + resolution: "cosmiconfig@npm:6.0.0" + dependencies: + "@types/parse-json": ^4.0.0 + import-fresh: ^3.1.0 + parse-json: ^5.0.0 + path-type: ^4.0.0 + yaml: ^1.7.2 + checksum: 8eed7c854b91643ecb820767d0deb038b50780ecc3d53b0b19e03ed8aabed4ae77271198d1ae3d49c3b110867edf679f5faad924820a8d1774144a87cb6f98fc + languageName: node + linkType: hard + "cosmiconfig@npm:^7.0.0": version: 7.0.1 resolution: "cosmiconfig@npm:7.0.1" @@ -4534,6 +5719,19 @@ __metadata: languageName: node linkType: hard +"cosmiconfig@npm:^7.0.1": + version: 7.1.0 + resolution: "cosmiconfig@npm:7.1.0" + dependencies: + "@types/parse-json": ^4.0.0 + import-fresh: ^3.2.1 + parse-json: ^5.0.0 + path-type: ^4.0.0 + yaml: ^1.10.0 + checksum: c53bf7befc1591b2651a22414a5e786cd5f2eeaa87f3678a3d49d6069835a9d8d1aef223728e98aa8fec9a95bf831120d245096db12abe019fecb51f5696c96f + languageName: node + linkType: hard + "cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3": version: 7.0.3 resolution: "cross-spawn@npm:7.0.3" @@ -4545,6 +5743,13 @@ __metadata: languageName: node linkType: hard +"css-color-names@npm:^0.0.4": + version: 0.0.4 + resolution: "css-color-names@npm:0.0.4" + checksum: 9c6106320430a9da3a13daab8d8b4def39113edbfb68042444585d9a214af5fd5cb384b9be45124bc75f88261d461b517e00e278f4d2e0ab5a619b182f9f0e2d + languageName: node + linkType: hard + "css-select@npm:~1.2.0": version: 1.2.0 resolution: "css-select@npm:1.2.0" @@ -4557,6 +5762,13 @@ __metadata: languageName: node linkType: hard +"css-unit-converter@npm:^1.1.1": + version: 1.1.2 + resolution: "css-unit-converter@npm:1.1.2" + checksum: 07888033346a5128f34dbe2f72884c966d24e9f29db24416dcde92860242490617ef9a178ac193a92f730834bbeea026cdc7027701d92ba9bbbe59db7a37eb2a + languageName: node + linkType: hard + "css-what@npm:2.1": version: 2.1.3 resolution: "css-what@npm:2.1.3" @@ -4979,22 +6191,44 @@ __metadata: dependencies: "@babel/core": 7.20.5 "@babel/plugin-transform-react-jsx": 7.20.13 + "@babel/preset-env": 7.20.2 + "@babel/preset-typescript": 7.18.6 + "@emotion/babel-plugin": 11.10.6 "@emotion/babel-plugin-jsx-pragmatic": 0.2.0 + "@testing-library/dom": 8.20.0 + "@testing-library/jest-dom": 5.16.5 + "@testing-library/react": 12.1.5 + "@testing-library/user-event": 14.4.3 + "@types/react": 16.14.34 + "@typescript-eslint/parser": 5.49.0 "@vitejs/plugin-react": 3.1.0 + autoprefixer: 10.4.13 + eslint: 8.32.0 eslint-plugin-header: 3.1.1 eslint-plugin-typescript-sort-keys: 2.1.0 - twin.macro: 3.1.0 + history: 5.3.0 + jest: 29.3.1 + postcss: 8.4.21 + postcss-calc: 8.2.4 + postcss-custom-properties: 12.1.11 + twin.macro: 2.8.2 + typescript: 4.9.4 vite: 4.1.1 - vite-plugin-dts: 1.7.2 + vite-plugin-dts: 2.0.2 + whatwg-fetch: 3.6.2 peerDependencies: "@emotion/react": 11.10.5 "@emotion/styled": 11.10.5 - "@typescript-eslint/parser": 5.49.0 - eslint: 8.32.0 + "@primer/octicons-react": 17.11.1 + classnames: 2.3.2 + clipboard: 2.0.11 + lodash: 4.17.21 react: 16.14.0 react-dom: 16.14.0 - tailwindcss: 3.2.6 - typescript: 4.9.4 + react-helmet-async: 1.3.0 + react-intl: 6.2.5 + react-router-dom: 6.7.0 + tailwindcss: 2.2.19 languageName: unknown linkType: soft @@ -5005,7 +6239,7 @@ __metadata: languageName: node linkType: hard -"detective@npm:^5.2.1": +"detective@npm:^5.2.0": version: 5.2.1 resolution: "detective@npm:5.2.1" dependencies: @@ -5166,6 +6400,13 @@ __metadata: languageName: node linkType: hard +"dset@npm:^2.0.1": + version: 2.1.0 + resolution: "dset@npm:2.1.0" + checksum: 9fdb325dde5cedb264e37a1bece8aeefb88023e9f2bbc9c38d656cd06b2322a050ba5b361e1c8047b84e482d6e50d0d6b10a6bf439afdd8ab131fd039c103483 + languageName: node + linkType: hard + "electron-to-chromium@npm:^1.4.17": version: 1.4.64 resolution: "electron-to-chromium@npm:1.4.64" @@ -5180,6 +6421,13 @@ __metadata: languageName: node linkType: hard +"electron-to-chromium@npm:^1.4.284": + version: 1.4.308 + resolution: "electron-to-chromium@npm:1.4.308" + checksum: 6e49a6c0e0ae2c4be3d5acd76ba6d497383a1ceb224cada6cdbba83aa95952336f96aa742cbf3697dd39d091624b53a912c0015fe5a30a8d6138e14287a0a9ad + languageName: node + linkType: hard + "emittery@npm:^0.13.1": version: 0.13.1 resolution: "emittery@npm:0.13.1" @@ -6152,7 +7400,7 @@ __metadata: languageName: node linkType: hard -"fast-glob@npm:^3.2.12": +"fast-glob@npm:^3.2.12, fast-glob@npm:^3.2.7": version: 3.2.12 resolution: "fast-glob@npm:3.2.12" dependencies: @@ -6343,7 +7591,7 @@ __metadata: languageName: node linkType: hard -"fs-extra@npm:^10.1.0": +"fs-extra@npm:^10.0.0, fs-extra@npm:^10.1.0": version: 10.1.0 resolution: "fs-extra@npm:10.1.0" dependencies: @@ -6533,7 +7781,7 @@ __metadata: languageName: node linkType: hard -"glob-parent@npm:^6.0.2": +"glob-parent@npm:^6.0.1, glob-parent@npm:^6.0.2": version: 6.0.2 resolution: "glob-parent@npm:6.0.2" dependencies: @@ -6556,6 +7804,20 @@ __metadata: languageName: node linkType: hard +"glob@npm:^7.1.7": + version: 7.2.3 + resolution: "glob@npm:7.2.3" + dependencies: + fs.realpath: ^1.0.0 + inflight: ^1.0.4 + inherits: 2 + minimatch: ^3.1.1 + once: ^1.3.0 + path-is-absolute: ^1.0.0 + checksum: 29452e97b38fa704dabb1d1045350fb2467cf0277e155aa9ff7077e90ad81d1ea9d53d3ee63bd37c05b09a065e90f16aec4a65f5b8de401d1dac40bc5605d133 + languageName: node + linkType: hard + "globals@npm:^11.1.0": version: 11.12.0 resolution: "globals@npm:11.12.0" @@ -6722,6 +7984,22 @@ __metadata: languageName: node linkType: hard +"hex-color-regex@npm:^1.1.0": + version: 1.1.0 + resolution: "hex-color-regex@npm:1.1.0" + checksum: 44fa1b7a26d745012f3bfeeab8015f60514f72d2fcf10dce33068352456b8d71a2e6bc5a17f933ab470da2c5ab1e3e04b05caf3fefe3c1cabd7e02e516fc8784 + languageName: node + linkType: hard + +"history@npm:5.3.0": + version: 5.3.0 + resolution: "history@npm:5.3.0" + dependencies: + "@babel/runtime": ^7.7.6 + checksum: d73c35df49d19ac172f9547d30a21a26793e83f16a78386d99583b5bf1429cc980799fcf1827eb215d31816a6600684fba9686ce78104e23bd89ec239e7c726f + languageName: node + linkType: hard + "hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.1, hoist-non-react-statics@npm:^3.3.2": version: 3.3.2 resolution: "hoist-non-react-statics@npm:3.3.2" @@ -6731,6 +8009,20 @@ __metadata: languageName: node linkType: hard +"hsl-regex@npm:^1.0.0": + version: 1.0.0 + resolution: "hsl-regex@npm:1.0.0" + checksum: de9ee1bf39de1b83cc3fa0fa1cc337f29f14911e79411d66347365c54fab6b109eea2dd741eaa02486e24de31627ad7bf4453f22224fb55a2fe2b58166fa63b8 + languageName: node + linkType: hard + +"hsla-regex@npm:^1.0.0": + version: 1.0.0 + resolution: "hsla-regex@npm:1.0.0" + checksum: 9aa6eb9ff6c102d2395435aa5d1d91eae20043c4b1497c543d8db501c05f3edacd9a07fb34a987059d7902dba415af4cb4e610f751859ae8e7525df4ffcd085f + languageName: node + linkType: hard + "html-element-map@npm:^1.2.0": version: 1.2.0 resolution: "html-element-map@npm:1.2.0" @@ -6756,6 +8048,13 @@ __metadata: languageName: node linkType: hard +"html-tags@npm:^3.1.0": + version: 3.2.0 + resolution: "html-tags@npm:3.2.0" + checksum: a0c9e96ac26c84adad9cc66d15d6711a17f60acda8d987218f1d4cbaacd52864939b230e635cce5a1179f3ddab2a12b9231355617dfbae7945fcfec5e96d2041 + languageName: node + linkType: hard + "htmlparser2@npm:^3.9.1": version: 3.10.1 resolution: "htmlparser2@npm:3.10.1" @@ -6872,7 +8171,7 @@ __metadata: languageName: node linkType: hard -"import-fresh@npm:^3.2.1": +"import-fresh@npm:^3.1.0, import-fresh@npm:^3.2.1": version: 3.3.0 resolution: "import-fresh@npm:3.3.0" dependencies: @@ -7024,6 +8323,13 @@ __metadata: languageName: node linkType: hard +"is-arrayish@npm:^0.3.1": + version: 0.3.2 + resolution: "is-arrayish@npm:0.3.2" + checksum: 977e64f54d91c8f169b59afcd80ff19227e9f5c791fa28fa2e5bce355cbaf6c2c356711b734656e80c9dd4a854dd7efcf7894402f1031dfc5de5d620775b4d5f + languageName: node + linkType: hard + "is-bigint@npm:^1.0.1": version: 1.0.4 resolution: "is-bigint@npm:1.0.4" @@ -7094,6 +8400,20 @@ __metadata: languageName: node linkType: hard +"is-color-stop@npm:^1.1.0": + version: 1.1.0 + resolution: "is-color-stop@npm:1.1.0" + dependencies: + css-color-names: ^0.0.4 + hex-color-regex: ^1.1.0 + hsl-regex: ^1.0.0 + hsla-regex: ^1.0.0 + rgb-regex: ^1.0.1 + rgba-regex: ^1.0.0 + checksum: 778dd52a603ab8da827925aa4200fe6733b667b216495a04110f038b925dc5ef58babe759b94ffc4e44fcf439328695770873937f59d6045f676322b97f3f92d + languageName: node + linkType: hard + "is-core-module@npm:^2.1.0, is-core-module@npm:^2.11.0": version: 2.11.0 resolution: "is-core-module@npm:2.11.0" @@ -8091,6 +9411,15 @@ __metadata: languageName: node linkType: hard +"jsesc@npm:~0.5.0": + version: 0.5.0 + resolution: "jsesc@npm:0.5.0" + bin: + jsesc: bin/jsesc + checksum: b8b44cbfc92f198ad972fba706ee6a1dfa7485321ee8c0b25f5cedd538dcb20cde3197de16a7265430fce8277a12db066219369e3d51055038946039f6e20e17 + languageName: node + linkType: hard + "json-parse-even-better-errors@npm:^2.3.0": version: 2.3.1 resolution: "json-parse-even-better-errors@npm:2.3.1" @@ -8268,7 +9597,7 @@ __metadata: languageName: node linkType: hard -"lilconfig@npm:^2.0.5, lilconfig@npm:^2.0.6": +"lilconfig@npm:^2.0.5": version: 2.0.6 resolution: "lilconfig@npm:2.0.6" checksum: 40a3cd72f103b1be5975f2ac1850810b61d4053e20ab09be8d3aeddfe042187e1ba70b4651a7e70f95efa1642e7dc8b2ae395b317b7d7753b241b43cef7c0f7d @@ -8307,6 +9636,13 @@ __metadata: languageName: node linkType: hard +"lodash.debounce@npm:^4.0.8": + version: 4.0.8 + resolution: "lodash.debounce@npm:4.0.8" + checksum: a3f527d22c548f43ae31c861ada88b2637eb48ac6aa3eb56e82d44917971b8aa96fbb37aa60efea674dc4ee8c42074f90f7b1f772e9db375435f6c83a19b3bc6 + languageName: node + linkType: hard + "lodash.escape@npm:^4.0.1": version: 4.0.1 resolution: "lodash.escape@npm:4.0.1" @@ -8314,6 +9650,13 @@ __metadata: languageName: node linkType: hard +"lodash.flatmap@npm:^4.5.0": + version: 4.5.0 + resolution: "lodash.flatmap@npm:4.5.0" + checksum: c01a47d32e99f8fce75409f0a4a9bd12fbb2d3a46519a0dde14deedb1e527b5ddccc2bf997705c67bdecb915f47749e8a9ffefa7a91c41f0c448e06348ec81c7 + languageName: node + linkType: hard + "lodash.flattendeep@npm:^4.4.0": version: 4.4.0 resolution: "lodash.flattendeep@npm:4.4.0" @@ -8342,6 +9685,13 @@ __metadata: languageName: node linkType: hard +"lodash.topath@npm:^4.5.2": + version: 4.5.2 + resolution: "lodash.topath@npm:4.5.2" + checksum: 04583e220f4bb1c4ac0008ff8f46d9cb4ddce0ea1090085790da30a41f4cb1b904d885cb73257fca619fa825cd96f9bb97c67d039635cb76056e18f5e08bfdee + languageName: node + linkType: hard + "lodash@npm:4.17.21, lodash@npm:^4.15.0, lodash@npm:^4.17.11, lodash@npm:^4.17.13, lodash@npm:^4.17.15, lodash@npm:^4.17.21, lodash@npm:~4.17.15": version: 4.17.21 resolution: "lodash@npm:4.17.21" @@ -8403,6 +9753,15 @@ __metadata: languageName: node linkType: hard +"magic-string@npm:^0.29.0": + version: 0.29.0 + resolution: "magic-string@npm:0.29.0" + dependencies: + "@jridgewell/sourcemap-codec": ^1.4.13 + checksum: 19e5398fcfc44804917127c72ad622c68a19a0a10cbdb8d4f9f9417584a087fe9e117140bfb2463d86743cf1ed9cf4182ae0b0ad1a7536f7fdda257ee4449ffb + languageName: node + linkType: hard + "make-dir@npm:^3.0.0": version: 3.0.2 resolution: "make-dir@npm:3.0.2" @@ -8475,16 +9834,6 @@ __metadata: languageName: node linkType: hard -"micromatch@npm:^4.0.5": - version: 4.0.5 - resolution: "micromatch@npm:4.0.5" - dependencies: - braces: ^3.0.2 - picomatch: ^2.3.1 - checksum: 02a17b671c06e8fefeeb6ef996119c1e597c942e632a21ef589154f23898c9c6a9858526246abb14f8bca6e77734aa9dcf65476fca47cedfb80d9577d52843fc - languageName: node - linkType: hard - "mime-db@npm:1.40.0": version: 1.40.0 resolution: "mime-db@npm:1.40.0" @@ -8524,7 +9873,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^3.0.5, minimatch@npm:^3.1.2": +"minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" dependencies: @@ -8635,6 +9984,13 @@ __metadata: languageName: node linkType: hard +"modern-normalize@npm:^1.1.0": + version: 1.1.0 + resolution: "modern-normalize@npm:1.1.0" + checksum: edfd40650bd7250eb4761651886a02ca3c524effca41b9832932eab9ccf9f2cfa7e5da8491c7c8bc2d58e1696e5e765adebeaf90cd9d3376444bd6bc0b0f2c99 + languageName: node + linkType: hard + "moo@npm:^0.4.3": version: 0.4.3 resolution: "moo@npm:0.4.3" @@ -8697,6 +10053,15 @@ __metadata: languageName: node linkType: hard +"node-emoji@npm:^1.11.0": + version: 1.11.0 + resolution: "node-emoji@npm:1.11.0" + dependencies: + lodash: ^4.17.21 + checksum: e8c856c04a1645062112a72e59a98b203505ed5111ff84a3a5f40611afa229b578c7d50f1e6a7f17aa62baeea4a640d2e2f61f63afc05423aa267af10977fb2b + languageName: node + linkType: hard + "node-gyp@npm:latest": version: 8.2.0 resolution: "node-gyp@npm:8.2.0" @@ -8738,6 +10103,13 @@ __metadata: languageName: node linkType: hard +"node-releases@npm:^2.0.8": + version: 2.0.10 + resolution: "node-releases@npm:2.0.10" + checksum: d784ecde25696a15d449c4433077f5cce620ed30a1656c4abf31282bfc691a70d9618bae6868d247a67914d1be5cc4fde22f65a05f4398cdfb92e0fc83cadfbc + languageName: node + linkType: hard + "nopt@npm:^5.0.0": version: 5.0.0 resolution: "nopt@npm:5.0.0" @@ -8814,10 +10186,10 @@ __metadata: languageName: node linkType: hard -"object-hash@npm:^3.0.0": - version: 3.0.0 - resolution: "object-hash@npm:3.0.0" - checksum: 80b4904bb3857c52cc1bfd0b52c0352532ca12ed3b8a6ff06a90cd209dfda1b95cee059a7625eb9da29537027f68ac4619363491eedb2f5d3dddbba97494fd6c +"object-hash@npm:^2.2.0": + version: 2.2.0 + resolution: "object-hash@npm:2.2.0" + checksum: 55ba841e3adce9c4f1b9b46b41983eda40f854e0d01af2802d3ae18a7085a17168d6b81731d43fdf1d6bcbb3c9f9c56d22c8fea992203ad90a38d7d919bc28f1 languageName: node linkType: hard @@ -9224,13 +10596,6 @@ __metadata: languageName: node linkType: hard -"pify@npm:^2.3.0": - version: 2.3.0 - resolution: "pify@npm:2.3.0" - checksum: 9503aaeaf4577acc58642ad1d25c45c6d90288596238fb68f82811c08104c800e5a7870398e9f015d82b44ecbcbef3dc3d4251a1cbb582f6e5959fe09884b2ba - languageName: node - linkType: hard - "pirates@npm:^4.0.4": version: 4.0.5 resolution: "pirates@npm:4.0.5" @@ -9270,31 +10635,17 @@ __metadata: languageName: node linkType: hard -"postcss-import@npm:^14.1.0": - version: 14.1.0 - resolution: "postcss-import@npm:14.1.0" - dependencies: - postcss-value-parser: ^4.0.0 - read-cache: ^1.0.0 - resolve: ^1.1.7 - peerDependencies: - postcss: ^8.0.0 - checksum: cd45d406e90f67cdab9524352e573cc6b4462b790934a05954e929a6653ebd31288ceebc8ce3c3ed7117ae672d9ebbec57df0bceec0a56e9b259c2e71d47ca86 - languageName: node - linkType: hard - -"postcss-js@npm:^4.0.0": - version: 4.0.1 - resolution: "postcss-js@npm:4.0.1" +"postcss-js@npm:^3.0.3": + version: 3.0.3 + resolution: "postcss-js@npm:3.0.3" dependencies: camelcase-css: ^2.0.1 - peerDependencies: - postcss: ^8.4.21 - checksum: 5c1e83efeabeb5a42676193f4357aa9c88f4dc1b3c4a0332c132fe88932b33ea58848186db117cf473049fc233a980356f67db490bd0a7832ccba9d0b3fd3491 + postcss: ^8.1.6 + checksum: cc17f59f2b9bb22ed1cf9daab1f9944635b0713dce923ff7d9fd10b89393fc9aa1fab43a97f9a71295827fa32c9676d52661d7d6a693ecc0c41541ee928c781e languageName: node linkType: hard -"postcss-load-config@npm:^3.1.4": +"postcss-load-config@npm:^3.1.0": version: 3.1.4 resolution: "postcss-load-config@npm:3.1.4" dependencies: @@ -9312,18 +10663,18 @@ __metadata: languageName: node linkType: hard -"postcss-nested@npm:6.0.0": - version: 6.0.0 - resolution: "postcss-nested@npm:6.0.0" +"postcss-nested@npm:5.0.6": + version: 5.0.6 + resolution: "postcss-nested@npm:5.0.6" dependencies: - postcss-selector-parser: ^6.0.10 + postcss-selector-parser: ^6.0.6 peerDependencies: postcss: ^8.2.14 - checksum: 2105dc52cd19747058f1a46862c9e454b5a365ac2e7135fc1015d67a8fe98ada2a8d9ee578e90f7a093bd55d3994dd913ba5ff1d5e945b4ed9a8a2992ecc8f10 + checksum: dbcbfd11e514f485ac0d2b649b32bcbd855665a88a76f697f8be6c5017aa0260954ecccd2475bbd5865a5d248eae9a4e6e10d2d51927621d05430381aa37e43b languageName: node linkType: hard -"postcss-selector-parser@npm:^6.0.10, postcss-selector-parser@npm:^6.0.11, postcss-selector-parser@npm:^6.0.9": +"postcss-selector-parser@npm:^6.0.6, postcss-selector-parser@npm:^6.0.9": version: 6.0.11 resolution: "postcss-selector-parser@npm:6.0.11" dependencies: @@ -9333,14 +10684,21 @@ __metadata: languageName: node linkType: hard -"postcss-value-parser@npm:^4.0.0, postcss-value-parser@npm:^4.2.0": +"postcss-value-parser@npm:^3.3.0": + version: 3.3.1 + resolution: "postcss-value-parser@npm:3.3.1" + checksum: 62cd26e1cdbcf2dcc6bcedf3d9b409c9027bc57a367ae20d31dd99da4e206f730689471fd70a2abe866332af83f54dc1fa444c589e2381bf7f8054c46209ce16 + languageName: node + linkType: hard + +"postcss-value-parser@npm:^4.1.0, postcss-value-parser@npm:^4.2.0": version: 4.2.0 resolution: "postcss-value-parser@npm:4.2.0" checksum: 819ffab0c9d51cf0acbabf8996dffbfafbafa57afc0e4c98db88b67f2094cb44488758f06e5da95d7036f19556a4a732525e84289a425f4f6fd8e412a9d7442f languageName: node linkType: hard -"postcss@npm:8.4.21, postcss@npm:^8.0.9, postcss@npm:^8.4.21": +"postcss@npm:8.4.21, postcss@npm:^8.1.6, postcss@npm:^8.1.8, postcss@npm:^8.3.5, postcss@npm:^8.4.21": version: 8.4.21 resolution: "postcss@npm:8.4.21" dependencies: @@ -9407,6 +10765,13 @@ __metadata: languageName: node linkType: hard +"pretty-hrtime@npm:^1.0.3": + version: 1.0.3 + resolution: "pretty-hrtime@npm:1.0.3" + checksum: bae0e6832fe13c3de43d1a3d43df52bf6090499d74dc65a17f5552cb1a94f1f8019a23284ddf988c3c408a09678d743901e1d8f5b7a71bec31eeeac445bef371 + languageName: node + linkType: hard + "process-nextick-args@npm:~2.0.0": version: 2.0.1 resolution: "process-nextick-args@npm:2.0.1" @@ -9488,6 +10853,20 @@ __metadata: languageName: node linkType: hard +"purgecss@npm:^4.0.3": + version: 4.1.3 + resolution: "purgecss@npm:4.1.3" + dependencies: + commander: ^8.0.0 + glob: ^7.1.7 + postcss: ^8.3.5 + postcss-selector-parser: ^6.0.6 + bin: + purgecss: bin/purgecss.js + checksum: 508613f904b130401f2a403d3383533f703c6bcd56e1254c1e8f57818a5337db3a667f66f48355f86271b20dd576691357752f460eb2edd94c095e4178391c5f + languageName: node + linkType: hard + "querystringify@npm:^2.1.1": version: 2.2.0 resolution: "querystringify@npm:2.2.0" @@ -9793,15 +11172,6 @@ __metadata: languageName: node linkType: hard -"read-cache@npm:^1.0.0": - version: 1.0.0 - resolution: "read-cache@npm:1.0.0" - dependencies: - pify: ^2.3.0 - checksum: cffc728b9ede1e0667399903f9ecaf3789888b041c46ca53382fa3a06303e5132774dc0a96d0c16aa702dbac1ea0833d5a868d414f5ab2af1e1438e19e6657c6 - languageName: node - linkType: hard - "readable-stream@npm:^2.0.6": version: 2.3.6 resolution: "readable-stream@npm:2.3.6" @@ -9847,6 +11217,16 @@ __metadata: languageName: node linkType: hard +"reduce-css-calc@npm:^2.1.8": + version: 2.1.8 + resolution: "reduce-css-calc@npm:2.1.8" + dependencies: + css-unit-converter: ^1.1.1 + postcss-value-parser: ^3.3.0 + checksum: 8fd27c06c4b443b84749a69a8b97d10e6ec7d142b625b41923a8807abb22b9e37e44df14e26cc606a802957be07bdce5e8ee2976a6952a7b438a7727007101e9 + languageName: node + linkType: hard + "reflect.ownkeys@npm:^0.2.0": version: 0.2.0 resolution: "reflect.ownkeys@npm:0.2.0" @@ -9854,6 +11234,22 @@ __metadata: languageName: node linkType: hard +"regenerate-unicode-properties@npm:^10.1.0": + version: 10.1.0 + resolution: "regenerate-unicode-properties@npm:10.1.0" + dependencies: + regenerate: ^1.4.2 + checksum: b1a8929588433ab8b9dc1a34cf3665b3b472f79f2af6ceae00d905fc496b332b9af09c6718fb28c730918f19a00dc1d7310adbaa9b72a2ec7ad2f435da8ace17 + languageName: node + linkType: hard + +"regenerate@npm:^1.4.2": + version: 1.4.2 + resolution: "regenerate@npm:1.4.2" + checksum: 3317a09b2f802da8db09aa276e469b57a6c0dd818347e05b8862959c6193408242f150db5de83c12c3fa99091ad95fb42a6db2c3329bfaa12a0ea4cbbeb30cb0 + languageName: node + linkType: hard + "regenerator-runtime@npm:0.13.11, regenerator-runtime@npm:^0.13.11": version: 0.13.11 resolution: "regenerator-runtime@npm:0.13.11" @@ -9875,6 +11271,15 @@ __metadata: languageName: node linkType: hard +"regenerator-transform@npm:^0.15.1": + version: 0.15.1 + resolution: "regenerator-transform@npm:0.15.1" + dependencies: + "@babel/runtime": ^7.8.4 + checksum: 2d15bdeadbbfb1d12c93f5775493d85874dbe1d405bec323da5c61ec6e701bc9eea36167483e1a5e752de9b2df59ab9a2dfff6bf3784f2b28af2279a673d29a4 + languageName: node + linkType: hard + "regexp.prototype.flags@npm:^1.4.3": version: 1.4.3 resolution: "regexp.prototype.flags@npm:1.4.3" @@ -9893,6 +11298,31 @@ __metadata: languageName: node linkType: hard +"regexpu-core@npm:^5.3.1": + version: 5.3.1 + resolution: "regexpu-core@npm:5.3.1" + dependencies: + "@babel/regjsgen": ^0.8.0 + regenerate: ^1.4.2 + regenerate-unicode-properties: ^10.1.0 + regjsparser: ^0.9.1 + unicode-match-property-ecmascript: ^2.0.0 + unicode-match-property-value-ecmascript: ^2.1.0 + checksum: 446fbbb79059afcd64d11ea573276e2df97ee7ad45aa452834d3b2aef7edf7bfe206c310f57f9345d8c95bfedbf9c16a9529f9219a05ae6a6b0d6f0dbe523b33 + languageName: node + linkType: hard + +"regjsparser@npm:^0.9.1": + version: 0.9.1 + resolution: "regjsparser@npm:0.9.1" + dependencies: + jsesc: ~0.5.0 + bin: + regjsparser: bin/parser + checksum: 5e1b76afe8f1d03c3beaf9e0d935dd467589c3625f6d65fb8ffa14f224d783a0fed4bf49c2c1b8211043ef92b6117313419edf055a098ed8342e340586741afc + languageName: node + linkType: hard + "require-directory@npm:^2.1.1": version: 2.1.1 resolution: "require-directory@npm:2.1.1" @@ -9944,7 +11374,7 @@ __metadata: languageName: node linkType: hard -"resolve@npm:^1.1.7, resolve@npm:^1.19.0, resolve@npm:^1.22.1, resolve@npm:~1.22.1": +"resolve@npm:^1.12.0, resolve@npm:^1.14.2, resolve@npm:^1.19.0, resolve@npm:^1.22.1, resolve@npm:~1.22.1": version: 1.22.1 resolution: "resolve@npm:1.22.1" dependencies: @@ -9993,7 +11423,7 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@^1.1.7#~builtin, resolve@patch:resolve@^1.19.0#~builtin, resolve@patch:resolve@^1.22.1#~builtin, resolve@patch:resolve@~1.22.1#~builtin": +"resolve@patch:resolve@^1.12.0#~builtin, resolve@patch:resolve@^1.14.2#~builtin, resolve@patch:resolve@^1.19.0#~builtin, resolve@patch:resolve@^1.22.1#~builtin, resolve@patch:resolve@~1.22.1#~builtin": version: 1.22.1 resolution: "resolve@patch:resolve@npm%3A1.22.1#~builtin::version=1.22.1&hash=c3c19d" dependencies: @@ -10063,7 +11493,21 @@ __metadata: languageName: node linkType: hard -"rimraf@npm:^3.0.2": +"rgb-regex@npm:^1.0.1": + version: 1.0.1 + resolution: "rgb-regex@npm:1.0.1" + checksum: b270ce8bc14782d2d21d3184c1e6c65b465476d8f03e72b93ef57c95710a452b2fe280e1d516c88873aec06efd7f71373e673f114b9d99f3a4f9a0393eb00126 + languageName: node + linkType: hard + +"rgba-regex@npm:^1.0.0": + version: 1.0.0 + resolution: "rgba-regex@npm:1.0.0" + checksum: 7f2cd271572700faea50753d82524cb2b98f17a5b9722965c7076f6cd674fe545f28145b7ef2cccabc9eca2475c793db16862cd5e7b3784a9f4b8d6496431057 + languageName: node + linkType: hard + +"rimraf@npm:^3.0.0, rimraf@npm:^3.0.2": version: 3.0.2 resolution: "rimraf@npm:3.0.2" dependencies: @@ -10193,7 +11637,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^6.3.0": +"semver@npm:^6.1.1, semver@npm:^6.1.2, semver@npm:^6.3.0": version: 6.3.0 resolution: "semver@npm:6.3.0" bin: @@ -10279,6 +11723,15 @@ __metadata: languageName: node linkType: hard +"simple-swizzle@npm:^0.2.2": + version: 0.2.2 + resolution: "simple-swizzle@npm:0.2.2" + dependencies: + is-arrayish: ^0.3.1 + checksum: a7f3f2ab5c76c4472d5c578df892e857323e452d9f392e1b5cf74b74db66e6294a1e1b8b390b519fa1b96b5b613f2a37db6cffef52c3f1f8f3c5ea64eb2d54c0 + languageName: node + linkType: hard + "sisteransi@npm:^1.0.0": version: 1.0.2 resolution: "sisteransi@npm:1.0.2" @@ -10419,6 +11872,13 @@ __metadata: languageName: node linkType: hard +"string-similarity@npm:^4.0.3": + version: 4.0.4 + resolution: "string-similarity@npm:4.0.4" + checksum: 797b41b24e1eb6b3b0ab896950b58c295a19a82933479c75f7b5279ffb63e0b456a8c8d10329c02f607ca1a50370e961e83d552aa468ff3b0fa15809abc9eff7 + languageName: node + linkType: hard + "string-width@npm:^1.0.1": version: 1.0.2 resolution: "string-width@npm:1.0.2" @@ -10739,39 +12199,49 @@ __metadata: languageName: node linkType: hard -"tailwindcss@npm:3.2.6": - version: 3.2.6 - resolution: "tailwindcss@npm:3.2.6" +"tailwindcss@npm:2.2.19, tailwindcss@npm:^2.2.7": + version: 2.2.19 + resolution: "tailwindcss@npm:2.2.19" dependencies: - arg: ^5.0.2 - chokidar: ^3.5.3 - color-name: ^1.1.4 - detective: ^5.2.1 + arg: ^5.0.1 + bytes: ^3.0.0 + chalk: ^4.1.2 + chokidar: ^3.5.2 + color: ^4.0.1 + cosmiconfig: ^7.0.1 + detective: ^5.2.0 didyoumean: ^1.2.2 dlv: ^1.1.3 - fast-glob: ^3.2.12 - glob-parent: ^6.0.2 - is-glob: ^4.0.3 - lilconfig: ^2.0.6 - micromatch: ^4.0.5 + fast-glob: ^3.2.7 + fs-extra: ^10.0.0 + glob-parent: ^6.0.1 + html-tags: ^3.1.0 + is-color-stop: ^1.1.0 + is-glob: ^4.0.1 + lodash: ^4.17.21 + lodash.topath: ^4.5.2 + modern-normalize: ^1.1.0 + node-emoji: ^1.11.0 normalize-path: ^3.0.0 - object-hash: ^3.0.0 - picocolors: ^1.0.0 - postcss: ^8.0.9 - postcss-import: ^14.1.0 - postcss-js: ^4.0.0 - postcss-load-config: ^3.1.4 - postcss-nested: 6.0.0 - postcss-selector-parser: ^6.0.11 - postcss-value-parser: ^4.2.0 + object-hash: ^2.2.0 + postcss-js: ^3.0.3 + postcss-load-config: ^3.1.0 + postcss-nested: 5.0.6 + postcss-selector-parser: ^6.0.6 + postcss-value-parser: ^4.1.0 + pretty-hrtime: ^1.0.3 + purgecss: ^4.0.3 quick-lru: ^5.1.1 - resolve: ^1.22.1 + reduce-css-calc: ^2.1.8 + resolve: ^1.20.0 + tmp: ^0.2.1 peerDependencies: + autoprefixer: ^10.0.2 postcss: ^8.0.9 bin: tailwind: lib/cli.js tailwindcss: lib/cli.js - checksum: 908451ff7b334b2aec2a0ba5bf426a786a3f190b440a1f8ede206d889448ffda3b77349829f06dc297336fd0b5edc696ae5f23b808d6444bb5c689f218e95323 + checksum: 660e8086fa2758f273b7ec87067c041185454374c5c916c236f9691b1c60c48166b2556b6327b3d912f018f48712105fa979b7f717b2db3111ea0850059a2b62 languageName: node linkType: hard @@ -10816,6 +12286,13 @@ __metadata: languageName: node linkType: hard +"timsort@npm:^0.3.0": + version: 0.3.0 + resolution: "timsort@npm:0.3.0" + checksum: 1a66cb897dacabd7dd7c91b7e2301498ca9e224de2edb9e42d19f5b17c4b6dc62a8d4cbc64f28be82aaf1541cb5a78ab49aa818f42a2989ebe049a64af731e2a + languageName: node + linkType: hard + "tiny-emitter@npm:^2.0.0": version: 2.1.0 resolution: "tiny-emitter@npm:2.1.0" @@ -10830,6 +12307,15 @@ __metadata: languageName: node linkType: hard +"tmp@npm:^0.2.1": + version: 0.2.1 + resolution: "tmp@npm:0.2.1" + dependencies: + rimraf: ^3.0.0 + checksum: 8b1214654182575124498c87ca986ac53dc76ff36e8f0e0b67139a8d221eaecfdec108c0e6ec54d76f49f1f72ab9325500b246f562b926f85bcdfca8bf35df9e + languageName: node + linkType: hard + "tmpl@npm:1.0.5": version: 1.0.5 resolution: "tmpl@npm:1.0.5" @@ -10999,19 +12485,26 @@ __metadata: languageName: node linkType: hard -"twin.macro@npm:3.1.0": - version: 3.1.0 - resolution: "twin.macro@npm:3.1.0" +"twin.macro@npm:2.8.2": + version: 2.8.2 + resolution: "twin.macro@npm:2.8.2" dependencies: - "@babel/template": ^7.18.10 - babel-plugin-macros: ^3.1.0 - chalk: 4.1.2 + "@babel/parser": ^7.12.5 + "@babel/template": ^7.14.5 + autoprefixer: ^10.2.5 + babel-plugin-macros: ^2.8.0 + chalk: ^4.1.0 + clean-set: ^1.1.1 + color: ^3.1.3 + dset: ^2.0.1 + lodash.flatmap: ^4.5.0 lodash.get: ^4.4.2 lodash.merge: ^4.6.2 - postcss-selector-parser: ^6.0.10 - peerDependencies: - tailwindcss: ^3.2.4 - checksum: 716695be03456adb023dea016044d56fa7bc84d86654153b410748cdcd15b285471191251f8c2354d930499a42fd4d520336c882c81974f55962939f634f06e0 + postcss: ^8.1.8 + string-similarity: ^4.0.3 + tailwindcss: ^2.2.7 + timsort: ^0.3.0 + checksum: d5af8fbac429e06968b61ce1f7cb2ced53f0d23d88fc680a42e743ddf47263b95db651c804424e7535f765d3ace3e347c2dc564f851f1f495383a83d176e8d05 languageName: node linkType: hard @@ -11118,6 +12611,37 @@ __metadata: languageName: node linkType: hard +"unicode-canonical-property-names-ecmascript@npm:^2.0.0": + version: 2.0.0 + resolution: "unicode-canonical-property-names-ecmascript@npm:2.0.0" + checksum: 39be078afd014c14dcd957a7a46a60061bc37c4508ba146517f85f60361acf4c7539552645ece25de840e17e293baa5556268d091ca6762747fdd0c705001a45 + languageName: node + linkType: hard + +"unicode-match-property-ecmascript@npm:^2.0.0": + version: 2.0.0 + resolution: "unicode-match-property-ecmascript@npm:2.0.0" + dependencies: + unicode-canonical-property-names-ecmascript: ^2.0.0 + unicode-property-aliases-ecmascript: ^2.0.0 + checksum: 1f34a7434a23df4885b5890ac36c5b2161a809887000be560f56ad4b11126d433c0c1c39baf1016bdabed4ec54829a6190ee37aa24919aa116dc1a5a8a62965a + languageName: node + linkType: hard + +"unicode-match-property-value-ecmascript@npm:^2.1.0": + version: 2.1.0 + resolution: "unicode-match-property-value-ecmascript@npm:2.1.0" + checksum: 8d6f5f586b9ce1ed0e84a37df6b42fdba1317a05b5df0c249962bd5da89528771e2d149837cad11aa26bcb84c35355cb9f58a10c3d41fa3b899181ece6c85220 + languageName: node + linkType: hard + +"unicode-property-aliases-ecmascript@npm:^2.0.0": + version: 2.1.0 + resolution: "unicode-property-aliases-ecmascript@npm:2.1.0" + checksum: 243524431893649b62cc674d877bd64ef292d6071dd2fd01ab4d5ad26efbc104ffcd064f93f8a06b7e4ec54c172bf03f6417921a0d8c3a9994161fe1f88f815b + languageName: node + linkType: hard + "unique-filename@npm:^1.1.1": version: 1.1.1 resolution: "unique-filename@npm:1.1.1" @@ -11157,7 +12681,7 @@ __metadata: languageName: node linkType: hard -"update-browserslist-db@npm:^1.0.9": +"update-browserslist-db@npm:^1.0.10, update-browserslist-db@npm:^1.0.9": version: 1.0.10 resolution: "update-browserslist-db@npm:1.0.10" dependencies: @@ -11243,10 +12767,11 @@ __metadata: languageName: node linkType: hard -"vite-plugin-dts@npm:1.7.2": - version: 1.7.2 - resolution: "vite-plugin-dts@npm:1.7.2" +"vite-plugin-dts@npm:2.0.2": + version: 2.0.2 + resolution: "vite-plugin-dts@npm:2.0.2" dependencies: + "@babel/parser": ^7.20.15 "@microsoft/api-extractor": ^7.33.5 "@rollup/pluginutils": ^5.0.2 "@rushstack/node-core-library": ^3.53.2 @@ -11254,10 +12779,11 @@ __metadata: fast-glob: ^3.2.12 fs-extra: ^10.1.0 kolorist: ^1.6.0 + magic-string: ^0.29.0 ts-morph: 17.0.1 peerDependencies: vite: ">=2.9.0" - checksum: 2445cc131481eddcac8fdff7feabf49018cabf745b0f1e955b45c9ce98313e5e7d9e9bb838af78577866404ed9070d915b4b6bc80c9f6eb91ec95608ce456f2d + checksum: d8bf9a8066d6db088f967379139bc54f78aef495a7827f0680a5c261e9b62b17cfc7b4f8cc4ad94cf3ef08b1aa4740040d471d6557641fbab2174ce4ad059430 languageName: node linkType: hard @@ -11531,7 +13057,7 @@ __metadata: languageName: node linkType: hard -"yaml@npm:^1.10.0, yaml@npm:^1.10.2": +"yaml@npm:^1.10.0, yaml@npm:^1.10.2, yaml@npm:^1.7.2": version: 1.10.2 resolution: "yaml@npm:1.10.2" checksum: ce4ada136e8a78a0b08dc10b4b900936912d15de59905b2bf415b4d33c63df1d555d23acb2a41b23cf9fb5da41c256441afca3d6509de7247daa062fd2c5ea5f diff --git a/settings.gradle b/settings.gradle index 5732248f08b..ff17baed98e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -32,6 +32,7 @@ include 'server:sonar-main' include 'server:sonar-process' include 'server:sonar-server-common' include 'server:sonar-web' +include 'server:sonar-web:design-system' include 'server:sonar-webserver' include 'server:sonar-webserver-api' include 'server:sonar-webserver-auth' diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 15bf247b56c..f8259c2f968 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -509,6 +509,13 @@ event.definition_change.branch_added={project} {branch} added event.definition_change.branch_removed={project} {branch} removed event.definition_change.branch_replaced={project} {oldBranch} replaced with {newBranch} +#------------------------------------------------------------------------------ +# +# GLOBAL NAVIGATION +# +#------------------------------------------------------------------------------ + +global_nav.account.tooltip=Account #------------------------------------------------------------------------------ # @@ -1413,6 +1420,7 @@ search.search_for_files=Search for files... search.search_for_modules=Search for modules... search.search_for_metrics=Search for metrics... +global_search.shortcut_hint=Hint: Press 'S' from anywhere to open this search bar. #------------------------------------------------------------------------------ # -- 2.39.5