aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web
diff options
context:
space:
mode:
authorJeremy Davis <jeremy.davis@sonarsource.com>2023-02-22 16:18:48 +0100
committersonartech <sonartech@sonarsource.com>2023-03-13 20:02:44 +0000
commitb33a7cd2193a47f90b22568dd0d58f404bc5f6d7 (patch)
tree6ad602cc5b4172f048a17ef33ed0267c8d96aed5 /server/sonar-web
parent8d902e9e2484b35b7a9fe6e8ed49e68ad3ff6ab5 (diff)
downloadsonarqube-b33a7cd2193a47f90b22568dd0d58f404bc5f6d7.tar.gz
sonarqube-b33a7cd2193a47f90b22568dd0d58f404bc5f6d7.zip
SONAR-18524 New Main App bar
Diffstat (limited to 'server/sonar-web')
-rw-r--r--server/sonar-web/build.gradle2
-rw-r--r--server/sonar-web/config/jest/SetupTheme.js26
-rw-r--r--server/sonar-web/design-system/babel.config.js3
-rw-r--r--server/sonar-web/design-system/build.gradle40
-rw-r--r--server/sonar-web/design-system/config/jest/SetupReactTestingLibrary.ts25
-rw-r--r--server/sonar-web/design-system/config/jest/SetupTestEnvironment.js50
-rw-r--r--server/sonar-web/design-system/config/jest/SetupTheme.js26
-rw-r--r--server/sonar-web/design-system/jest.config.js61
-rw-r--r--server/sonar-web/design-system/package.json49
-rw-r--r--server/sonar-web/design-system/src/@types/css.d.ts27
-rw-r--r--server/sonar-web/design-system/src/@types/emotion.d.ts25
-rw-r--r--server/sonar-web/design-system/src/components/Avatar.tsx117
-rw-r--r--server/sonar-web/design-system/src/components/Checkbox.tsx174
-rw-r--r--server/sonar-web/design-system/src/components/ClickEventBoundary.tsx35
-rw-r--r--server/sonar-web/design-system/src/components/DeferredSpinner.tsx138
-rw-r--r--server/sonar-web/design-system/src/components/Dropdown.tsx140
-rw-r--r--server/sonar-web/design-system/src/components/DropdownMenu.tsx370
-rw-r--r--server/sonar-web/design-system/src/components/DropdownToggler.tsx48
-rw-r--r--server/sonar-web/design-system/src/components/EscKeydownHandler.tsx48
-rw-r--r--server/sonar-web/design-system/src/components/GenericAvatar.tsx60
-rw-r--r--server/sonar-web/design-system/src/components/InputSearch.tsx243
-rw-r--r--server/sonar-web/design-system/src/components/InteractiveIcon.tsx182
-rw-r--r--server/sonar-web/design-system/src/components/Link.tsx173
-rw-r--r--server/sonar-web/design-system/src/components/MainAppBar.tsx89
-rw-r--r--server/sonar-web/design-system/src/components/MainMenu.tsx30
-rw-r--r--server/sonar-web/design-system/src/components/MainMenuItem.tsx59
-rw-r--r--server/sonar-web/design-system/src/components/NavLink.tsx74
-rw-r--r--server/sonar-web/design-system/src/components/OutsideClickHandler.tsx68
-rw-r--r--server/sonar-web/design-system/src/components/RadioButton.tsx125
-rw-r--r--server/sonar-web/design-system/src/components/SonarQubeLogo.tsx50
-rw-r--r--server/sonar-web/design-system/src/components/Text.tsx62
-rw-r--r--server/sonar-web/design-system/src/components/Tooltip.tsx504
-rw-r--r--server/sonar-web/design-system/src/components/__tests__/Avatar-test.tsx69
-rw-r--r--server/sonar-web/design-system/src/components/__tests__/DeferredSpinner-test.tsx73
-rw-r--r--server/sonar-web/design-system/src/components/__tests__/Dropdown-test.tsx65
-rw-r--r--server/sonar-web/design-system/src/components/__tests__/DropdownMenu-test.tsx100
-rw-r--r--server/sonar-web/design-system/src/components/__tests__/GenericAvatar-test.tsx51
-rw-r--r--server/sonar-web/design-system/src/components/__tests__/InputSearch-test.tsx90
-rw-r--r--server/sonar-web/design-system/src/components/__tests__/Link-test.tsx129
-rw-r--r--server/sonar-web/design-system/src/components/__tests__/MainAppBar-test.tsx54
-rw-r--r--server/sonar-web/design-system/src/components/__tests__/MainMenuItem-test.tsx64
-rw-r--r--server/sonar-web/design-system/src/components/__tests__/NavLink-test.tsx112
-rw-r--r--server/sonar-web/design-system/src/components/__tests__/Text-test.tsx41
-rw-r--r--server/sonar-web/design-system/src/components/__tests__/Tooltip-test.tsx126
-rw-r--r--server/sonar-web/design-system/src/components/__tests__/clipboard-test.tsx69
-rw-r--r--server/sonar-web/design-system/src/components/buttons.tsx219
-rw-r--r--server/sonar-web/design-system/src/components/clipboard.tsx170
-rw-r--r--server/sonar-web/design-system/src/components/icons/CheckIcon.tsx36
-rw-r--r--server/sonar-web/design-system/src/components/icons/ClockIcon.tsx23
-rw-r--r--server/sonar-web/design-system/src/components/icons/CloseIcon.tsx23
-rw-r--r--server/sonar-web/design-system/src/components/icons/CopyIcon.tsx23
-rw-r--r--server/sonar-web/design-system/src/components/icons/Icon.tsx86
-rw-r--r--server/sonar-web/design-system/src/components/icons/MenuHelpIcon.tsx36
-rw-r--r--server/sonar-web/design-system/src/components/icons/MenuIcon.tsx29
-rw-r--r--server/sonar-web/design-system/src/components/icons/MenuSearchIcon.tsx37
-rw-r--r--server/sonar-web/design-system/src/components/icons/OpenNewTabIcon.tsx23
-rw-r--r--server/sonar-web/design-system/src/components/icons/SearchIcon.tsx23
-rw-r--r--server/sonar-web/design-system/src/components/icons/StarIcon.tsx23
-rw-r--r--server/sonar-web/design-system/src/components/icons/__tests__/Icon-test.tsx54
-rw-r--r--server/sonar-web/design-system/src/components/icons/index.ts24
-rw-r--r--server/sonar-web/design-system/src/components/index.ts19
-rw-r--r--server/sonar-web/design-system/src/components/popups.tsx256
-rw-r--r--server/sonar-web/design-system/src/helpers/__tests__/colors-test.ts61
-rw-r--r--server/sonar-web/design-system/src/helpers/__tests__/positioning-test.ts167
-rw-r--r--server/sonar-web/design-system/src/helpers/__tests__/theme-test.ts148
-rw-r--r--server/sonar-web/design-system/src/helpers/colors.ts56
-rw-r--r--server/sonar-web/design-system/src/helpers/constants.ts68
-rw-r--r--server/sonar-web/design-system/src/helpers/index.ts (renamed from server/sonar-web/design-system/src/components/DummyComponent.tsx)6
-rw-r--r--server/sonar-web/design-system/src/helpers/keyboard.ts52
-rw-r--r--server/sonar-web/design-system/src/helpers/l10n.ts30
-rw-r--r--server/sonar-web/design-system/src/helpers/positioning.ts185
-rw-r--r--server/sonar-web/design-system/src/helpers/testUtils.tsx117
-rw-r--r--server/sonar-web/design-system/src/helpers/theme.ts130
-rw-r--r--server/sonar-web/design-system/src/helpers/types.ts22
-rw-r--r--server/sonar-web/design-system/src/index.ts23
-rw-r--r--server/sonar-web/design-system/src/theme/colors.ts136
-rw-r--r--server/sonar-web/design-system/src/theme/index.ts20
-rw-r--r--server/sonar-web/design-system/src/theme/light.ts743
-rw-r--r--server/sonar-web/design-system/src/types/misc.ts21
-rw-r--r--server/sonar-web/design-system/src/types/theme.ts45
-rw-r--r--server/sonar-web/design-system/tsconfig.json8
-rw-r--r--server/sonar-web/design-system/vite.config.js4
-rw-r--r--server/sonar-web/jest.config.js8
-rw-r--r--server/sonar-web/package.json3
-rw-r--r--server/sonar-web/src/main/js/app/components/GlobalContainer.tsx60
-rw-r--r--server/sonar-web/src/main/js/app/components/SimpleContainer.tsx5
-rw-r--r--server/sonar-web/src/main/js/app/components/global-search/GlobalSearch.tsx (renamed from server/sonar-web/src/main/js/app/components/search/Search.tsx)169
-rw-r--r--server/sonar-web/src/main/js/app/components/global-search/GlobalSearchResult.tsx65
-rw-r--r--server/sonar-web/src/main/js/app/components/global-search/GlobalSearchResults.tsx (renamed from server/sonar-web/src/main/js/app/components/search/SearchResults.tsx)33
-rw-r--r--server/sonar-web/src/main/js/app/components/global-search/GlobalSearchShowMore.tsx (renamed from server/sonar-web/src/main/js/app/components/search/SearchShowMore.tsx)45
-rw-r--r--server/sonar-web/src/main/js/app/components/global-search/__tests__/GlobalSearch-it.tsx214
-rw-r--r--server/sonar-web/src/main/js/app/components/global-search/utils.ts (renamed from server/sonar-web/src/main/js/app/components/search/utils.ts)0
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.css125
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx30
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx79
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMore.tsx69
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx132
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUserMenu.tsx66
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/MainSonarQubeBar.tsx42
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNav-test.tsx54
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavBranding-test.tsx50
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.tsx12
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx20
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNav-test.tsx.snap95
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavBranding-test.tsx.snap46
-rw-r--r--server/sonar-web/src/main/js/app/components/search/Search.css126
-rw-r--r--server/sonar-web/src/main/js/app/components/search/SearchResult.tsx80
-rw-r--r--server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.tsx175
-rw-r--r--server/sonar-web/src/main/js/app/components/search/__tests__/SearchResult-test.tsx76
-rw-r--r--server/sonar-web/src/main/js/app/components/search/__tests__/SearchResults-test.tsx83
-rw-r--r--server/sonar-web/src/main/js/app/components/search/__tests__/SearchShowMore-test.tsx61
-rw-r--r--server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/Search-test.tsx.snap28
-rw-r--r--server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.tsx.snap242
-rw-r--r--server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResults-test.tsx.snap86
-rw-r--r--server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchShowMore-test.tsx.snap41
-rw-r--r--server/sonar-web/src/main/js/app/utils/startReactApp.tsx106
-rw-r--r--server/sonar-web/src/main/js/components/embed-docs-modal/DocItemLink.tsx44
-rw-r--r--server/sonar-web/src/main/js/components/embed-docs-modal/EmbedDocsPopup.tsx204
-rw-r--r--server/sonar-web/src/main/js/components/embed-docs-modal/EmbedDocsPopupHelper.tsx90
-rw-r--r--server/sonar-web/src/main/js/components/embed-docs-modal/__tests__/EmbedDocsPopup-test.tsx79
-rw-r--r--server/sonar-web/src/main/js/components/embed-docs-modal/__tests__/SuggestionsProvider-test.tsx53
-rw-r--r--server/sonar-web/tailwind-utilities.js87
-rw-r--r--server/sonar-web/tailwind.base.config.js195
-rw-r--r--server/sonar-web/tailwind.config.js6
-rw-r--r--server/sonar-web/yarn.lock1822
125 files changed, 10291 insertions, 2127 deletions
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: '<rootDir>/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)$':
+ '<rootDir>/config/jest/FileStub.js',
+ // '^.+\\.css$': '<rootDir>/config/jest/CSSStub.js',
+ },
+ setupFiles: [
+ '<rootDir>/config/jest/SetupTestEnvironment.js',
+ '<rootDir>/config/jest/SetupTheme.js',
+ ],
+ setupFilesAfterEnv: ['<rootDir>/config/jest/SetupReactTestingLibrary.ts'],
+ snapshotSerializers: ['@emotion/jest/serializer'],
+ testEnvironment: 'jsdom',
+ testPathIgnorePatterns: ['<rootDir>/config/jest', '<rootDir>/node_modules', '<rootDir>/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<Size, number> = {
+ 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<HTMLImageElement> = () => {
+ setImgError(true);
+ };
+
+ if (!imgError) {
+ if (enableGravatar && gravatarServerUrl && hash) {
+ const url = gravatarServerUrl
+ .replace('{EMAIL_MD5}', hash)
+ .replace('{SIZE}', String(numberSize * 2));
+
+ return (
+ <StyledAvatar
+ alt={resolvedName}
+ border={border}
+ className={className}
+ height={numberSize}
+ onError={handleImgError}
+ role="img"
+ src={url}
+ width={numberSize}
+ />
+ );
+ }
+
+ if (resolvedName && organizationAvatar) {
+ return (
+ <StyledAvatar
+ alt={resolvedName}
+ border={border}
+ className={className}
+ height={numberSize}
+ onError={handleImgError}
+ role="img"
+ src={organizationAvatar}
+ width={numberSize}
+ />
+ );
+ }
+ }
+
+ if (!resolvedName) {
+ return <input className="sw-appearance-none" />;
+ }
+
+ return <GenericAvatar className={className} name={resolvedName} size={numberSize} />;
+}
+
+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<HTMLInputElement>) => 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 (
+ <CheckboxContainer className={className} disabled={disabled}>
+ {right && children}
+ <AccessibleCheckbox
+ aria-label={title}
+ checked={checked}
+ disabled={disabled || loading}
+ id={id}
+ onChange={handleChange}
+ onClick={onClick}
+ onFocus={onFocus}
+ type="checkbox"
+ />
+ <DeferredSpinner loading={loading}>
+ <StyledCheckbox aria-hidden={true} data-clickable="true" title={title}>
+ <CheckboxIcon checked={checked} thirdState={thirdState} />
+ </StyledCheckbox>
+ </DeferredSpinner>
+ {!right && children}
+ </CheckboxContainer>
+ );
+}
+
+interface CheckIconProps {
+ checked?: boolean;
+ thirdState?: boolean;
+}
+
+function CheckboxIcon({ checked, thirdState }: CheckIconProps) {
+ if (checked && thirdState) {
+ return (
+ <CustomIcon>
+ <rect fill="currentColor" height="2" rx="1" width="50%" x="4" y="7" />
+ </CustomIcon>
+ );
+ } else if (checked) {
+ return <CheckIcon fill="currentColor" />;
+ }
+ 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<MouseEvent>) => {
+ 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<Props, State> {
+ 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 <Spinner className={className} role="status" />;
+ }
+ if (children) {
+ return children;
+ }
+ if (placeholder) {
+ return <Placeholder className={className} />;
+ }
+ 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<HTMLElement>) => void;
+type A11yAttrs = Pick<React.AriaAttributes, 'aria-controls' | 'aria-expanded' | 'aria-haspopup'> & {
+ 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<Props, State> {
+ 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 (
+ <DropdownToggler
+ allowResizing={allowResizing}
+ aria-labelledby={`${id}-trigger`}
+ className={className}
+ id={`${id}-dropdown`}
+ onRequestClose={this.handleClose}
+ open={open}
+ overlay={
+ <DropdownMenu onClick={closeOnClick ? this.handleClose : undefined} size={size}>
+ {this.props.overlay}
+ </DropdownMenu>
+ }
+ placement={this.props.placement}
+ zLevel={zLevel}
+ >
+ {children}
+ </DropdownToggler>
+ );
+ }
+}
+
+interface ActionsDropdownProps extends Omit<Props, 'children' | 'overlay'> {
+ buttonSize?: 'small' | 'medium';
+ children: React.ReactNode;
+}
+
+export function ActionsDropdown(props: ActionsDropdownProps) {
+ const { children, buttonSize, ...dropdownProps } = props;
+ return (
+ <Dropdown overlay={children} {...dropdownProps}>
+ <InteractiveIcon
+ Icon={MenuIcon}
+ aria-label={translate('menu')}
+ size={buttonSize}
+ stopPropagation={false}
+ />
+ </Dropdown>
+ );
+}
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<HTMLMenuElement> {
+ children?: React.ReactNode;
+ className?: string;
+ innerRef?: React.Ref<HTMLUListElement>;
+ maxHeight?: string;
+ size?: InputSizeKeys;
+}
+
+export function DropdownMenu({
+ children,
+ className,
+ innerRef,
+ maxHeight = 'inherit',
+ size = 'small',
+ ...menuProps
+}: Props) {
+ return (
+ <DropdownMenuWrapper
+ className={classNames('dropdown-menu', className)}
+ ref={innerRef}
+ role="menu"
+ style={{ '--inputSize': INPUT_SIZES[size], maxHeight }}
+ {...menuProps}
+ >
+ {children}
+ </DropdownMenuWrapper>
+ );
+}
+
+interface ListItemProps {
+ children?: React.ReactNode;
+ className?: string;
+ innerRef?: React.Ref<HTMLLIElement>;
+ onFocus?: VoidFunction;
+ onPointerEnter?: VoidFunction;
+ onPointerLeave?: VoidFunction;
+}
+
+type ItemLinkProps = Omit<ListItemProps, 'innerRef'> &
+ Pick<LinkProps, 'disabled' | 'icon' | 'onClick' | 'to'> & {
+ innerRef?: React.Ref<HTMLAnchorElement>;
+ };
+
+export function ItemLink(props: ItemLinkProps) {
+ const { children, className, disabled, icon, onClick, innerRef, to, ...liProps } = props;
+ return (
+ <li {...liProps}>
+ <ItemLinkStyled
+ className={classNames(className, { disabled })}
+ disabled={disabled}
+ icon={icon}
+ onClick={onClick}
+ ref={innerRef}
+ role="menuitem"
+ showExternalIcon={false}
+ to={to}
+ >
+ {children}
+ </ItemLinkStyled>
+ </li>
+ );
+}
+
+interface ItemNavLinkProps extends ItemLinkProps {
+ end?: boolean;
+}
+
+export function ItemNavLink(props: ItemNavLinkProps) {
+ const { children, className, disabled, end, icon, onClick, innerRef, to, ...liProps } = props;
+ return (
+ <li {...liProps}>
+ <ItemNavLinkStyled
+ className={classNames(className, { disabled })}
+ disabled={disabled}
+ end={end}
+ onClick={onClick}
+ ref={innerRef}
+ role="menuitem"
+ to={to}
+ >
+ {icon}
+ {children}
+ </ItemNavLinkStyled>
+ </li>
+ );
+}
+
+interface ItemButtonProps extends ListItemProps {
+ disabled?: boolean;
+ icon?: React.ReactNode;
+ onClick: React.MouseEventHandler<HTMLButtonElement>;
+}
+
+export function ItemButton(props: ItemButtonProps) {
+ const { children, className, disabled, icon, innerRef, onClick, ...liProps } = props;
+ return (
+ <li ref={innerRef} role="none" {...liProps}>
+ <ItemButtonStyled className={className} disabled={disabled} onClick={onClick} role="menuitem">
+ {icon}
+ {children}
+ </ItemButtonStyled>
+ </li>
+ );
+}
+
+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 (
+ <li ref={innerRef} role="none" {...liProps}>
+ <ItemCheckboxStyled
+ checked={checked}
+ className={classNames(className, { disabled })}
+ disabled={disabled}
+ id={id}
+ onCheck={onCheck}
+ onFocus={onFocus}
+ >
+ {children}
+ </ItemCheckboxStyled>
+ </li>
+ );
+}
+
+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 (
+ <li ref={innerRef} role="none" {...liProps}>
+ <ItemRadioButtonStyled
+ checked={checked}
+ className={classNames(className, { disabled })}
+ disabled={disabled}
+ onCheck={onCheck}
+ value={value}
+ >
+ {children}
+ </ItemRadioButtonStyled>
+ </li>
+ );
+}
+
+interface ItemCopyProps {
+ children?: React.ReactNode;
+ className?: string;
+ copyValue: string;
+}
+
+export function ItemCopy(props: ItemCopyProps) {
+ const { children, className, copyValue } = props;
+ return (
+ <ClipboardBase>
+ {({ setCopyButton, copySuccess }) => (
+ <Tooltip overlay={translate('copied_action')} visible={copySuccess}>
+ <li role="none">
+ <ItemButtonStyled
+ className={className}
+ data-clipboard-text={copyValue}
+ ref={setCopyButton}
+ role="menuitem"
+ >
+ {children}
+ </ItemButtonStyled>
+ </li>
+ </Tooltip>
+ )}
+ </ClipboardBase>
+ );
+}
+
+interface ItemDownloadProps extends ListItemProps {
+ download: string;
+ href: string;
+}
+
+export function ItemDownload(props: ItemDownloadProps) {
+ const { children, className, download, href, innerRef, ...liProps } = props;
+ return (
+ <li ref={innerRef} role="none" {...liProps}>
+ <ItemDownloadStyled
+ className={className}
+ download={download}
+ href={href}
+ rel="noopener noreferrer"
+ role="menuitem"
+ target="_blank"
+ >
+ {children}
+ </ItemDownloadStyled>
+ </li>
+ );
+}
+
+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 (
+ <PortalPopup
+ overlay={
+ open ? (
+ <OutsideClickHandler onClickOutside={onRequestClose}>
+ <EscKeydownHandler onKeydown={onRequestClose}>{overlay}</EscKeydownHandler>
+ </OutsideClickHandler>
+ ) : undefined
+ }
+ {...popupProps}
+ >
+ {children}
+ </PortalPopup>
+ );
+}
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<Props> {
+ 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<IconProps>;
+ 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 (
+ <StyledGenericAvatar aria-label={name} className={className} name={name} role="img" size={size}>
+ {Icon ? <Icon fill={themeAvatarColor(name, true)({ theme })} /> : text}
+ </StyledGenericAvatar>
+ );
+}
+
+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<HTMLInputElement>;
+ loading?: boolean;
+ maxLength?: number;
+ minLength?: number;
+ onBlur?: React.FocusEventHandler<HTMLInputElement>;
+ onChange: (value: string) => void;
+ onFocus?: React.FocusEventHandler<HTMLInputElement>;
+ onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>;
+ onMouseDown?: React.MouseEventHandler<HTMLInputElement>;
+ 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 | HTMLElement>(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<HTMLInputElement>) => {
+ const eventValue = event.currentTarget.value;
+ setValue(eventValue);
+ changeValue(eventValue);
+ };
+
+ const handleInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
+ 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 (
+ <InputSearchWrapper
+ className={className}
+ id={id}
+ onMouseDown={onMouseDown}
+ style={{ '--inputSize': INPUT_SIZES[size] }}
+ title={tooShort && isDefined(minLength) ? tooShortText : ''}
+ >
+ <StyledInputWrapper className="sw-flex sw-items-center">
+ <input
+ aria-label={searchInputAriaLabel}
+ autoComplete="off"
+ autoFocus={autoFocus}
+ className={inputClassName}
+ maxLength={maxLength}
+ onBlur={onBlur}
+ onChange={handleInputChange}
+ onFocus={onFocus}
+ onKeyDown={handleInputKeyDown}
+ placeholder={placeholder}
+ ref={ref}
+ role="searchbox"
+ type="search"
+ value={value}
+ />
+ <DeferredSpinner loading={loading !== undefined ? loading : false}>
+ <StyledSearchIcon />
+ </DeferredSpinner>
+ {value && (
+ <StyledInteractiveIcon
+ Icon={CloseIcon}
+ aria-label={clearIconAriaLabel}
+ className="js-input-search-clear"
+ onClick={handleClearClick}
+ size="small"
+ />
+ )}
+
+ {tooShort && isDefined(minLength) && (
+ <StyledNote className="sw-ml-1" role="note">
+ {tooShortText}
+ </StyledNote>
+ )}
+ </StyledInputWrapper>
+ </InputSearchWrapper>
+ );
+}
+
+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<IconProps>;
+ 'aria-label': string;
+ children?: React.ReactNode;
+ className?: string;
+ currentColor?: boolean;
+ disabled?: boolean;
+ id?: string;
+ innerRef?: React.Ref<HTMLButtonElement>;
+ onClick?: VoidFunction;
+ size?: InteractiveIconSize;
+ stopPropagation?: boolean;
+ to?: LinkProps['to'];
+}
+
+export class InteractiveIconBase extends React.PureComponent<InteractiveIconProps> {
+ handleClick = (event: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => {
+ 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 (
+ <IconLink
+ {...props}
+ onClick={onClick}
+ showExternalIcon={false}
+ stopPropagation={true}
+ to={to}
+ >
+ <Icon className={classNames({ 'sw-mr-1': isDefined(children) })} />
+ {children}
+ </IconLink>
+ );
+ }
+
+ return (
+ <IconButton {...props} onClick={this.handleClick} ref={innerRef}>
+ <Icon className={classNames({ 'sw-mr-1': isDefined(children) })} />
+ {children}
+ </IconButton>
+ );
+ }
+}
+
+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<InteractiveIconProps> = 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<InteractiveIconProps> = styled(InteractiveIcon)`
+ --color: ${themeColor('discreetInteractiveIcon')};
+`;
+
+export const DestructiveIcon: React.FC<InteractiveIconProps> = styled(InteractiveIconBase)`
+ --background: ${themeColor('destructiveIcon')};
+ --backgroundHover: ${themeColor('destructiveIconHover')};
+ --color: ${themeContrast('destructiveIcon')};
+ --colorHover: ${themeContrast('destructiveIconHover')};
+ --focus: ${themeColor('destructiveIconFocus', 0.2)};
+`;
+
+export const DismissProductNewsIcon: React.FC<InteractiveIconProps> = 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<HTMLAnchorElement>) => void;
+ preventDefault?: boolean;
+ showExternalIcon?: boolean;
+ stopPropagation?: boolean;
+ target?: HTMLAttributeAnchorTarget;
+}
+
+function BaseLinkWithRef(props: LinkProps, ref: React.ForwardedRef<HTMLAnchorElement>) {
+ 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<HTMLAnchorElement>) => {
+ 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 ? (
+ <a
+ {...rest}
+ href={to}
+ onClick={handleClick}
+ ref={ref}
+ rel="noopener noreferrer"
+ target={target}
+ >
+ {icon}
+ {children}
+ {showExternalIcon && <OpenNewTabIcon className="sw-ml-1" />}
+ </a>
+ ) : (
+ <RouterLink ref={ref} {...rest} onClick={handleClick} to={to}>
+ {icon}
+ {children}
+ </RouterLink>
+ );
+}
+
+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 (
+ <MainAppBarContainerDiv>
+ <MainAppBarDiv>
+ <MainAppBarNavLogoDiv>
+ <MainAppBarNavLogoLink href="/">
+ <Logo />
+ </MainAppBarNavLogoLink>
+ </MainAppBarNavLogoDiv>
+ <MainAppBarNavRightDiv>{children}</MainAppBarNavRightDiv>
+ </MainAppBarDiv>
+ </MainAppBarContainerDiv>
+ );
+}
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 <MainMenuUl>{children}</MainMenuUl>;
+}
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<HTMLAnchorElement>) => 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<HTMLAnchorElement>) {
+ const {
+ blurAfterClick,
+ children,
+ disabled,
+ onClick,
+ preventDefault,
+ stopPropagation,
+ ...otherProps
+ } = props;
+
+ const handleClick = React.useCallback(
+ (event: React.MouseEvent<HTMLAnchorElement>) => {
+ 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 (
+ <RouterNavLink onClick={handleClick} ref={ref} {...otherProps}>
+ {children}
+ </RouterNavLink>
+ );
+}
+
+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<Props> {
+ 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<HTMLInputElement>,
+ '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 (
+ <label className={classNames('sw-flex sw-items-center', className)}>
+ <RadioButtonStyled
+ aria-disabled={disabled}
+ checked={checked}
+ disabled={disabled}
+ onChange={handleChange}
+ type="radio"
+ value={value}
+ {...htmlProps}
+ />
+ {children}
+ </label>
+ );
+}
+
+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 (
+ <SonarQubeLogoSvg viewBox="0 0 540.33 156.33" xmlns="http://www.w3.org/2000/svg">
+ <path
+ d="M11.89 101.92a29.92 29.92 0 0 0 13.23 3.74c4.65 0 6.57-1.62 6.57-4.14s-1.51-3.74-7.27-5.66c-10.21-3.44-14.15-9-14-14.85 0-9.2 7.89-16.17 20.11-16.17a33.07 33.07 0 0 1 13.95 2.83l-2.78 10.6A24.24 24.24 0 0 0 31 75.44c-3.74 0-5.87 1.51-5.87 4 0 2.33 1.93 3.54 8 5.66 9.4 3.23 13.34 8 13.44 15.26 0 9.19-7.27 16-21.42 16-6.47 0-12.22-1.42-16-3.44zM100.63 90.09c0 18.09-12.83 26.38-26.08 26.38C60.11 116.48 49 107 49 91s10.5-26.17 26.37-26.17c15.16 0 25.26 10.41 25.26 25.26zm-35.78.51c0 8.49 3.54 14.85 10.11 14.85 6 0 9.8-6 9.8-14.85 0-7.38-2.83-14.87-9.8-14.87-7.37.01-10.11 7.59-10.11 14.87zM106.11 81.71c0-6.16-.2-11.42-.41-15.76H119l.7 6.76h.31a18.08 18.08 0 0 1 15.25-7.88c10.11 0 17.69 6.66 17.69 21.22v29.31h-15.31V88c0-6.37-2.22-10.71-7.78-10.71a8.18 8.18 0 0 0-7.78 5.71 10.41 10.41 0 0 0-.61 3.84v28.51h-15.36zM189.39 115.36l-.91-5h-.3c-3.23 3.95-8.3 6.07-14.15 6.07-10 0-16-7.29-16-15.16 0-12.83 11.52-19 29-18.91v-.7c0-2.63-1.42-6.37-9-6.37a27.8 27.8 0 0 0-13.64 3.73l-2.84-9.9c3.44-1.93 10.21-4.35 19.2-4.35 16.48 0 21.73 9.7 21.73 21.32v17.18a75.92 75.92 0 0 0 .71 12zM187.58 92c-8.08-.1-14.35 1.83-14.35 7.78 0 3.95 2.63 5.87 6.07 5.87a8.39 8.39 0 0 0 8-5.66 10.87 10.87 0 0 0 .31-2.63zM210.63 82.21c0-7.27-.2-12-.41-16.26h13.24L224 75h.4c2.53-7.17 8.59-10.2 13.34-10.2a16.56 16.56 0 0 1 3.26.2v14.48a21.82 21.82 0 0 0-4.14-.41c-5.66 0-9.5 3-10.52 7.78a18.94 18.94 0 0 0-.3 3.44v25.07h-15.41zM342.35 102c0 5 .1 9.5.41 13.34h-7.89l-.51-8h-.19a18.43 18.43 0 0 1-16.17 9.1c-7.68 0-16.89-4.24-16.89-21.42V66.44H310v27.09c0 9.29 2.83 15.57 10.92 15.57a12.88 12.88 0 0 0 11.72-8.1 13.15 13.15 0 0 0 .81-4.55v-30h8.9zM352.67 115.36c.2-3.34.4-8.3.4-12.64V43.6h8.79v30.73h.2c3.13-5.46 8.79-9 16.68-9 12.12 0 20.71 10.11 20.61 25 0 17.49-11 26.18-21.92 26.18-7.08 0-12.73-2.73-16.37-9.2h-.31l-.4 8.09zm9.19-19.61a16.48 16.48 0 0 0 .41 3.23 13.71 13.71 0 0 0 13.33 10.41c9.31 0 14.85-7.58 14.85-18.79 0-9.8-5-18.19-14.55-18.19a14.17 14.17 0 0 0-13.54 10.91 17.47 17.47 0 0 0-.51 3.64zM411.5 92.52c.19 12 7.88 17 16.77 17a32.24 32.24 0 0 0 13.54-2.52l1.52 6.37c-3.13 1.41-8.49 3-16.27 3-15.06 0-24.06-9.9-24.06-24.65s8.69-26.38 22.94-26.38c16 0 20.21 14 20.21 23a33.67 33.67 0 0 1-.3 4.14zm26.07-6.37c.1-5.66-2.31-14.46-12.32-14.46-9 0-12.94 8.3-13.65 14.46z"
+ fill="#1b171b"
+ />
+ <path
+ d="M290.55 75.25a26.41 26.41 0 1 0-11.31 39.07l10.22 16.6 8.11-5.51-10.22-16.6a26.42 26.42 0 0 0 3.2-33.56M279.1 105.4a18.5 18.5 0 1 1 4.9-25.7 18.52 18.52 0 0 1-4.9 25.7"
+ fill="#1b171b"
+ fillRule="evenodd"
+ />
+ <path
+ d="M506.94 115.57h-6.27c0-50.44-41.62-91.48-92.78-91.48v-6.26c54.62 0 99.05 43.84 99.05 97.74z"
+ fill="#4e9bcd"
+ />
+ <path
+ d="M511.27 81.93c-7.52-31.65-33.16-58.06-65.27-67.29l1.44-5c33.93 9.74 61 37.65 68.95 71.1zM516.09 52.23a96 96 0 0 0-37.17-41.49l2.17-3.57a100.24 100.24 0 0 1 38.8 43.31z"
+ fill="#4e9bcd"
+ />
+ </SonarQubeLogoSvg>
+ );
+}
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 ? (
+ <StyledText
+ // Safe: comes from the search engine, that injects bold tags into component names
+ // eslint-disable-next-line react/no-danger
+ dangerouslySetInnerHTML={{ __html: match }}
+ />
+ ) : (
+ <StyledText title={name}>{name}</StyledText>
+ );
+}
+
+export function TextMuted({ text }: { text: string }) {
+ return <StyledMutedText title={text}>{text}</StyledMutedText>;
+}
+
+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<Measurements>;
+
+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 ? <TooltipInner {...props}>{props.children}</TooltipInner> : props.children;
+}
+
+export class TooltipInner extends React.Component<TooltipProps, State> {
+ 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() && (
+ <TooltipPortal>
+ <TooltipWrapper
+ className={classNames(placement)}
+ onPointerEnter={this.handleOverlayPointerEnter}
+ onPointerLeave={this.handleOverlayPointerLeave}
+ ref={this.tooltipNodeRef}
+ role="tooltip"
+ style={style}
+ >
+ <TooltipWrapperInner>{this.props.overlay}</TooltipWrapperInner>
+ <TooltipWrapperArrow
+ style={
+ isMeasured(this.state)
+ ? this.adjustArrowPosition(placement, this.state)
+ : undefined
+ }
+ />
+ </TooltipWrapper>
+ </TooltipPortal>
+ )}
+ </>
+ );
+ }
+}
+
+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<FCProps<typeof Avatar>> = {}) {
+ return render(
+ <Avatar enableGravatar={true} gravatarServerUrl={gravatarServerUrl} name="foo" {...props} />
+ );
+}
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: <a href="#">foo</a> });
+ 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<DeferredSpinner['props']> = {}) {
+ // 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<DeferredSpinner['props']> = {}) {
+ return <DeferredSpinner {...props} />;
+}
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 }) => (
+ <ButtonSecondary onClick={onToggleClick} />
+ ));
+
+ await user.click(screen.getByRole('button'));
+ expect(screen.getByRole('menu')).toBeVisible();
+ });
+
+ function setupWithChildren(children?: Dropdown['props']['children']) {
+ return renderWithRouter(
+ <Dropdown id="test-menu" overlay={<div id="overlay" />}>
+ {children ?? <ButtonSecondary />}
+ </Dropdown>
+ );
+ }
+});
+
+describe('ActionsDropdown', () => {
+ it('renders', () => {
+ setup();
+ expect(screen.getByRole('button')).toHaveAccessibleName('menu');
+ });
+
+ function setup() {
+ return renderWithRouter(
+ <ActionsDropdown id="test-menu">
+ <div id="overlay" />
+ </ActionsDropdown>
+ );
+ }
+});
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(
+ <Tooltip overlay="test tooltip">
+ <ItemButton onClick={jest.fn()}>button</ItemButton>
+ </Tooltip>,
+ {},
+ { 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(
+ <DropdownMenu>
+ <ItemHeader>My header</ItemHeader>
+ <ItemNavLink to="/test">Test menu item</ItemNavLink>
+ <ItemDivider />
+ <ItemLink disabled={true} to="/test-disabled">
+ Test disabled item
+ </ItemLink>
+ <ItemButton icon={<MenuIcon />} onClick={noop}>
+ Button
+ </ItemButton>
+ <ItemDangerButton onClick={noop}>DangerButton</ItemDangerButton>
+ <ItemCopy copyValue="copy">Copy</ItemCopy>
+ <ItemCheckbox checked={true} onCheck={noop}>
+ Checkbox item
+ </ItemCheckbox>
+ <ItemRadioButton checked={false} onCheck={noop} value="radios">
+ Radio item
+ </ItemRadioButton>
+ </DropdownMenu>
+ );
+}
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 (
+ <CustomIcon {...props}>
+ <path d="l10 10" />
+ </CustomIcon>
+ );
+}
+
+it('should render single word and size', () => {
+ render(<GenericAvatar name="foo" size={15} />);
+ const image = screen.getByRole('img');
+ expect(image).toHaveAttribute('size', '15');
+ expect(screen.getByText('F')).toBeInTheDocument();
+});
+
+it('should render multiple word with default size', () => {
+ render(<GenericAvatar name="foo bar" />);
+ const image = screen.getByRole('img');
+ expect(image).toHaveAttribute('size', '24');
+ expect(screen.getByText('F')).toBeInTheDocument();
+});
+
+it('should render without name', () => {
+ render(<GenericAvatar Icon={TestIcon} name="" size={32} />);
+ 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<FCProps<typeof InputSearch>> = {}) {
+ return render(
+ <InputSearch
+ clearIconAriaLabel=""
+ maxLength={150}
+ minLength={2}
+ onChange={jest.fn()}
+ placeholder="placeholder"
+ searchInputAriaLabel=""
+ tooShortText=""
+ value="foo"
+ {...props}
+ />
+ );
+}
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(
+ <Link blurAfterClick={true} icon={<div>Icon</div>} 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(<Link preventDefault={true} to="/second" />);
+
+ 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(
+ <button onClick={buttonOnClick} type="button">
+ <Link stopPropagation={true} to="/second" />
+ </button>
+ );
+
+ 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(
+ <Link onClick={onClick} stopPropagation={true} to="/second" />
+ );
+
+ await user.click(screen.getByRole('link'));
+
+ expect(onClick).toHaveBeenCalled();
+});
+
+it('internal link should be clickable', async () => {
+ const { user } = setupWithMemoryRouter(<Link to="/second">internal link</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(<Link to="https://google.com">external link</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(<DiscreetLink to="https://google.com">external link</DiscreetLink>);
+ expect(screen.getByRole('link')).toBeVisible();
+
+ expect(screen.getByRole('img', { hidden: true })).toBeInTheDocument();
+});
+
+function ShowPath() {
+ const { pathname } = useLocation();
+ return <pre>{pathname}</pre>;
+}
+
+const setupWithMemoryRouter = (component: JSX.Element, initialEntries = ['/initial']) => {
+ return render(
+ <MemoryRouter initialEntries={initialEntries}>
+ <Routes>
+ <Route
+ element={
+ <>
+ {component}
+ <ShowPath />
+ </>
+ }
+ path="/initial"
+ />
+ <Route element={<ShowPath />} path="/second" />
+ </Routes>
+ </MemoryRouter>
+ );
+};
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<typeof MainAppBar> = {
+ Logo: () => <img alt="logo" src="http://example.com/logo.png" />,
+ }
+) {
+ return render(<MainAppBar {...props} />);
+}
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(
+ <MainMenuItem>
+ <a>Hi</a>
+ </MainMenuItem>
+ );
+
+ expect(screen.getByText('Hi')).toHaveStyle({
+ color: 'rgb(62, 67, 87)',
+ 'border-bottom': '3px solid transparent',
+ });
+});
+
+it('should render active link', () => {
+ render(
+ <MainMenuItem>
+ <a className="active">Hi</a>
+ </MainMenuItem>
+ );
+
+ 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(
+ <MainMenuItem>
+ <a className="hover">Hi</a>
+ </MainMenuItem>
+ );
+
+ 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(<NavLink blurAfterClick={true} 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(<NavLink preventDefault={true} to="/second" />);
+
+ 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(
+ <button onClick={buttonOnClick} type="button">
+ <NavLink stopPropagation={true} to="/second" />
+ </button>
+ );
+
+ 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(
+ <NavLink onClick={onClick} stopPropagation={true} to="/second" />
+ );
+
+ await user.click(screen.getByRole('link'));
+
+ expect(onClick).toHaveBeenCalled();
+});
+
+it('NavLink should be clickable', async () => {
+ const { user } = setupWithMemoryRouter(<NavLink to="/second">internal link</NavLink>);
+ expect(screen.getByRole('link')).toBeVisible();
+
+ await user.click(screen.getByRole('link'));
+
+ expect(screen.getByText('/second')).toBeVisible();
+});
+
+function ShowPath() {
+ const { pathname } = useLocation();
+ return <pre>{pathname}</pre>;
+}
+
+const setupWithMemoryRouter = (component: JSX.Element, initialEntries = ['/initial']) => {
+ return render(
+ <MemoryRouter initialEntries={initialEntries}>
+ <Routes>
+ <Route
+ element={
+ <>
+ {component}
+ <ShowPath />
+ </>
+ }
+ path="/initial"
+ />
+ <Route element={<ShowPath />} path="/second" />
+ </Routes>
+ </MemoryRouter>
+ );
+};
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(<SearchText match="hi" name="hiya" />);
+
+ expect(screen.getByText('hi')).toHaveStyle({
+ 'font-weight': '600',
+ });
+});
+
+it('should render TextMuted', () => {
+ render(<TextMuted text="Hi" />);
+
+ 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 },
+ <div onPointerEnter={onPointerEnter} onPointerLeave={onPointerLeave} role="note" />
+ );
+
+ 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<TooltipInner['props']> = {},
+ children = <div role="note" />
+ ) {
+ return render(
+ <TooltipInner mouseLeaveDelay={0} overlay={<span id="overlay" />} {...props}>
+ {children}
+ </TooltipInner>
+ );
+ }
+});
+
+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<FCProps<typeof Tooltip>> = {},
+ children = <div role="note" />
+ ) {
+ return render(
+ <Tooltip overlay={<span id="overlay" />} {...props}>
+ {children}
+ </Tooltip>
+ );
+ }
+});
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(<ClipboardButton copyValue="foo">{children}</ClipboardButton>);
+ }
+});
+
+describe('ClipboardIconButton', () => {
+ it('should display correctly', () => {
+ renderWithContext(<ClipboardIconButton copyValue="foo" />);
+
+ 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<HTMLButtonElement>,
+ '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<HTMLButtonElement>;
+ onClick?: VoidFunction;
+
+ preventDefault?: boolean;
+ reloadDocument?: LinkProps['reloadDocument'];
+ stopPropagation?: boolean;
+ target?: LinkProps['target'];
+ to?: LinkProps['to'];
+}
+
+class Button extends React.PureComponent<ButtonProps> {
+ handleClick = (event: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => {
+ 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 (
+ <BaseButtonLink {...props} onClick={onClick} to={to}>
+ {icon}
+ {children}
+ </BaseButtonLink>
+ );
+ }
+
+ return (
+ <BaseButton {...props} onClick={this.handleClick} ref={innerRef}>
+ {icon}
+ {children}
+ </BaseButton>
+ );
+ }
+}
+
+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<ButtonProps> = 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<ButtonProps> = 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<ButtonProps> = 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<ButtonProps> = styled(Button)`
+ --background: ${themeColor('dangerButtonSecondary')};
+ --backgroundHover: ${themeColor('dangerButtonSecondaryHover')};
+ --color: ${themeContrast('dangerButtonSecondary')};
+ --focus: ${themeColor('dangerButtonSecondaryFocus', 0.2)};
+ --border: ${themeBorder('default', 'dangerButtonSecondaryBorder')};
+`;
+
+interface ThirdPartyProps extends Omit<ButtonProps, 'Icon'> {
+ iconPath: string;
+ name: string;
+}
+
+export function ThirdPartyButton({ children, iconPath, name, ...buttonProps }: ThirdPartyProps) {
+ const size = 16;
+ return (
+ <ThirdPartyButtonStyled {...buttonProps}>
+ <img alt={name} className="sw-mr-1" height={size} src={iconPath} width={size} />
+ {children}
+ </ThirdPartyButtonStyled>
+ );
+}
+
+const ThirdPartyButtonStyled: React.FC<ButtonProps> = 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<BaseProps, State> {
+ 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 = <CopyIcon />,
+ className,
+ children,
+ copyValue,
+}: ButtonProps) {
+ return (
+ <ClipboardBase>
+ {({ setCopyButton, copySuccess }) => (
+ <Tooltip overlay={translate('copied_action')} visible={copySuccess}>
+ <ButtonSecondary
+ className={classNames('sw-select-none', className)}
+ data-clipboard-text={copyValue}
+ icon={icon}
+ innerRef={setCopyButton}
+ >
+ {children || translate('copy')}
+ </ButtonSecondary>
+ </Tooltip>
+ )}
+ </ClipboardBase>
+ );
+}
+
+interface IconButtonProps {
+ Icon?: React.ComponentType<IconProps>;
+ '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 (
+ <ClipboardBase>
+ {({ setCopyButton, copySuccess }) => {
+ return (
+ <Tooltip
+ mouseEnterDelay={INTERACTIVE_TOOLTIP_DELAY}
+ overlay={
+ <div className="sw-w-abs-150 sw-text-center">
+ {translate(copySuccess ? 'copied_action' : 'copy_to_clipboard')}
+ </div>
+ }
+ {...(copySuccess ? { visible: copySuccess } : undefined)}
+ >
+ <InteractiveIconComponent
+ Icon={Icon}
+ aria-label={props['aria-label'] ?? translate('copy_to_clipboard')}
+ className={className}
+ data-clipboard-text={copyValue}
+ innerRef={setCopyButton}
+ size={size}
+ />
+ </Tooltip>
+ );
+ }}
+ </ClipboardBase>
+ );
+}
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 (
+ <CustomIcon {...iconProps}>
+ <path
+ clipRule="evenodd"
+ d="M11.6634 5.47789c.2884.29737.2811.77218-.0163 1.06054L7.52211 10.5384c-.29414.2852-.76273.2816-1.05244-.0081l-2-1.99997c-.29289-.29289-.29289-.76777 0-1.06066s.76777-.29289 1.06066 0L7.0081 8.94744l3.5948-3.48586c.2974-.28836.7722-.28105 1.0605.01631Z"
+ fill={themeColor(fill)({ theme })}
+ fillRule="evenodd"
+ />
+ </CustomIcon>
+ );
+}
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<Props, 'children'> {
+ fill?: ThemeColors | CSSColor;
+}
+
+export function CustomIcon(props: Props) {
+ const { 'aria-label': ariaLabel, children, className, ...iconProps } = props;
+ return (
+ <svg
+ aria-hidden={ariaLabel ? 'false' : 'true'}
+ aria-label={ariaLabel}
+ className={className}
+ fill="none"
+ height={theme('height.icon')}
+ role="img"
+ style={{
+ clipRule: 'evenodd',
+ display: 'inline-block',
+ fillRule: 'evenodd',
+ userSelect: 'none',
+ verticalAlign: 'middle',
+ strokeLinejoin: 'round',
+ strokeMiterlimit: 1.414,
+ }}
+ version="1.1"
+ viewBox="0 0 16 16"
+ width={theme('width.icon')}
+ xmlSpace="preserve"
+ xmlnsXlink="http://www.w3.org/1999/xlink"
+ {...iconProps}
+ >
+ {children}
+ </svg>
+ );
+}
+
+export function OcticonHoc(
+ WrappedOcticon: React.ComponentType<OcticonProps>,
+ displayName?: string
+): React.ComponentType<IconProps> {
+ function IconWrapper({ fill, ...props }: IconProps) {
+ const theme = useTheme();
+ return (
+ <WrappedOcticon
+ fill={fill && themeColor(fill)({ theme })}
+ size="small"
+ verticalAlign="middle"
+ {...props}
+ />
+ );
+ }
+
+ 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 (
+ <CustomIcon {...iconProps}>
+ <path
+ clipRule="evenodd"
+ d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16Zm.507-5.451H6.66v-.166c.005-1.704.462-2.226 1.28-2.742.6-.38 1.062-.803 1.062-1.441 0-.677-.53-1.116-1.188-1.116-.638 0-1.227.424-1.257 1.218H4.571c.044-1.948 1.486-2.873 3.254-2.873 1.933 0 3.307.993 3.307 2.698 0 1.144-.595 1.86-1.505 2.4-.77.463-1.11.906-1.12 1.856v.166Zm.282 1.948a1.185 1.185 0 0 1-1.169 1.169 1.164 1.164 0 1 1 0-2.328c.624 0 1.164.52 1.169 1.159Z"
+ fill={themeColor(fill)({ theme })}
+ fillRule="evenodd"
+ />
+ </CustomIcon>
+ );
+}
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 (
+ <CustomIcon {...iconProps}>
+ <path
+ clipRule="evenodd"
+ d="M12 7c0 2.76142-2.23858 5-5 5S2 9.76142 2 7s2.23858-5 5-5 5 2.23858 5 5Zm-.8078 5.6064C10.0236 13.4816 8.57234 14 7 14c-3.86599 0-7-3.134-7-7 0-3.86599 3.13401-7 7-7 3.866 0 7 3.13401 7 7 0 1.57234-.5184 3.0236-1.3936 4.1922l3.0505 3.0504c.3905.3906.3905 1.0237 0 1.4143-.3906.3905-1.0237.3905-1.4143 0l-3.0504-3.0505Z"
+ fill={themeColor(fill)({ theme })}
+ fillRule="evenodd"
+ />
+ </CustomIcon>
+ );
+}
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(
+ <CustomIcon>
+ <path d="test" />
+ </CustomIcon>
+ );
+
+ expect(screen.queryByRole('img')).not.toBeInTheDocument();
+ expect(screen.getByRole('img', { hidden: true })).toContainHTML('<path d="test"/>');
+});
+
+it('should not be hidden when aria-label is set', () => {
+ render(
+ <CustomIcon aria-label="test">
+ <path d="test" />
+ </CustomIcon>
+ );
+
+ expect(screen.getByRole('img')).toBeVisible();
+});
+
+describe('Octicon HOC', () => {
+ it('should render correctly', () => {
+ const Wrapped = OcticonHoc(CheckIcon, 'TestIcon');
+
+ render(<Wrapped aria-label="visible" />);
+
+ 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<HTMLDivElement>) {
+ const {
+ children,
+ className,
+ placement = PopupPlacement.Bottom,
+ style,
+ zLevel = PopupZLevel.Default,
+ ...ariaProps
+ } = props;
+ return (
+ <ClickEventBoundary>
+ <PopupWrapper
+ className={classNames(`is-${placement}`, className)}
+ ref={ref || React.createRef()}
+ style={style}
+ zLevel={zLevel}
+ {...ariaProps}
+ >
+ {children}
+ </PopupWrapper>
+ </ClickEventBoundary>
+ );
+}
+
+const PopupWithRef = React.forwardRef(PopupBase);
+PopupWithRef.displayName = 'Popup';
+
+export const Popup = PopupWithRef;
+
+interface PortalPopupProps extends Omit<PopupProps, 'style'> {
+ allowResizing?: boolean;
+ children: React.ReactNode;
+ overlay: React.ReactNode;
+}
+
+interface Measurements {
+ height: number;
+ left: number;
+ top: number;
+ width: number;
+}
+
+type State = Partial<Measurements>;
+
+function isMeasured(state: State): state is Measurements {
+ return state.height !== undefined;
+}
+
+export class PortalPopup extends React.PureComponent<PortalPopupProps, State> {
+ mounted = false;
+ popupNode = React.createRef<HTMLDivElement>();
+ 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 && (
+ <PortalWrapper>
+ <Popup placement={placement} ref={this.popupNode} style={style} {...popupProps}>
+ {overlay}
+ </Popup>
+ </PortalWrapper>
+ )}
+ </>
+ );
+ }
+}
+
+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<number | string>, 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
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 <div>I&apos;m a dummy</div>;
-}
+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 | number>
+): 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<RenderOptions, 'wrapper'> & {
+ 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 (
+ <HelmetProvider>
+ <MemoryRouter>
+ <Routes>
+ <Route element={children} path="/" />
+ {additionalRoutes}
+ </Routes>
+ </MemoryRouter>
+ </HelmetProvider>
+ );
+ }
+
+ return render(ui, { ...renderOptions, wrapper: RouterWrapper }, userEventOptions);
+}
+
+function getContextWrapper() {
+ return function ContextWrapper({ children }: React.PropsWithChildren<{}>) {
+ return (
+ <HelmetProvider>
+ <IntlProvider defaultLocale="en" locale="en">
+ {children}
+ </IntlProvider>
+ </HelmetProvider>
+ );
+ };
+}
+
+export function mockComponent(name: string, transformProps: (props: any) => any = identity) {
+ function MockedComponent({ ...props }: PropsWithChildren<any>) {
+ 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<void> {
+ 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<T>(name: keyof Omit<T, keyof ThemedProps>) {
+ 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<T>(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<T extends React.FunctionComponent<any>> = Parameters<T>[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<LightTheme, 'colors' | 'contrasts'> {
+ 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 = {
'<rootDir>/config/polyfills.ts',
'<rootDir>/config/jest/SetupEnzyme.ts',
'<rootDir>/config/jest/SetupTestEnvironment.ts',
+ '<rootDir>/config/jest/SetupTheme.js',
],
setupFilesAfterEnv: ['<rootDir>/config/jest/SetupReactTestingLibrary.ts'],
snapshotSerializers: ['enzyme-to-json/serializer', '@emotion/jest/serializer'],
testEnvironment: 'jsdom',
- testPathIgnorePatterns: ['<rootDir>/config', '<rootDir>/node_modules', '<rootDir>/scripts'],
+ testPathIgnorePatterns: [
+ '<rootDir>/config',
+ '<rootDir>/design-system',
+ '<rootDir>/node_modules',
+ '<rootDir>/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 (
- <SuggestionsProvider>
- <A11yProvider>
- <StartupModal>
- <A11ySkipLinks />
- <div className="global-container">
- <div className="page-wrapper" id="container">
- <div className="page-container">
- <BranchStatusContextProvider>
- <Workspace>
- <IndexationContextProvider>
- <LanguagesContextProvider>
- <MetricsContextProvider>
- <SystemAnnouncement />
- <IndexationNotification />
- <UpdateNotification dismissable={true} />
- <GlobalNav location={location} />
- <Outlet />
- </MetricsContextProvider>
- </LanguagesContextProvider>
- </IndexationContextProvider>
- </Workspace>
- </BranchStatusContextProvider>
+ <ThemeProvider theme={lightTheme}>
+ <SuggestionsProvider>
+ <A11yProvider>
+ <StartupModal>
+ <A11ySkipLinks />
+ <div className="global-container">
+ <div className="page-wrapper" id="container">
+ <div className="page-container">
+ <BranchStatusContextProvider>
+ <Workspace>
+ <IndexationContextProvider>
+ <LanguagesContextProvider>
+ <MetricsContextProvider>
+ <SystemAnnouncement />
+ <IndexationNotification />
+ <UpdateNotification dismissable={true} />
+ <GlobalNav location={location} />
+ <Outlet />
+ </MetricsContextProvider>
+ </LanguagesContextProvider>
+ </IndexationContextProvider>
+ </Workspace>
+ </BranchStatusContextProvider>
+ </div>
+ <PromotionNotification />
</div>
- <PromotionNotification />
+ <GlobalFooter />
</div>
- <GlobalFooter />
- </div>
- </StartupModal>
- </A11yProvider>
- </SuggestionsProvider>
+ </StartupModal>
+ </A11yProvider>
+ </SuggestionsProvider>
+ </ThemeProvider>
);
}
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 (
<div className="global-container">
<div className="page-wrapper" id="container">
- <NavBar className="global-navbar" height={rawSizes.globalNavHeightRaw} />
+ <MainSonarQubeBar />
{children !== undefined ? children : <Outlet />}
</div>
<GlobalFooter />
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
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<Props, State> {
+export class GlobalSearch extends React.PureComponent<Props, State> {
input?: HTMLInputElement | null;
node?: HTMLElement | null;
nodes: Dict<HTMLElement>;
@@ -74,13 +77,11 @@ export class Search extends React.PureComponent<Props, State> {
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<Props, State> {
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<Props, State> {
query: '',
results: {},
selected: undefined,
- shortQuery: false,
});
} else {
this.setState({ open: false });
@@ -178,8 +177,6 @@ export class Search extends React.PureComponent<Props, State> {
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<Props, State> {
};
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<Props, State> {
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<Props, State> {
}
};
- handleKeyDown = (event: KeyboardEvent) => {
+ handleKeyDown = (event: React.KeyboardEvent) => {
if (!this.state.open) {
return;
}
@@ -330,7 +331,7 @@ export class Search extends React.PureComponent<Props, State> {
};
renderResult = (component: ComponentResult) => (
- <SearchResult
+ <GlobalSearchResult
component={component}
innerRef={this.innerRef}
key={component.key}
@@ -341,73 +342,89 @@ export class Search extends React.PureComponent<Props, State> {
);
renderNoResults = () => (
- <div className="navbar-search-no-results" aria-live="assertive">
+ <div className="sw-px-3 sw-py-2" aria-live="assertive">
{translateWithParameters('no_results_for_x', this.state.query)}
</div>
);
render() {
+ const { open, query, results, more, loadingMore, selected, loading } = this.state;
+ if (!open && !query) {
+ return (
+ <Tooltip mouseEnterDelay={INTERACTIVE_TOOLTIP_DELAY} overlay={translate('search_verb')}>
+ <InteractiveIcon
+ className="it__search-icon"
+ Icon={MenuSearchIcon}
+ aria-label={translate('search_verb')}
+ currentColor={true}
+ onClick={this.handleFocus}
+ size="medium"
+ />
+ </Tooltip>
+ );
+ }
+
+ const list = this.getPlainComponentsList(results, more);
const search = (
- <div role="search" className="navbar-search dropdown">
- <DeferredSpinner className="navbar-search-icon" loading={this.state.loading} />
-
- <SearchBox
- autoFocus={this.state.open}
- innerRef={this.searchInputRef}
- minLength={2}
- onChange={this.handleQueryChange}
- onFocus={this.handleFocus}
- placeholder={translate('search.placeholder')}
- value={this.state.query}
- />
-
- {this.state.shortQuery && (
- <span className="navbar-search-input-hint" aria-live="assertive">
- {translateWithParameters('select2.tooShort', MIN_SEARCH_QUERY_LENGTH)}
- </span>
- )}
-
- {this.state.open && Object.keys(this.state.results).length > 0 && (
- <DropdownOverlay noPadding={true}>
- <div className="global-navbar-search-dropdown" ref={(node) => (this.node = node)}>
- <SearchResults
- allowMore={this.state.query.length !== 1}
- loadingMore={this.state.loadingMore}
- more={this.state.more}
- onMoreClick={this.searchMore}
- onSelect={this.handleSelect}
- renderNoResults={this.renderNoResults}
- renderResult={this.renderResult}
- results={this.state.results}
- selected={this.state.selected}
- />
- <div className="dropdown-bottom-hint">
- <div className="pull-right" aria-hidden={true}>
- <ClockIcon className="little-spacer-right" size={12} />
- {translate('recently_browsed')}
- </div>
- <FormattedMessage
- defaultMessage={translate('search.shortcut_hint')}
- id="search.shortcut_hint"
- values={{
- shortcut: <span className="shortcut-button shortcut-button-small">s</span>,
- }}
+ <div role="search" className="sw-min-w-abs-200 sw-max-w-abs-350 sw-w-full">
+ <PortalPopup
+ allowResizing={true}
+ overlay={
+ open && (
+ <DropdownMenu
+ className="it__global-navbar-search-dropdown sw-overflow-y-auto sw-overflow-x-hidden"
+ maxHeight="38rem"
+ innerRef={(node: HTMLUListElement | null) => (this.node = node)}
+ size="auto"
+ >
+ <GlobalSearchResults
+ query={query}
+ loadingMore={loadingMore}
+ more={more}
+ onMoreClick={this.searchMore}
+ onSelect={this.handleSelect}
+ renderNoResults={this.renderNoResults}
+ renderResult={this.renderResult}
+ results={results}
+ selected={selected}
/>
- </div>
- </div>
- </DropdownOverlay>
- )}
+ {list.length > 0 && (
+ <li className="sw-px-3 sw-pt-1">
+ <TextMuted text={translate('global_search.shortcut_hint')} />
+ </li>
+ )}
+ </DropdownMenu>
+ )
+ }
+ placement={PopupPlacement.BottomLeft}
+ zLevel={PopupZLevel.Global}
+ >
+ <InputSearch
+ className="sw-w-full"
+ autoFocus={open}
+ innerRef={this.searchInputRef}
+ loading={loading}
+ minLength={MIN_SEARCH_QUERY_LENGTH}
+ onChange={this.handleQueryChange}
+ onFocus={this.handleFocus}
+ onKeyDown={this.handleKeyDown}
+ placeholder={translate('search.search_for_projects')}
+ size="auto"
+ value={query}
+ tooShortText={translateWithParameters('select2.tooShort', MIN_SEARCH_QUERY_LENGTH)}
+ searchInputAriaLabel={translate('search_verb')}
+ clearIconAriaLabel={translate('clear')}
+ />
+ </PortalPopup>
</div>
);
- return this.state.open ? (
- <FocusOutHandler onFocusOut={this.handleClickOutside}>
- <OutsideClickHandler onClickOutside={this.handleClickOutside}>{search}</OutsideClickHandler>
- </FocusOutHandler>
+ return open ? (
+ <OutsideClickHandler onClickOutside={this.handleClickOutside}>{search}</OutsideClickHandler>
) : (
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<Props> {
+ doSelect = () => {
+ this.props.onSelect(this.props.component.key);
+ };
+
+ render() {
+ const { component, selected } = this.props;
+ const to = getComponentOverviewUrl(component.key, component.qualifier);
+ return (
+ <ItemLink
+ className={classNames('sw-flex sw-flex-col sw-items-start sw-space-y-1', {
+ active: selected,
+ })}
+ innerRef={(node: HTMLAnchorElement | null) => this.props.innerRef(component.key, node)}
+ key={component.key}
+ onClick={this.props.onClose}
+ onPointerEnter={this.doSelect}
+ to={to}
+ >
+ <div className="sw-flex sw-justify-between sw-items-center sw-w-full">
+ <SearchText match={component.match} name={component.name} />
+ {component.isFavorite && <FavoriteIcon favorite={true} size={16} />}
+ {!component.isFavorite && component.isRecentlyBrowsed && (
+ <ClockIcon aria-label={translate('recently_browsed')} />
+ )}
+ </div>
+ <TextMuted text={component.key} />
+ </ItemLink>
+ );
+ }
+}
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
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<Props> {
+export default function GlobalSearchResults(props: Props): React.ReactElement<Props> {
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(
- <>
- <h2 className="menu-header no-margin" id={translate('qualifiers', qualifier)}>
- {translate('qualifiers', qualifier)}
- </h2>
- <ul
- className="menu"
- key={`header-${qualifier}`}
- aria-labelledby={translate('qualifiers', qualifier)}
- >
+ <li key={`group-${qualifier}`}>
+ <ul key={`header-${qualifier}`} aria-labelledby={translate('qualifiers', qualifier)}>
+ <ItemHeader>
+ <p id={translate('qualifiers', qualifier)}>{translate('qualifiers', qualifier)}</p>
+ </ItemHeader>
{components.map((component) => props.renderResult(component))}
{more !== undefined && more > 0 && (
- <SearchShowMore
- allowMore={props.allowMore}
+ <GlobalSearchShowMore
+ allowMore={allowMore}
key={`more-${qualifier}`}
loadingMore={props.loadingMore}
onMoreClick={props.onMoreClick}
@@ -66,11 +62,12 @@ export default function SearchResults(props: Props): React.ReactElement<Props> {
selected={props.selected === `qualifier###${qualifier}`}
/>
)}
+ <ItemDivider />
</ul>
- </>
+ </li>
);
}
});
- return renderedComponents.length > 0 ? <div>{renderedComponents}</div> : 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
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<Props> {
- handleMoreClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
+export default class GlobalSearchShowMore extends React.PureComponent<Props> {
+ handleMoreClick = (event: React.MouseEvent<HTMLButtonElement>, qualifier: string) => {
event.preventDefault();
event.stopPropagation();
event.currentTarget.blur();
- const { qualifier } = event.currentTarget.dataset;
if (qualifier) {
this.props.onMoreClick(qualifier);
}
};
- handleMoreMouseEnter = (event: React.MouseEvent<HTMLAnchorElement>) => {
- 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 (
- <li className={classNames('menu-footer', { active: selected })} key={`more-${qualifier}`}>
- <DeferredSpinner className="navbar-search-icon" loading={loadingMore === qualifier}>
- <a
- className={classNames({ 'cursor-not-allowed': !this.props.allowMore })}
- data-qualifier={qualifier}
- href="#"
- onClick={this.handleMoreClick}
- onMouseEnter={this.handleMoreMouseEnter}
- >
- <div className="pull-right text-muted-2 menu-footer-note">
- <FormattedMessage
- defaultMessage={translate('search.show_more.hint')}
- id="search.show_more.hint"
- values={{
- key: <span className="shortcut-button shortcut-button-small">Enter</span>,
- }}
- />
- </div>
- <span>{translate('show_more')}</span>
- </a>
+ <ItemButton
+ className={classNames({ active: selected })}
+ disabled={!allowMore}
+ onClick={(e: React.MouseEvent<HTMLButtonElement>) => this.handleMoreClick(e, qualifier)}
+ onPointerEnter={() => {
+ this.handleMouseEnter(qualifier);
+ }}
+ >
+ <DeferredSpinner loading={loadingMore === qualifier}>
+ {translate('show_more')}
</DeferredSpinner>
- </li>
+ </ItemButton>
);
}
}
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: '<mark>Bar</mark>',
+ 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(<GlobalSearchWithoutRouter router={router} />);
+ 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(<GlobalSearch />);
+}
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
index 51f9fc636e5..51f9fc636e5 100644
--- 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
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 (
- <div style={{ height: sizes.globalNavHeight }}>
- <div className="navbar global-navbar" id="global-navigation">
- <div className="global-navbar-inner">
- <GlobalNavBranding />
-
+ <MainSonarQubeBar>
+ <div className="sw-flex" id="global-navigation">
+ <div className="it__global-navbar-menu sw-flex sw-justify-start sw-items-center sw-flex-1">
<GlobalNavMenu currentUser={currentUser} location={location} />
+ <div className="sw-px-8 sw-flex-1">
+ <GlobalSearch />
+ </div>
+ </div>
- <div className="global-navbar-menu global-navbar-menu-right">
- <EmbedDocsPopupHelper />
- <Search />
- <GlobalNavUser currentUser={currentUser} />
+ <div className="sw-flex sw-items-center sw-ml-2">
+ <EmbedDocsPopupHelper />
+ <div className="sw-ml-4">
+ <GlobalNavUser />
</div>
</div>
</div>
- </div>
+ </MainSonarQubeBar>
);
}
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx
index 2eabde72d79..a579af63b49 100644
--- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx
@@ -18,19 +18,18 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import classNames from 'classnames';
+import { MainMenu, MainMenuItem } from 'design-system';
import * as React from 'react';
import { NavLink } from 'react-router-dom';
import { isMySet } from '../../../../apps/issues/utils';
import Link from '../../../../components/common/Link';
-import Dropdown from '../../../../components/controls/Dropdown';
-import DropdownIcon from '../../../../components/icons/DropdownIcon';
import { translate } from '../../../../helpers/l10n';
import { getQualityGatesUrl } from '../../../../helpers/urls';
import { AppState } from '../../../../types/appstate';
import { ComponentQualifier } from '../../../../types/component';
-import { Extension } from '../../../../types/types';
import { CurrentUser } from '../../../../types/users';
import withAppStateContext from '../../app-state/withAppStateContext';
+import GlobalNavMore from './GlobalNavMore';
interface Props {
appState: AppState;
@@ -39,14 +38,15 @@ interface Props {
}
const ACTIVE_CLASS_NAME = 'active';
-export class GlobalNavMenu extends React.PureComponent<Props> {
+
+class GlobalNavMenu extends React.PureComponent<Props> {
renderProjects() {
const active =
this.props.location.pathname.startsWith('/projects') &&
this.props.location.pathname !== '/projects/create';
return (
- <li>
+ <MainMenuItem>
<Link
aria-current={active ? 'page' : undefined}
className={classNames({ active })}
@@ -54,17 +54,17 @@ export class GlobalNavMenu extends React.PureComponent<Props> {
>
{translate('projects.page')}
</Link>
- </li>
+ </MainMenuItem>
);
}
renderPortfolios() {
return (
- <li>
+ <MainMenuItem>
<NavLink className={({ isActive }) => (isActive ? ACTIVE_CLASS_NAME : '')} to="/portfolios">
{translate('portfolios.page')}
</NavLink>
- </li>
+ </MainMenuItem>
);
}
@@ -76,50 +76,50 @@ export class GlobalNavMenu extends React.PureComponent<Props> {
).toString();
return (
- <li>
+ <MainMenuItem>
<NavLink
className={({ isActive }) => (isActive ? ACTIVE_CLASS_NAME : '')}
to={{ pathname: '/issues', search }}
>
{translate('issues.page')}
</NavLink>
- </li>
+ </MainMenuItem>
);
}
renderRulesLink() {
return (
- <li>
+ <MainMenuItem>
<NavLink
className={({ isActive }) => (isActive ? ACTIVE_CLASS_NAME : '')}
to="/coding_rules"
>
{translate('coding_rules.page')}
</NavLink>
- </li>
+ </MainMenuItem>
);
}
renderProfilesLink() {
return (
- <li>
+ <MainMenuItem>
<NavLink className={({ isActive }) => (isActive ? ACTIVE_CLASS_NAME : '')} to="/profiles">
{translate('quality_profiles.page')}
</NavLink>
- </li>
+ </MainMenuItem>
);
}
renderQualityGatesLink() {
return (
- <li>
+ <MainMenuItem>
<NavLink
className={({ isActive }) => (isActive ? ACTIVE_CLASS_NAME : '')}
to={getQualityGatesUrl()}
>
{translate('quality_gates.page')}
</NavLink>
- </li>
+ </MainMenuItem>
);
}
@@ -129,51 +129,14 @@ export class GlobalNavMenu extends React.PureComponent<Props> {
}
return (
- <li>
+ <MainMenuItem>
<NavLink
className={({ isActive }) => (isActive ? ACTIVE_CLASS_NAME : '')}
to="/admin/settings"
>
{translate('layout.settings')}
</NavLink>
- </li>
- );
- }
-
- renderGlobalPageLink = ({ key, name }: Extension) => {
- return (
- <li key={key}>
- <Link to={`/extension/${key}`}>{name}</Link>
- </li>
- );
- };
-
- renderMore() {
- const { globalPages = [] } = this.props.appState;
- const withoutPortfolios = globalPages.filter((page) => page.key !== 'governance/portfolios');
- if (withoutPortfolios.length === 0) {
- return null;
- }
- return (
- <Dropdown
- overlay={<ul className="menu">{withoutPortfolios.map(this.renderGlobalPageLink)}</ul>}
- tagName="li"
- >
- {({ onToggleClick, open }) => (
- <a
- aria-expanded={open}
- aria-haspopup="menu"
- role="button"
- className={classNames('dropdown-toggle', { active: open })}
- href="#"
- id="global-navigation-more"
- onClick={onToggleClick}
- >
- {translate('more')}
- <DropdownIcon className="little-spacer-left text-middle" />
- </a>
- )}
- </Dropdown>
+ </MainMenuItem>
);
}
@@ -184,7 +147,7 @@ export class GlobalNavMenu extends React.PureComponent<Props> {
return (
<nav aria-label={translate('global')}>
- <ul className="global-navbar-menu">
+ <MainMenu>
{this.renderProjects()}
{governanceInstalled && this.renderPortfolios()}
{this.renderIssuesLink()}
@@ -192,8 +155,8 @@ export class GlobalNavMenu extends React.PureComponent<Props> {
{this.renderProfilesLink()}
{this.renderQualityGatesLink()}
{this.renderAdministrationLink()}
- {this.renderMore()}
- </ul>
+ <GlobalNavMore />
+ </MainMenu>
</nav>
);
}
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 (
+ <ItemNavLink key={key} to={`/extension/${key}`}>
+ {name}
+ </ItemNavLink>
+ );
+};
+
+function GlobalNavMore({ appState: { globalPages = [] } }: { appState: AppState }) {
+ const withoutPortfolios = globalPages.filter((page) => page.key !== 'governance/portfolios');
+
+ if (withoutPortfolios.length === 0) {
+ return null;
+ }
+
+ return (
+ <Dropdown
+ id="moreMenuDropdown"
+ overlay={<ul>{withoutPortfolios.map(renderGlobalPageLink)}</ul>}
+ placement={PopupPlacement.BottomLeft}
+ >
+ {({ onToggleClick, open }) => (
+ <ul>
+ <MainMenuItem>
+ <a
+ aria-expanded={open}
+ aria-haspopup="menu"
+ href="#"
+ id="global-navigation-more"
+ onClick={onToggleClick}
+ role="button"
+ >
+ {translate('more')}
+ </a>
+ </MainMenuItem>
+ </ul>
+ )}
+ </Dropdown>
+ );
+}
+
+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<Props> {
- focusNode = (node: HTMLAnchorElement | null) => {
- if (node) {
- node.focus();
- }
- };
+ const { settings } = React.useContext(AppStateContext);
- handleLogin = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
- 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<HTMLAnchorElement>) => {
- event.preventDefault();
- this.props.router.push('/sessions/logout');
- };
+ }, []);
- renderAuthenticated() {
- const currentUser = this.props.currentUser as LoggedInUser;
- return (
- <Dropdown
- className="js-user-authenticated"
- overlay={
- <ul className="menu">
- <li className="menu-item">
- <div className="text-ellipsis text-muted" title={currentUser.name}>
- <strong>{currentUser.name}</strong>
- </div>
- {currentUser.email != null && (
- <div
- className="little-spacer-top text-ellipsis text-muted"
- title={currentUser.email}
- >
- {currentUser.email}
- </div>
- )}
- </li>
- <li className="divider" />
- <li>
- <Link ref={this.focusNode} to="/account">
- {translate('my_account.page')}
- </Link>
- </li>
- <li>
- <a href="#" onClick={this.handleLogout}>
- {translate('layout.logout')}
- </a>
- </li>
- </ul>
- }
- >
- <a className="dropdown-toggle navbar-avatar" href="#" title={currentUser.name}>
- <Avatar
- hash={currentUser.avatar}
- name={currentUser.name}
- size={rawSizes.globalNavContentHeightRaw}
- />
- </a>
- </Dropdown>
- );
- }
-
- renderAnonymous() {
+ if (!currentUser || !isLoggedIn(currentUser)) {
return (
<div>
- <Link className="navbar-login" to="/sessions/new" onClick={this.handleLogin}>
- {translate('layout.login')}
- </Link>
+ <ButtonSecondary onClick={handleLogin}>{translate('layout.login')}</ButtonSecondary>
</div>
);
}
- 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 (
+ <Dropdown
+ id="userAccountMenuDropdown"
+ placement={PopupPlacement.BottomRight}
+ zLevel={PopupZLevel.Global}
+ overlay={<GlobalNavUserMenu currentUser={currentUser} />}
+ >
+ {({ a11yAttrs: { role, ...a11yAttrs }, onToggleClick, open }) => (
+ <Tooltip
+ mouseEnterDelay={0.2}
+ overlay={translate('global_nav.account.tooltip')}
+ visible={open ? false : undefined}
+ >
+ <BareButton
+ aria-label={translate('global_nav.account.tooltip')}
+ onClick={onToggleClick}
+ {...a11yAttrs}
+ >
+ <Avatar
+ enableGravatar={enableGravatar}
+ gravatarServerUrl={gravatarServerUrl}
+ hash={currentUser.avatar}
+ name={currentUser.name}
+ />
+ </BareButton>
+ </Tooltip>
+ )}
+ </Dropdown>
+ );
+}
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<HTMLAnchorElement>(null);
+
+ const handleLogout = React.useCallback(() => {
+ navigateTo('/sessions/logout');
+ }, [navigateTo]);
+
+ React.useEffect(() => {
+ firstItemRef.current?.focus();
+ }, [firstItemRef]);
+
+ return (
+ <>
+ <ItemHeader>
+ <ItemHeaderHighlight title={currentUser.name}>{currentUser.name}</ItemHeaderHighlight>
+ {currentUser.email != null && (
+ <div className="sw-mt-1" title={currentUser.email}>
+ {currentUser.email}
+ </div>
+ )}
+ </ItemHeader>
+ <ItemDivider />
+ <ItemNavLink end={true} to="/account" innerRef={firstItemRef}>
+ {translate('my_account.page')}
+ </ItemNavLink>
+ <ItemDivider />
+ <ItemButton onClick={handleLogout}>{translate('layout.logout')}</ItemButton>
+ </>
+ );
+}
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 (
+ <div aria-label={title} role="img">
+ {customLogoUrl ? <img alt={title} src={customLogoUrl} /> : <SonarQubeLogo />}
+ </div>
+ );
+}
+
+export default function MainSonarQubeBar({ children }: React.PropsWithChildren<{}>) {
+ return <MainAppBar Logo={LogoWithAriaText}>{children}</MainAppBar>;
+}
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<GlobalNavProps> = {}) {
- return shallow(
- <GlobalNav
- currentUser={{ isLoggedIn: false, dismissedNotices: {} }}
- location={location}
- {...props}
- />
- );
+function renderGlobalNav({ appState = mockAppState(), currentUser = mockCurrentUser() }) {
+ renderApp('/', <GlobalNav location={mockLocation()} />, {
+ 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<GlobalNavBrandingProps> = {}) {
- return shallow(<GlobalNavBranding appState={mockAppState()} {...overrides} />);
-}
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<GlobalNavMenu['props']>) {
- renderComponent(
- <GlobalNavMenu appState={appState} currentUser={currentUser} location={location} />
- );
+}) {
+ renderApp('/', <GlobalNavMenu currentUser={currentUser} location={location} />, {
+ 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<GlobalNavUser['props']> = {}) {
- return renderComponent(
- <GlobalNavUser currentUser={mockLoggedInUser()} router={mockRouter()} {...overrides} />
- );
+function renderGlobalNavUser(overrides: { currentUser?: CurrentUser } = {}) {
+ return renderApp('/', <GlobalNavUser />, { 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`] = `
-<div
- style={
- {
- "height": "48px",
- }
- }
->
- <div
- className="navbar global-navbar"
- id="global-navigation"
- >
- <div
- className="global-navbar-inner"
- >
- <withAppStateContext(GlobalNavBranding) />
- <withAppStateContext(GlobalNavMenu)
- currentUser={
- {
- "dismissedNotices": {},
- "isLoggedIn": false,
- }
- }
- location={
- {
- "pathname": "",
- }
- }
- />
- <div
- className="global-navbar-menu global-navbar-menu-right"
- >
- <EmbedDocsPopupHelper />
- <withRouter(Search) />
- <withRouter(GlobalNavUser)
- currentUser={
- {
- "dismissedNotices": {},
- "isLoggedIn": false,
- }
- }
- />
- </div>
- </div>
- </div>
-</div>
-`;
-
-exports[`should render correctly: logged in users 1`] = `
-<div
- style={
- {
- "height": "48px",
- }
- }
->
- <div
- className="navbar global-navbar"
- id="global-navigation"
- >
- <div
- className="global-navbar-inner"
- >
- <withAppStateContext(GlobalNavBranding) />
- <withAppStateContext(GlobalNavMenu)
- currentUser={
- {
- "isLoggedIn": true,
- }
- }
- location={
- {
- "pathname": "",
- }
- }
- />
- <div
- className="global-navbar-menu global-navbar-menu-right"
- >
- <EmbedDocsPopupHelper />
- <withRouter(Search) />
- <withRouter(GlobalNavUser)
- currentUser={
- {
- "isLoggedIn": true,
- }
- }
- />
- </div>
- </div>
- </div>
-</div>
-`;
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`] = `
-<ForwardRef(Link)
- className="navbar-brand"
- to="/"
->
- <img
- alt="layout.nav.home_logo_alt"
- height={30}
- src="/images/logo.svg?v=6.6"
- title="layout.nav.home_logo_alt"
- width={83}
- />
-</ForwardRef(Link)>
-`;
-
-exports[`should render correctly: with logo 1`] = `
-<ForwardRef(Link)
- className="navbar-brand"
- to="/"
->
- <img
- alt="layout.nav.home_logo_alt"
- height={30}
- src="http://sonarsource.com/custom-logo.svg"
- title="layout.nav.home_logo_alt"
- width={100}
- />
-</ForwardRef(Link)>
-`;
-
-exports[`should render correctly: with logo and width 1`] = `
-<ForwardRef(Link)
- className="navbar-brand"
- to="/"
->
- <img
- alt="layout.nav.home_logo_alt"
- height={30}
- src="http://sonarsource.com/custom-logo.svg"
- title="layout.nav.home_logo_alt"
- width="200"
- />
-</ForwardRef(Link)>
-`;
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<Props> {
- doSelect = () => {
- this.props.onSelect(this.props.component.key);
- };
-
- render() {
- const { component } = this.props;
-
- const to = getComponentOverviewUrl(component.key, component.qualifier);
-
- return (
- <li key={component.key} ref={(node) => this.props.innerRef(component.key, node)}>
- <Link
- className={this.props.selected ? 'hover' : undefined}
- data-key={component.key}
- onClick={this.props.onClose}
- onFocus={this.doSelect}
- to={to}
- >
- <div className="navbar-search-item-link little-padded-top" onMouseEnter={this.doSelect}>
- <div className="display-flex-center">
- <span className="navbar-search-item-icons little-spacer-right">
- {component.isFavorite && <FavoriteIcon favorite={true} size={12} />}
- {!component.isFavorite && component.isRecentlyBrowsed && <ClockIcon size={12} />}
- <QualifierIcon className="little-spacer-right" qualifier={component.qualifier} />
- </span>
-
- {component.match ? (
- <span
- className="navbar-search-item-match"
- // Safe: comes from the search engine, that injects bold tags into component names
- // eslint-disable-next-line react/no-danger
- dangerouslySetInnerHTML={{ __html: component.match }}
- />
- ) : (
- <span className="navbar-search-item-match">{component.name}</span>
- )}
- </div>
-
- <div className="navbar-search-item-right text-muted-2">{component.key}</div>
- </div>
- </Link>
- </li>
- );
- }
-}
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<Search['props']> = {}) {
- return shallow<Search>(<Search router={mockRouter()} {...props} />);
-}
-
-function component(key: string, qualifier = ComponentQualifier.Project) {
- return { key, name: key, qualifier };
-}
-
-function next(form: ShallowWrapper<Search['props'], Search['state']>, expected: string) {
- keydown({ key: KeyboardKeys.DownArrow });
- expect(form.state().selected).toBe(expected);
-}
-
-function prev(form: ShallowWrapper<Search['props'], Search['state']>, expected: string) {
- keydown({ key: KeyboardKeys.UpArrow });
- expect(form.state().selected).toBe(expected);
-}
-
-function select(form: ShallowWrapper<Search['props'], Search['state']>, 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: 'f<mark>o</mark>o',
- 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<SearchResult['props']> = {}) {
- return shallow(
- <SearchResult
- component={{ key: 'foo', name: 'foo', qualifier: ComponentQualifier.Project }}
- innerRef={jest.fn()}
- onClose={jest.fn()}
- onSelect={jest.fn()}
- selected={false}
- {...props}
- />
- );
-}
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(
- <SearchResults
- allowMore={true}
- more={{}}
- onMoreClick={jest.fn()}
- onSelect={jest.fn()}
- renderNoResults={() => <div />}
- renderResult={(component) => <span key={component.key}>{component.name}</span>}
- results={{
- TRK: [component('foo'), component('bar')],
- FIL: [component('zux', 'FIL')],
- }}
- />
- )
- ).toMatchSnapshot();
-});
-
-it('renders "Show More" link', () => {
- expect(
- shallow(
- <SearchResults
- allowMore={true}
- more={{ TRK: 175 }}
- onMoreClick={jest.fn()}
- onSelect={jest.fn()}
- renderNoResults={() => <div />}
- renderResult={(component) => <span key={component.key}>{component.name}</span>}
- results={{
- TRK: [component('foo'), component('bar')],
- }}
- />
- )
- ).toMatchSnapshot();
-});
-
-it('should render no results', () => {
- // eslint-disable-next-line react/display-name
- expect(shallowRender({ renderNoResults: () => <div id="no-results" /> })).toMatchSnapshot();
-});
-
-function component(key: string, qualifier = 'TRK') {
- return { key, name: key, qualifier };
-}
-
-function shallowRender(props: Partial<Props> = {}) {
- return shallow(
- <SearchResults
- allowMore={true}
- more={{}}
- onMoreClick={jest.fn()}
- onSelect={jest.fn()}
- renderNoResults={() => <div />}
- renderResult={() => <div />}
- 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<SearchShowMore['props']> = {}) {
- return shallow(
- <SearchShowMore
- allowMore={true}
- onMoreClick={jest.fn()}
- onSelect={jest.fn()}
- qualifier="TRK"
- selected={false}
- {...props}
- />
- );
-}
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`] = `
-<div
- aria-live="assertive"
- className="navbar-search-no-results"
->
- no_results_for_x.
-</div>
-`;
-
-exports[`shows warning about short input 1`] = `
-<span
- aria-live="assertive"
- className="navbar-search-input-hint"
->
- select2.tooShort.2
-</span>
-`;
-
-exports[`shows warning about short input 2`] = `
-<span
- aria-live="assertive"
- className="navbar-search-input-hint"
->
- select2.tooShort.2
-</span>
-`;
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`] = `
-<li
- key="foo"
->
- <ForwardRef(Link)
- data-key="foo"
- onClick={[MockFunction]}
- onFocus={[Function]}
- to={
- {
- "pathname": "/dashboard",
- "search": "?id=foo",
- }
- }
- >
- <div
- className="navbar-search-item-link little-padded-top"
- onMouseEnter={[Function]}
- >
- <div
- className="display-flex-center"
- >
- <span
- className="navbar-search-item-icons little-spacer-right"
- >
- <FavoriteIcon
- favorite={true}
- size={12}
- />
- <QualifierIcon
- className="little-spacer-right"
- qualifier="TRK"
- />
- </span>
- <span
- className="navbar-search-item-match"
- >
- foo
- </span>
- </div>
- <div
- className="navbar-search-item-right text-muted-2"
- >
- foo
- </div>
- </div>
- </ForwardRef(Link)>
-</li>
-`;
-
-exports[`renders match 1`] = `
-<li
- key="foo"
->
- <ForwardRef(Link)
- data-key="foo"
- onClick={[MockFunction]}
- onFocus={[Function]}
- to={
- {
- "pathname": "/dashboard",
- "search": "?id=foo",
- }
- }
- >
- <div
- className="navbar-search-item-link little-padded-top"
- onMouseEnter={[Function]}
- >
- <div
- className="display-flex-center"
- >
- <span
- className="navbar-search-item-icons little-spacer-right"
- >
- <QualifierIcon
- className="little-spacer-right"
- qualifier="TRK"
- />
- </span>
- <span
- className="navbar-search-item-match"
- dangerouslySetInnerHTML={
- {
- "__html": "f<mark>o</mark>o",
- }
- }
- />
- </div>
- <div
- className="navbar-search-item-right text-muted-2"
- >
- foo
- </div>
- </div>
- </ForwardRef(Link)>
-</li>
-`;
-
-exports[`renders recently browsed 1`] = `
-<li
- key="foo"
->
- <ForwardRef(Link)
- data-key="foo"
- onClick={[MockFunction]}
- onFocus={[Function]}
- to={
- {
- "pathname": "/dashboard",
- "search": "?id=foo",
- }
- }
- >
- <div
- className="navbar-search-item-link little-padded-top"
- onMouseEnter={[Function]}
- >
- <div
- className="display-flex-center"
- >
- <span
- className="navbar-search-item-icons little-spacer-right"
- >
- <ClockIcon
- size={12}
- />
- <QualifierIcon
- className="little-spacer-right"
- qualifier="TRK"
- />
- </span>
- <span
- className="navbar-search-item-match"
- >
- foo
- </span>
- </div>
- <div
- className="navbar-search-item-right text-muted-2"
- >
- foo
- </div>
- </div>
- </ForwardRef(Link)>
-</li>
-`;
-
-exports[`renders selected 1`] = `
-<li
- key="foo"
->
- <ForwardRef(Link)
- data-key="foo"
- onClick={[MockFunction]}
- onFocus={[Function]}
- to={
- {
- "pathname": "/dashboard",
- "search": "?id=foo",
- }
- }
- >
- <div
- className="navbar-search-item-link little-padded-top"
- onMouseEnter={[Function]}
- >
- <div
- className="display-flex-center"
- >
- <span
- className="navbar-search-item-icons little-spacer-right"
- >
- <QualifierIcon
- className="little-spacer-right"
- qualifier="TRK"
- />
- </span>
- <span
- className="navbar-search-item-match"
- >
- foo
- </span>
- </div>
- <div
- className="navbar-search-item-right text-muted-2"
- >
- foo
- </div>
- </div>
- </ForwardRef(Link)>
-</li>
-`;
-
-exports[`renders selected 2`] = `
-<li
- key="foo"
->
- <ForwardRef(Link)
- className="hover"
- data-key="foo"
- onClick={[MockFunction]}
- onFocus={[Function]}
- to={
- {
- "pathname": "/dashboard",
- "search": "?id=foo",
- }
- }
- >
- <div
- className="navbar-search-item-link little-padded-top"
- onMouseEnter={[Function]}
- >
- <div
- className="display-flex-center"
- >
- <span
- className="navbar-search-item-icons little-spacer-right"
- >
- <QualifierIcon
- className="little-spacer-right"
- qualifier="TRK"
- />
- </span>
- <span
- className="navbar-search-item-match"
- >
- foo
- </span>
- </div>
- <div
- className="navbar-search-item-right text-muted-2"
- >
- foo
- </div>
- </div>
- </ForwardRef(Link)>
-</li>
-`;
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`] = `
-<div>
- <h2
- className="menu-header no-margin"
- id="qualifiers.TRK"
- >
- qualifiers.TRK
- </h2>
- <ul
- aria-labelledby="qualifiers.TRK"
- className="menu"
- key="header-TRK"
- >
- <span
- key="foo"
- >
- foo
- </span>
- <span
- key="bar"
- >
- bar
- </span>
- <SearchShowMore
- allowMore={true}
- key="more-TRK"
- onMoreClick={[MockFunction]}
- onSelect={[MockFunction]}
- qualifier="TRK"
- selected={false}
- />
- </ul>
-</div>
-`;
-
-exports[`renders different components and dividers between them 1`] = `
-<div>
- <h2
- className="menu-header no-margin"
- id="qualifiers.FIL"
- >
- qualifiers.FIL
- </h2>
- <ul
- aria-labelledby="qualifiers.FIL"
- className="menu"
- key="header-FIL"
- >
- <span
- key="zux"
- >
- zux
- </span>
- </ul>
- <h2
- className="menu-header no-margin"
- id="qualifiers.TRK"
- >
- qualifiers.TRK
- </h2>
- <ul
- aria-labelledby="qualifiers.TRK"
- className="menu"
- key="header-TRK"
- >
- <span
- key="foo"
- >
- foo
- </span>
- <span
- key="bar"
- >
- bar
- </span>
- </ul>
-</div>
-`;
-
-exports[`should render no results 1`] = `
-<div
- id="no-results"
-/>
-`;
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`] = `
-<li
- className="menu-footer"
- key="more-TRK"
->
- <DeferredSpinner
- className="navbar-search-icon"
- loading={false}
- >
- <a
- data-qualifier="TRK"
- href="#"
- onClick={[Function]}
- onMouseEnter={[Function]}
- >
- <div
- className="pull-right text-muted-2 menu-footer-note"
- >
- <FormattedMessage
- defaultMessage="search.show_more.hint"
- id="search.show_more.hint"
- values={
- {
- "key": <span
- className="shortcut-button shortcut-button-small"
- >
- Enter
- </span>,
- }
- }
- />
- </div>
- <span>
- show_more
- </span>
- </a>
- </DeferredSpinner>
-</li>
-`;
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(
<AvailableFeaturesContext.Provider value={availableFeatures ?? DEFAULT_AVAILABLE_FEATURES}>
<CurrentUserContextProvider currentUser={currentUser}>
<IntlProvider defaultLocale={lang} locale={lang}>
- <GlobalMessagesContainer />
- <BrowserRouter basename={getBaseUrl()}>
- <Helmet titleTemplate={translate('page_title.template.default')} />
- <Routes>
- {renderRedirects()}
+ <ThemeProvider theme={lightTheme}>
+ <GlobalMessagesContainer />
+ <BrowserRouter basename={getBaseUrl()}>
+ <Helmet titleTemplate={translate('page_title.template.default')} />
+ <Routes>
+ {renderRedirects()}
- <Route path="formatting/help" element={<FormattingHelp />} />
+ <Route path="formatting/help" element={<FormattingHelp />} />
- <Route element={<SimpleContainer />}>{maintenanceRoutes()}</Route>
+ <Route element={<SimpleContainer />}>{maintenanceRoutes()}</Route>
- <Route element={<MigrationContainer />}>
- {sessionsRoutes()}
+ <Route element={<MigrationContainer />}>
+ {sessionsRoutes()}
- <Route path="/" element={<App />}>
- <Route index={true} element={<Landing />} />
+ <Route path="/" element={<App />}>
+ <Route index={true} element={<Landing />} />
- <Route element={<GlobalContainer />}>
- {accountRoutes()}
+ <Route element={<GlobalContainer />}>
+ {accountRoutes()}
- {codingRulesRoutes()}
+ {codingRulesRoutes()}
- <Route
- path="extension/:pluginKey/:extensionKey"
- element={<GlobalPageExtension />}
- />
+ <Route
+ path="extension/:pluginKey/:extensionKey"
+ element={<GlobalPageExtension />}
+ />
- {globalIssuesRoutes()}
+ {globalIssuesRoutes()}
- {projectsRoutes()}
+ {projectsRoutes()}
- {qualityGatesRoutes()}
- {qualityProfilesRoutes()}
+ {qualityGatesRoutes()}
+ {qualityProfilesRoutes()}
- <Route path="portfolios" element={<PortfoliosPage />} />
+ <Route path="portfolios" element={<PortfoliosPage />} />
- <Route path="sonarlint/auth" element={<SonarLintConnection />} />
+ <Route path="sonarlint/auth" element={<SonarLintConnection />} />
- {webAPIRoutes()}
+ {webAPIRoutes()}
- {renderComponentRoutes()}
+ {renderComponentRoutes()}
- {renderAdminRoutes()}
- </Route>
- <Route
- // We don't want this route to have any menu.
- // That is why we can not have it under the accountRoutes
- path="account/reset_password"
- element={<ResetPassword />}
- />
+ {renderAdminRoutes()}
+ </Route>
+ <Route
+ // We don't want this route to have any menu.
+ // That is why we can not have it under the accountRoutes
+ path="account/reset_password"
+ element={<ResetPassword />}
+ />
- <Route
- // We don't want this route to have any menu. This is why we define it here
- // rather than under the admin routes.
- path="admin/change_admin_password"
- element={<ChangeAdminPasswordApp />}
- />
+ <Route
+ // We don't want this route to have any menu. This is why we define it here
+ // rather than under the admin routes.
+ path="admin/change_admin_password"
+ element={<ChangeAdminPasswordApp />}
+ />
- <Route
- // We don't want this route to have any menu. This is why we define it here
- // rather than under the admin routes.
- path="admin/plugin_risk_consent"
- element={<PluginRiskConsent />}
- />
- <Route path="not_found" element={<NotFound />} />
- <Route path="*" element={<NotFound />} />
+ <Route
+ // We don't want this route to have any menu. This is why we define it here
+ // rather than under the admin routes.
+ path="admin/plugin_risk_consent"
+ element={<PluginRiskConsent />}
+ />
+ <Route path="not_found" element={<NotFound />} />
+ <Route path="*" element={<NotFound />} />
+ </Route>
</Route>
- </Route>
- </Routes>
- </BrowserRouter>
+ </Routes>
+ </BrowserRouter>
+ </ThemeProvider>
</IntlProvider>
</CurrentUserContextProvider>
</AvailableFeaturesContext.Provider>
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<HTMLAnchorElement>;
+ children: React.ReactNode;
+}
+
+export function DocItemLink({ to, innerRef, children }: Props) {
+ const { version } = React.useContext(AppStateContext);
+
+ const toStatic = getUrlForDoc(version, to);
+
+ return (
+ <ItemLink innerRef={innerRef} to={toStatic}>
+ <OpenNewTabIcon />
+ {children}
+ </ItemLink>
+ );
+}
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 (
+ <ItemLink to={link}>
+ <img
+ alt={text}
+ aria-hidden={true}
+ className="spacer-right"
+ height="18"
+ src={`${getBaseUrl()}/images/${icon}`}
+ width="18"
+ />
+ {text}
+ </ItemLink>
+ );
}
-export default class EmbedDocsPopup extends React.PureComponent<Props> {
- 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<HTMLAnchorElement> = (node: HTMLAnchorElement | null) => {
- if (node && !this.firstItem) {
- this.firstItem = node;
- this.firstItem.focus();
- }
- };
-
- renderTitle(text: string, labelId: string) {
- return (
- <h2 className="menu-header" id={labelId}>
- {text}
- </h2>
- );
- }
+function Suggestions({
+ firstItemRef,
+ suggestions,
+}: {
+ firstItemRef: React.RefObject<HTMLAnchorElement>;
+ suggestions: SuggestionLink[];
+}) {
+ return (
+ <>
+ <ItemHeader id="suggestion">{translate('docs.suggestion')}</ItemHeader>
+ {suggestions.map((suggestion, i) => (
+ <DocItemLink
+ innerRef={i === 0 ? firstItemRef : undefined}
+ key={suggestion.link}
+ to={suggestion.link}
+ >
+ {suggestion.text}
+ </DocItemLink>
+ ))}
+ <ItemDivider />
+ </>
+ );
+}
- renderSuggestions = ({ suggestions }: { suggestions: SuggestionLink[] }) => {
- if (suggestions.length === 0) {
- return null;
- }
- return (
- <>
- {this.renderTitle(translate('docs.suggestion'), 'suggestion')}
- <ul className="menu abs-width-240" aria-labelledby="suggestion">
- {suggestions.map((suggestion, i) => (
- <li key={suggestion.link}>
- <DocLink
- innerRef={i === 0 ? this.focusFirstItem : undefined}
- onClick={this.props.onClose}
- to={suggestion.link}
- >
- {suggestion.text}
- </DocLink>
- </li>
- ))}
- </ul>
- </>
- );
- };
+export function EmbedDocsPopup() {
+ const firstItemRef = React.useRef<HTMLAnchorElement>(null);
+ const { suggestions } = React.useContext(SuggestionsContext);
- renderIconLink(link: string, icon: string, text: string) {
- return (
- <a href={link} rel="noopener noreferrer" target="_blank">
- <img
- alt={text}
- aria-hidden={true}
- className="spacer-right"
- height="18"
- src={`${getBaseUrl()}/images/${icon}`}
- width="18"
- />
- {text}
- </a>
- );
- }
+ React.useEffect(() => {
+ firstItemRef.current?.focus();
+ }, []);
- render() {
- return (
- <DropdownOverlay>
- <SuggestionsContext.Consumer>{this.renderSuggestions}</SuggestionsContext.Consumer>
- <ul className="menu abs-width-240">
- <li>
- <DocLink innerRef={this.focusFirstItem} onClick={this.props.onClose} to="/">
- {translate('docs.documentation')}
- </DocLink>
- </li>
- <li>
- <Link onClick={this.props.onClose} to="/web_api">
- {translate('api_documentation.page')}
- </Link>
- </li>
- </ul>
- <ul className="menu abs-width-240">
- <li>
- <Link
- className="display-flex-center"
- to="https://community.sonarsource.com/"
- target="_blank"
- >
- {translate('docs.get_help')}
- </Link>
- </li>
- </ul>
- {this.renderTitle(translate('docs.stay_connected'), 'stay_connected')}
- <ul className="menu abs-width-240" aria-labelledby="stay_connected">
- <li>
- {this.renderIconLink(
- 'https://www.sonarqube.org/whats-new/?referrer=sonarqube',
- 'embed-doc/sq-icon.svg',
- translate('docs.news')
- )}
- </li>
- <li>
- {this.renderIconLink(
- 'https://www.sonarqube.org/roadmap/?referrer=sonarqube',
- 'embed-doc/sq-icon.svg',
- translate('docs.roadmap')
- )}
- </li>
- <li>
- {this.renderIconLink(
- 'https://twitter.com/SonarQube',
- 'embed-doc/twitter-icon.svg',
- 'Twitter'
- )}
- </li>
- </ul>
- </DropdownOverlay>
- );
- }
+ return (
+ <>
+ {suggestions.length !== 0 && (
+ <Suggestions firstItemRef={firstItemRef} suggestions={suggestions} />
+ )}
+ <DocItemLink innerRef={suggestions.length === 0 ? firstItemRef : undefined} to="/">
+ {translate('docs.documentation')}
+ </DocItemLink>
+ <ItemLink to="/web_api">{translate('api_documentation.page')}</ItemLink>
+ <ItemDivider />
+ <DocItemLink to="https://community.sonarsource.com/">
+ {translate('docs.get_help')}
+ </DocItemLink>
+ <ItemDivider />
+ <ItemHeader id="stay_connected">{translate('docs.stay_connected')}</ItemHeader>
+ <IconLink
+ link="https://www.sonarqube.org/whats-new/?referrer=sonarqube"
+ text={translate('docs.news')}
+ />
+ <IconLink
+ link="https://www.sonarqube.org/roadmap/?referrer=sonarqube"
+ text={translate('docs.roadmap')}
+ />
+ <IconLink
+ icon="embed-doc/twitter-icon.svg"
+ link="https://twitter.com/SonarQube"
+ text="Twitter"
+ />
+ </>
+ );
}
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 (
- <div className="dropdown">
- <Toggler
- onRequestClose={this.closeHelp}
- open={this.state.helpOpen}
- overlay={<EmbedDocsPopup onClose={this.closeHelp} />}
- >
- <ButtonLink
- aria-expanded={this.state.helpOpen}
- aria-haspopup={true}
- className="navbar-help navbar-icon"
- onClick={this.handleClick}
- title={translate('help')}
+export default function EmbedDocsPopupHelper() {
+ return (
+ <div className="dropdown">
+ <Dropdown
+ id="help-menu-dropdown"
+ placement={PopupPlacement.BottomRight}
+ overlay={<EmbedDocsPopup />}
+ allowResizing={true}
+ zLevel={PopupZLevel.Global}
+ >
+ {({ onToggleClick, open }) => (
+ <Tooltip
+ mouseLeaveDelay={0.2}
+ overlay={translate('help')}
+ visible={open ? false : undefined}
>
- <HelpIcon />
- </ButtonLink>
- </Toggler>
- </div>
- );
- }
+ <InteractiveIcon
+ Icon={MenuHelpIcon}
+ aria-expanded={open}
+ aria-controls="help-menu-dropdown"
+ aria-haspopup={true}
+ aria-label={translate('help')}
+ currentColor={true}
+ onClick={onToggleClick}
+ size="medium"
+ stopPropagation={false}
+ />
+ </Tooltip>
+ )}
+ </Dropdown>
+ </div>
+ );
}
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(
- <SuggestionsContext.Provider
- value={{ addSuggestions: jest.fn(), removeSuggestions: jest.fn(), suggestions }}
- >
- <EmbedDocsPopup onClose={jest.fn()} />
- </SuggestionsContext.Provider>
- );
+function renderEmbedDocsPopup() {
+ function Test() {
+ const [suggestions, setSuggestions] = React.useState<string[]>(['account']);
+
+ const addSuggestion = () => {
+ setSuggestions([...suggestions, 'background_tasks']);
+ };
+
+ return (
+ <SuggestionsProvider>
+ <button onClick={addSuggestion} type="button">
+ add.suggestion
+ </button>
+ <button onClick={() => setSuggestions([])} type="button">
+ remove.suggestion
+ </button>
+ <EmbedDocsPopupHelper />
+ {suggestions.map((suggestion) => (
+ <Suggestions key={suggestion} suggestions={suggestion} />
+ ))}
+ </SuggestionsProvider>
+ );
+ }
+
+ return renderComponent(<Test />);
}
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<SuggestionsProvider>(
- <SuggestionsProvider>
- <div />
- </SuggestionsProvider>
- );
- 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,6 +821,219 @@ __metadata:
languageName: node
linkType: hard
+"@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"
@@ -657,7 +1056,7 @@ __metadata:
languageName: node
linkType: hard
-"@babel/plugin-syntax-class-properties@npm:^7.8.3":
+"@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:
@@ -668,6 +1067,50 @@ __metadata:
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"
@@ -701,7 +1144,7 @@ __metadata:
languageName: node
linkType: hard
-"@babel/plugin-syntax-logical-assignment-operators@npm:^7.8.3":
+"@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:
@@ -723,7 +1166,7 @@ __metadata:
languageName: node
linkType: hard
-"@babel/plugin-syntax-numeric-separator@npm:^7.8.3":
+"@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:
@@ -767,7 +1210,18 @@ __metadata:
languageName: node
linkType: hard
-"@babel/plugin-syntax-top-level-await@npm:^7.8.3":
+"@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:
@@ -778,6 +1232,17 @@ __metadata:
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"
@@ -789,6 +1254,283 @@ __metadata:
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"
@@ -826,6 +1568,241 @@ __metadata:
languageName: node
linkType: hard
+"@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.20.2
+ regenerator-transform: ^0.15.1
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ checksum: 13164861e71fb23d84c6270ef5330b03c54d5d661c2c7468f28e21c4f8598558ca0c8c3cb1d996219352946e849d270a61372bc93c8fbe9676e78e3ffd0dea07
+ languageName: node
+ linkType: hard
+
+"@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.18.6
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ checksum: 0738cdc30abdae07c8ec4b233b30c31f68b3ff0eaa40eddb45ae607c066127f5fa99ddad3c0177d8e2832e3a7d3ad115775c62b431ebd6189c40a951b867a80c
+ languageName: node
+ linkType: hard
+
+"@babel/plugin-transform-shorthand-properties@npm:^7.18.6":
+ version: 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: b8e4e8acc2700d1e0d7d5dbfd4fdfb935651913de6be36e6afb7e739d8f9ca539a5150075a0f9b79c88be25ddf45abb912fe7abf525f0b80f5b9d9860de685d7
+ languageName: node
+ linkType: hard
+
+"@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.20.2
+ "@babel/helper-skip-transparent-expression-wrappers": ^7.20.0
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ checksum: 8ea698a12da15718aac7489d4cde10beb8a3eea1f66167d11ab1e625033641e8b328157fd1a0b55dd6531933a160c01fc2e2e61132a385cece05f26429fd0cc2
+ languageName: node
+ linkType: hard
+
+"@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.18.6
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ checksum: 68ea18884ae9723443ffa975eb736c8c0d751265859cd3955691253f7fee37d7a0f7efea96c8a062876af49a257a18ea0ed5fea0d95a7b3611ce40f7ee23aee3
+ languageName: node
+ linkType: hard
+
+"@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.18.9
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ checksum: 3d2fcd79b7c345917f69b92a85bdc3ddd68ce2c87dc70c7d61a8373546ccd1f5cb8adc8540b49dfba08e1b82bb7b3bbe23a19efdb2b9c994db2db42906ca9fb2
+ languageName: node
+ linkType: hard
+
+"@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.18.9
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ checksum: e754e0d8b8a028c52e10c148088606e3f7a9942c57bd648fc0438e5b4868db73c386a5ed47ab6d6f0594aae29ee5ffc2ffc0f7ebee7fae560a066d6dea811cd4
+ languageName: node
+ linkType: hard
+
+"@babel/plugin-transform-typescript@npm:^7.18.6":
+ version: 7.21.0
+ resolution: "@babel/plugin-transform-typescript@npm:7.21.0"
+ dependencies:
+ "@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: 091931118eb515738a4bc8245875f985fc9759d3f85cdf08ee641779b41520241b369404e2bb86fc81907ad827678fdb704e8e5a995352def5dd3051ea2cd870
+ languageName: node
+ linkType: hard
+
+"@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.18.9
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ checksum: f5baca55cb3c11bc08ec589f5f522d85c1ab509b4d11492437e45027d64ae0b22f0907bd1381e8d7f2a436384bb1f9ad89d19277314242c5c2671a0f91d0f9cd
+ languageName: node
+ linkType: hard
+
+"@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-create-regexp-features-plugin": ^7.18.6
+ "@babel/helper-plugin-utils": ^7.18.6
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ checksum: d9e18d57536a2d317fb0b7c04f8f55347f3cfacb75e636b4c6fa2080ab13a3542771b5120e726b598b815891fc606d1472ac02b749c69fd527b03847f22dc25e
+ languageName: node
+ linkType: hard
+
+"@babel/preset-env@npm:7.20.2":
+ version: 7.20.2
+ resolution: "@babel/preset-env@npm:7.20.2"
+ dependencies:
+ "@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: ece2d7e9c7789db6116e962b8e1a55eb55c110c44c217f0c8f6ffea4ca234954e66557f7bd019b7affadf7fbb3a53ccc807e93fc935aacd48146234b73b6947e
+ languageName: node
+ linkType: hard
+
+"@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.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: 8430e0e9e9d520b53e22e8c4c6a5a080a12b63af6eabe559c2310b187bd62ae113f3da82ba33e9d1d0f3230930ca702843aae9dd226dec51f7d7114dc1f51c10
+ languageName: node
+ linkType: hard
+
+"@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.18.6
+ "@babel/helper-validator-option": ^7.18.6
+ "@babel/plugin-transform-typescript": ^7.18.6
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ checksum: 7fe0da5103eb72d3cf39cf3e138a794c8cdd19c0b38e3e101507eef519c46a87a0d6d0e8bc9e28a13ea2364001ebe7430b9d75758aab4c3c3a8db9a487b9dc7c
+ languageName: node
+ linkType: hard
+
+"@babel/regjsgen@npm:^0.8.0":
+ version: 0.8.0
+ resolution: "@babel/regjsgen@npm:0.8.0"
+ checksum: 89c338fee774770e5a487382170711014d49a68eb281e74f2b5eac88f38300a4ad545516a7786a8dd5702e9cf009c94c2f582d200f077ac5decd74c56b973730
+ languageName: node
+ linkType: hard
+
"@babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.8.7":
version: 7.16.3
resolution: "@babel/runtime@npm:7.16.3"
@@ -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<compat/resolve>, resolve@patch:resolve@^1.19.0#~builtin<compat/resolve>, resolve@patch:resolve@^1.22.1#~builtin<compat/resolve>, resolve@patch:resolve@~1.22.1#~builtin<compat/resolve>":
+"resolve@patch:resolve@^1.12.0#~builtin<compat/resolve>, resolve@patch:resolve@^1.14.2#~builtin<compat/resolve>, resolve@patch:resolve@^1.19.0#~builtin<compat/resolve>, resolve@patch:resolve@^1.22.1#~builtin<compat/resolve>, resolve@patch:resolve@~1.22.1#~builtin<compat/resolve>":
version: 1.22.1
resolution: "resolve@patch:resolve@npm%3A1.22.1#~builtin<compat/resolve>::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