From 814e30e422183726861cb4342c8e67d28ff9015d Mon Sep 17 00:00:00 2001 From: Kevin Silva Date: Wed, 17 May 2023 12:17:17 +0200 Subject: [PATCH] SONAR-19173 - Create a "code snippet" component design-system --- server/sonar-web/design-system/package.json | 1 + .../src/components/CodeSnippet.tsx | 120 +++++ .../src/components/Highlighter.tsx | 182 +++++++ .../components/__tests__/CodeSnippet-test.tsx | 46 ++ .../components/__tests__/Highlighter-test.tsx | 50 ++ .../__snapshots__/CodeSnippet-test.tsx.snap | 471 ++++++++++++++++++ .../__snapshots__/Highlighter-test.tsx.snap | 463 +++++++++++++++++ .../src/components/icons/PencilIcon.tsx | 23 + .../src/components/icons/index.ts | 1 + .../design-system/src/components/index.ts | 1 + .../design-system/src/theme/light.ts | 20 + server/sonar-web/yarn.lock | 8 + 12 files changed, 1386 insertions(+) create mode 100644 server/sonar-web/design-system/src/components/CodeSnippet.tsx create mode 100644 server/sonar-web/design-system/src/components/Highlighter.tsx create mode 100644 server/sonar-web/design-system/src/components/__tests__/CodeSnippet-test.tsx create mode 100644 server/sonar-web/design-system/src/components/__tests__/Highlighter-test.tsx create mode 100644 server/sonar-web/design-system/src/components/__tests__/__snapshots__/CodeSnippet-test.tsx.snap create mode 100644 server/sonar-web/design-system/src/components/__tests__/__snapshots__/Highlighter-test.tsx.snap create mode 100644 server/sonar-web/design-system/src/components/icons/PencilIcon.tsx diff --git a/server/sonar-web/design-system/package.json b/server/sonar-web/design-system/package.json index 45633e20409..ff65cf5ab5b 100644 --- a/server/sonar-web/design-system/package.json +++ b/server/sonar-web/design-system/package.json @@ -34,6 +34,7 @@ "eslint-plugin-import": "2.27.5", "eslint-plugin-local-rules": "1.3.2", "eslint-plugin-typescript-sort-keys": "2.3.0", + "highlight.js": "11.7.0", "history": "5.3.0", "jest": "29.5.0", "postcss": "8.4.21", diff --git a/server/sonar-web/design-system/src/components/CodeSnippet.tsx b/server/sonar-web/design-system/src/components/CodeSnippet.tsx new file mode 100644 index 00000000000..e1fed4f8808 --- /dev/null +++ b/server/sonar-web/design-system/src/components/CodeSnippet.tsx @@ -0,0 +1,120 @@ +/* + * 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 tw from 'twin.macro'; +import { themeBorder, themeColor } from '../helpers/theme'; +import { isDefined } from '../helpers/types'; +import { Highlighter, RegisteredLanguages } from './Highlighter'; +import { ClipboardButton } from './clipboard'; + +interface Props { + className?: string; + highlight?: boolean; + isOneLine?: boolean; + join?: string; + language?: RegisteredLanguages; + noCopy?: boolean; + render?: string; + snippet: string | Array; + toggleEdit?: VoidFunction; + wrap?: boolean; +} + +// keep this "useless" concatenation for the readability reason +// eslint-disable-next-line no-useless-concat +const s = ' \\' + '\n '; + +export function CodeSnippet(props: Props) { + const { + className, + isOneLine, + highlight, + join = s, + language, + noCopy, + render, + snippet, + toggleEdit, + wrap, + } = props; + const snippetArray = Array.isArray(snippet) ? snippet.filter(isDefined) : [snippet]; + const finalSnippet = isOneLine ? snippetArray.join(' ') : snippetArray.join(join); + + const isSimpleOneLine = isOneLine && noCopy; + + const copyButton = isOneLine ? ( + + ) : ( + + ); + + return ( + + {!noCopy && copyButton} + + + ); +} + +const Wrapper = styled.div` + background-color: ${themeColor('codeSnippetBackground')}; + border: ${themeBorder('default', 'codeSnippetBorder')}; + + ${tw`sw-rounded-2`} + ${tw`sw-relative`} + ${tw`sw-my-2`} + + &.code-snippet-simple-oneline { + ${tw`sw-my-0`} + ${tw`sw-rounded-1`} + } +`; + +const StyledClipboardButton = styled(ClipboardButton)` + ${tw`sw-select-none`} + ${tw`sw-body-sm`} + ${tw`sw-top-6 sw-right-6`} + ${tw`sw-absolute`} + + .code-snippet-highlighted-oneline & { + ${tw`sw-bottom-2`} + } +`; + +const StyledSingleLineClipboardButton = styled(StyledClipboardButton)` + ${tw`sw-top-6 sw-bottom-6`} +`; diff --git a/server/sonar-web/design-system/src/components/Highlighter.tsx b/server/sonar-web/design-system/src/components/Highlighter.tsx new file mode 100644 index 00000000000..73690313334 --- /dev/null +++ b/server/sonar-web/design-system/src/components/Highlighter.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 styled from '@emotion/styled'; +import classNames from 'classnames'; +import hljs from 'highlight.js/lib/core'; +import bash from 'highlight.js/lib/languages/bash'; +import gradle from 'highlight.js/lib/languages/gradle'; +import plaintext from 'highlight.js/lib/languages/plaintext'; +import powershell from 'highlight.js/lib/languages/powershell'; +import properties from 'highlight.js/lib/languages/properties'; +import shell from 'highlight.js/lib/languages/shell'; +import xml from 'highlight.js/lib/languages/xml'; +import yaml from 'highlight.js/lib/languages/yaml'; +import { useMemo } from 'react'; +import tw from 'twin.macro'; +import { translate } from '../helpers/l10n'; +import { themeColor, themeContrast } from '../helpers/theme'; +import { InteractiveIcon } from './InteractiveIcon'; +import { PencilIcon } from './icons'; + +hljs.registerLanguage('yaml', yaml); +hljs.registerLanguage('gradle', gradle); +hljs.registerLanguage('properties', properties); +hljs.registerLanguage('xml', xml); +hljs.registerLanguage('bash', bash); +hljs.registerLanguage('powershell', powershell); +hljs.registerLanguage('shell', shell); +hljs.registerLanguage('plaintext', plaintext); + +hljs.addPlugin({ + 'after:highlight': (data) => { + data.value = data.value + .replace(/<mark>/g, '') + .replace(/<\/mark>/g, ''); + }, +}); + +export type RegisteredLanguages = + | 'bash' + | 'gradle' + | 'plaintext' + | 'powershell' + | 'properties' + | 'shell' + | 'xml' + | 'yaml'; + +interface Props { + className?: string; + code: string; + highlight?: boolean; + isSimpleOneLine?: boolean; + language?: RegisteredLanguages; + toggleEdit?: VoidFunction; + wrap?: boolean; +} + +export function Highlighter({ + className, + code, + highlight = true, + isSimpleOneLine = false, + language = 'yaml', + toggleEdit, + wrap, +}: Props) { + const highlighted = useMemo( + () => hljs.highlight(code, { language: highlight ? language : 'plaintext' }), + [code, highlight, language] + ); + + return ( + + + {toggleEdit && ( + + )} + + ); +} + +const StyledPre = styled.pre` + ${tw`sw-flex sw-items-center`} + ${tw`sw-overflow-x-auto`} + ${tw`sw-p-6`} + + code { + color: ${themeColor('codeSnippetBody')}; + background: ${themeColor('codeSnippetBackground')}; + ${tw`sw-code`}; + + &.hljs { + padding: unset; + } + } + + .hljs-variable, + .hljs-meta { + color: ${themeColor('codeSnippetBody')}; + } + + .hljs-doctag, + .hljs-title, + .hljs-title.class_, + .hljs-title.function_ { + color: ${themeColor('codeSnippetAnnotations')}; + } + + .hljs-comment { + color: ${themeColor('codeSnippetComments')}; + + ${tw`sw-code-comment`} + } + + .hljs-tag, + .hljs-type, + .hljs-keyword { + color: ${themeColor('codeSnippetKeyword')}; + + ${tw`sw-code-highlight`} + } + + .hljs-literal, + .hljs-number { + color: ${themeColor('codeSnippetConstants')}; + } + + .hljs-string { + color: ${themeColor('codeSnippetString')}; + } + + .hljs-meta .hljs-keyword { + color: ${themeColor('codeSnippetPreprocessingDirective')}; + } + + &.code-wrap { + ${tw`sw-whitespace-pre-wrap`} + ${tw`sw-break-all`} + } + + mark { + color: ${themeContrast('codeSnippetHighlight')}; + background-color: ${themeColor('codeSnippetHighlight')}; + ${tw`sw-font-regular`} + ${tw`sw-rounded-1`} + ${tw`sw-p-1`} + } + + &.simple-one-line { + ${tw`sw-min-h-[1.25rem]`} + ${tw`sw-py-0 sw-px-1`} + } +`; diff --git a/server/sonar-web/design-system/src/components/__tests__/CodeSnippet-test.tsx b/server/sonar-web/design-system/src/components/__tests__/CodeSnippet-test.tsx new file mode 100644 index 00000000000..41e98ce716d --- /dev/null +++ b/server/sonar-web/design-system/src/components/__tests__/CodeSnippet-test.tsx @@ -0,0 +1,46 @@ +/* + * 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 { HelmetProvider } from 'react-helmet-async'; +import { renderWithContext } from '../../helpers/testUtils'; +import { FCProps } from '../../types/misc'; +import { CodeSnippet } from '../CodeSnippet'; + +it('should show full size when multiline with no editting', () => { + const { container } = setupWithProps(); + const copyButton = screen.getByRole('button', { name: 'copy' }); + expect(copyButton).toHaveStyle('top: 1.5rem'); + expect(container).toMatchSnapshot(); +}); + +it('should show reduced size when single line with no editting', () => { + const { container } = setupWithProps({ isOneLine: true, snippet: 'foobar' }); + const copyButton = screen.getByRole('button', { name: 'copy' }); + expect(copyButton).toHaveStyle('top: 1.5rem'); + expect(container).toMatchSnapshot(); +}); + +function setupWithProps(props: Partial> = {}) { + return renderWithContext( + + + + ); +} diff --git a/server/sonar-web/design-system/src/components/__tests__/Highlighter-test.tsx b/server/sonar-web/design-system/src/components/__tests__/Highlighter-test.tsx new file mode 100644 index 00000000000..c02ca7f6e51 --- /dev/null +++ b/server/sonar-web/design-system/src/components/__tests__/Highlighter-test.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 { renderWithContext } from '../../helpers/testUtils'; +import { FCProps } from '../../types/misc'; +import { Highlighter } from '../Highlighter'; + +it('renders correctly', () => { + expect(setupWithProps().container).toMatchSnapshot(); +}); + +it('should handle multiple lines of code', () => { + expect( + setupWithProps({ + code: `foo: bar + pleh: help + stuff: + foo: bar + bar: foo`, + language: 'yaml', + }).container + ).toMatchSnapshot(); +}); + +it('should display edit functions', () => { + expect( + setupWithProps({ code: 'One line command', toggleEdit: jest.fn() }).container + ).toMatchSnapshot(); +}); + +function setupWithProps(props: Partial> = {}) { + return renderWithContext(); +} diff --git a/server/sonar-web/design-system/src/components/__tests__/__snapshots__/CodeSnippet-test.tsx.snap b/server/sonar-web/design-system/src/components/__tests__/__snapshots__/CodeSnippet-test.tsx.snap new file mode 100644 index 00000000000..715c4e4d72d --- /dev/null +++ b/server/sonar-web/design-system/src/components/__tests__/__snapshots__/CodeSnippet-test.tsx.snap @@ -0,0 +1,471 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should show full size when multiline with no editting 1`] = ` +.emotion-0 { + background-color: rgb(252,252,253); + border: 1px solid rgb(225,230,243); + border-radius: 0.5rem; + position: relative; + margin-top: 0.5rem; + margin-bottom: 0.5rem; +} + +.emotion-0.code-snippet-simple-oneline { + margin-top: 0; + margin-bottom: 0; + border-radius: 0.25rem; +} + +.emotion-4 { + box-sizing: border-box; + -webkit-text-decoration: none; + text-decoration: none; + outline: none; + border: var(--border); + color: var(--color); + background-color: var(--background); + -webkit-transition: background-color 0.2s ease,outline 0.2s ease; + transition: background-color 0.2s ease,outline 0.2s ease; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + height: 2.25rem; + font-family: Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 600; + padding-left: 1rem; + padding-right: 1rem; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + border-radius: 0.5rem; + cursor: pointer; + --background: rgb(255,255,255); + --backgroundHover: rgb(239,242,249); + --color: rgb(62,67,87); + --focus: rgba(197,205,223,0.2); + --border: 1px solid rgb(197,205,223); + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + font-family: Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 400; + right: 1.5rem; + top: 1.5rem; + position: absolute; +} + +.emotion-4:hover { + color: var(--color); + background-color: var(--backgroundHover); +} + +.emotion-4:focus, +.emotion-4:active { + color: var(--color); + outline: 4px solid var(--focus); +} + +.emotion-4:disabled, +.emotion-4:disabled:hover { + color: rgb(166,173,194); + background-color: rgb(239,242,249); + border: 1px solid rgb(197,205,223); + cursor: not-allowed; +} + +.emotion-4>svg { + margin-right: 0.25rem; +} + +.emotion-4 [disabled] { + pointer-events: none; +} + +.code-snippet-highlighted-oneline .emotion-4 { + bottom: 0.5rem; +} + +.emotion-6 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + overflow-x: auto; + padding: 1.5rem; +} + +.emotion-6 code { + color: rgb(51,53,60); + background: rgb(252,252,253); + font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 400; +} + +.emotion-6 code.hljs { + padding: unset; +} + +.emotion-6 .hljs-variable, +.emotion-6 .hljs-meta { + color: rgb(51,53,60); +} + +.emotion-6 .hljs-doctag, +.emotion-6 .hljs-title, +.emotion-6 .hljs-title.class_, +.emotion-6 .hljs-title.function_ { + color: rgb(34,84,192); +} + +.emotion-6 .hljs-comment { + color: rgb(109,111,119); + font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; + font-size: 0.875rem; + line-height: 1.25rem; + font-style: italic; +} + +.emotion-6 .hljs-tag, +.emotion-6 .hljs-type, +.emotion-6 .hljs-keyword { + color: rgb(152,29,150); + font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 700; +} + +.emotion-6 .hljs-literal, +.emotion-6 .hljs-number { + color: rgb(126,83,5); +} + +.emotion-6 .hljs-string { + color: rgb(32,105,31); +} + +.emotion-6 .hljs-meta .hljs-keyword { + color: rgb(47,103,48); +} + +.emotion-6.code-wrap { + white-space: pre-wrap; + word-break: break-all; +} + +.emotion-6 mark { + color: rgb(217,45,32); + background-color: rgb(197,205,223); + font-weight: 400; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.emotion-6.simple-one-line { + min-height: 1.25rem; + padding-left: 0.25rem; + padding-right: 0.25rem; + padding-top: 0; + padding-bottom: 0; +} + +
+
+ +
+      
+        
+          foo
+        
+        
+
+        
+          bar
+        
+      
+    
+
+
+`; + +exports[`should show reduced size when single line with no editting 1`] = ` +.emotion-0 { + background-color: rgb(252,252,253); + border: 1px solid rgb(225,230,243); + border-radius: 0.5rem; + position: relative; + margin-top: 0.5rem; + margin-bottom: 0.5rem; +} + +.emotion-0.code-snippet-simple-oneline { + margin-top: 0; + margin-bottom: 0; + border-radius: 0.25rem; +} + +.emotion-4 { + box-sizing: border-box; + -webkit-text-decoration: none; + text-decoration: none; + outline: none; + border: var(--border); + color: var(--color); + background-color: var(--background); + -webkit-transition: background-color 0.2s ease,outline 0.2s ease; + transition: background-color 0.2s ease,outline 0.2s ease; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + height: 2.25rem; + font-family: Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 600; + padding-left: 1rem; + padding-right: 1rem; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + border-radius: 0.5rem; + cursor: pointer; + --background: rgb(255,255,255); + --backgroundHover: rgb(239,242,249); + --color: rgb(62,67,87); + --focus: rgba(197,205,223,0.2); + --border: 1px solid rgb(197,205,223); + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + font-family: Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 400; + right: 1.5rem; + top: 1.5rem; + position: absolute; + bottom: 1.5rem; + top: 1.5rem; +} + +.emotion-4:hover { + color: var(--color); + background-color: var(--backgroundHover); +} + +.emotion-4:focus, +.emotion-4:active { + color: var(--color); + outline: 4px solid var(--focus); +} + +.emotion-4:disabled, +.emotion-4:disabled:hover { + color: rgb(166,173,194); + background-color: rgb(239,242,249); + border: 1px solid rgb(197,205,223); + cursor: not-allowed; +} + +.emotion-4>svg { + margin-right: 0.25rem; +} + +.emotion-4 [disabled] { + pointer-events: none; +} + +.code-snippet-highlighted-oneline .emotion-4 { + bottom: 0.5rem; +} + +.emotion-6 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + overflow-x: auto; + padding: 1.5rem; +} + +.emotion-6 code { + color: rgb(51,53,60); + background: rgb(252,252,253); + font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 400; +} + +.emotion-6 code.hljs { + padding: unset; +} + +.emotion-6 .hljs-variable, +.emotion-6 .hljs-meta { + color: rgb(51,53,60); +} + +.emotion-6 .hljs-doctag, +.emotion-6 .hljs-title, +.emotion-6 .hljs-title.class_, +.emotion-6 .hljs-title.function_ { + color: rgb(34,84,192); +} + +.emotion-6 .hljs-comment { + color: rgb(109,111,119); + font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; + font-size: 0.875rem; + line-height: 1.25rem; + font-style: italic; +} + +.emotion-6 .hljs-tag, +.emotion-6 .hljs-type, +.emotion-6 .hljs-keyword { + color: rgb(152,29,150); + font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 700; +} + +.emotion-6 .hljs-literal, +.emotion-6 .hljs-number { + color: rgb(126,83,5); +} + +.emotion-6 .hljs-string { + color: rgb(32,105,31); +} + +.emotion-6 .hljs-meta .hljs-keyword { + color: rgb(47,103,48); +} + +.emotion-6.code-wrap { + white-space: pre-wrap; + word-break: break-all; +} + +.emotion-6 mark { + color: rgb(217,45,32); + background-color: rgb(197,205,223); + font-weight: 400; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.emotion-6.simple-one-line { + min-height: 1.25rem; + padding-left: 0.25rem; + padding-right: 0.25rem; + padding-top: 0; + padding-bottom: 0; +} + +
+
+ +
+      
+        
+          foobar
+        
+      
+    
+
+
+`; diff --git a/server/sonar-web/design-system/src/components/__tests__/__snapshots__/Highlighter-test.tsx.snap b/server/sonar-web/design-system/src/components/__tests__/__snapshots__/Highlighter-test.tsx.snap new file mode 100644 index 00000000000..45b4dd7b63b --- /dev/null +++ b/server/sonar-web/design-system/src/components/__tests__/__snapshots__/Highlighter-test.tsx.snap @@ -0,0 +1,463 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` +.emotion-0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + overflow-x: auto; + padding: 1.5rem; +} + +.emotion-0 code { + color: rgb(51,53,60); + background: rgb(252,252,253); + font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 400; +} + +.emotion-0 code.hljs { + padding: unset; +} + +.emotion-0 .hljs-variable, +.emotion-0 .hljs-meta { + color: rgb(51,53,60); +} + +.emotion-0 .hljs-doctag, +.emotion-0 .hljs-title, +.emotion-0 .hljs-title.class_, +.emotion-0 .hljs-title.function_ { + color: rgb(34,84,192); +} + +.emotion-0 .hljs-comment { + color: rgb(109,111,119); + font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; + font-size: 0.875rem; + line-height: 1.25rem; + font-style: italic; +} + +.emotion-0 .hljs-tag, +.emotion-0 .hljs-type, +.emotion-0 .hljs-keyword { + color: rgb(152,29,150); + font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 700; +} + +.emotion-0 .hljs-literal, +.emotion-0 .hljs-number { + color: rgb(126,83,5); +} + +.emotion-0 .hljs-string { + color: rgb(32,105,31); +} + +.emotion-0 .hljs-meta .hljs-keyword { + color: rgb(47,103,48); +} + +.emotion-0.code-wrap { + white-space: pre-wrap; + word-break: break-all; +} + +.emotion-0 mark { + color: rgb(217,45,32); + background-color: rgb(197,205,223); + font-weight: 400; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.emotion-0.simple-one-line { + min-height: 1.25rem; + padding-left: 0.25rem; + padding-right: 0.25rem; + padding-top: 0; + padding-bottom: 0; +} + +
+
+    
+      
+        foo\\nbar
+      
+    
+  
+
+`; + +exports[`should display edit functions 1`] = ` +.emotion-0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + overflow-x: auto; + padding: 1.5rem; +} + +.emotion-0 code { + color: rgb(51,53,60); + background: rgb(252,252,253); + font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 400; +} + +.emotion-0 code.hljs { + padding: unset; +} + +.emotion-0 .hljs-variable, +.emotion-0 .hljs-meta { + color: rgb(51,53,60); +} + +.emotion-0 .hljs-doctag, +.emotion-0 .hljs-title, +.emotion-0 .hljs-title.class_, +.emotion-0 .hljs-title.function_ { + color: rgb(34,84,192); +} + +.emotion-0 .hljs-comment { + color: rgb(109,111,119); + font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; + font-size: 0.875rem; + line-height: 1.25rem; + font-style: italic; +} + +.emotion-0 .hljs-tag, +.emotion-0 .hljs-type, +.emotion-0 .hljs-keyword { + color: rgb(152,29,150); + font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 700; +} + +.emotion-0 .hljs-literal, +.emotion-0 .hljs-number { + color: rgb(126,83,5); +} + +.emotion-0 .hljs-string { + color: rgb(32,105,31); +} + +.emotion-0 .hljs-meta .hljs-keyword { + color: rgb(47,103,48); +} + +.emotion-0.code-wrap { + white-space: pre-wrap; + word-break: break-all; +} + +.emotion-0 mark { + color: rgb(217,45,32); + background-color: rgb(197,205,223); + font-weight: 400; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.emotion-0.simple-one-line { + min-height: 1.25rem; + padding-left: 0.25rem; + padding-right: 0.25rem; + padding-top: 0; + padding-bottom: 0; +} + +.emotion-3 { + box-sizing: border-box; + border: none; + outline: none; + -webkit-text-decoration: none; + text-decoration: none; + color: var(--color); + background-color: var(--background); + -webkit-transition: background-color 0.2s ease,outline 0.2s ease,color 0.2s ease; + transition: background-color 0.2s ease,outline 0.2s ease,color 0.2s ease; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -ms-flex-pack: center; + -webkit-justify-content: center; + justify-content: center; + cursor: pointer; + height: 2.25rem; + border-radius: 0.5rem; + padding-left: 0.625rem; + padding-right: 0.625rem; + --background: transparent; + --backgroundHover: rgb(232,235,255); + --color: rgb(75,86,187); + --colorHover: rgb(43,51,104); + --focus: rgba(93,108,208,0.2); +} + +.emotion-3:hover, +.emotion-3:focus, +.emotion-3:active { + color: var(--colorHover); + background-color: var(--backgroundHover); +} + +.emotion-3:focus, +.emotion-3:active { + outline: 4px solid var(--focus); +} + +.emotion-3:disabled, +.emotion-3:disabled:hover { + color: rgb(166,173,194); + background-color: var(--background); + cursor: not-allowed; +} + +
+
+    
+      
+        One
+      
+       
+      
+        line
+      
+       
+      
+        command
+      
+    
+    
+  
+
+`; + +exports[`should handle multiple lines of code 1`] = ` +.emotion-0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + overflow-x: auto; + padding: 1.5rem; +} + +.emotion-0 code { + color: rgb(51,53,60); + background: rgb(252,252,253); + font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 400; +} + +.emotion-0 code.hljs { + padding: unset; +} + +.emotion-0 .hljs-variable, +.emotion-0 .hljs-meta { + color: rgb(51,53,60); +} + +.emotion-0 .hljs-doctag, +.emotion-0 .hljs-title, +.emotion-0 .hljs-title.class_, +.emotion-0 .hljs-title.function_ { + color: rgb(34,84,192); +} + +.emotion-0 .hljs-comment { + color: rgb(109,111,119); + font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; + font-size: 0.875rem; + line-height: 1.25rem; + font-style: italic; +} + +.emotion-0 .hljs-tag, +.emotion-0 .hljs-type, +.emotion-0 .hljs-keyword { + color: rgb(152,29,150); + font-family: Ubuntu Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 700; +} + +.emotion-0 .hljs-literal, +.emotion-0 .hljs-number { + color: rgb(126,83,5); +} + +.emotion-0 .hljs-string { + color: rgb(32,105,31); +} + +.emotion-0 .hljs-meta .hljs-keyword { + color: rgb(47,103,48); +} + +.emotion-0.code-wrap { + white-space: pre-wrap; + word-break: break-all; +} + +.emotion-0 mark { + color: rgb(217,45,32); + background-color: rgb(197,205,223); + font-weight: 400; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.emotion-0.simple-one-line { + min-height: 1.25rem; + padding-left: 0.25rem; + padding-right: 0.25rem; + padding-top: 0; + padding-bottom: 0; +} + +
+
+    
+      
+        foo:
+      
+       
+      
+        bar
+      
+      
+              
+      
+        pleh:
+      
+       
+      
+        help
+      
+      
+              
+      
+        stuff:
+      
+      
+               
+      
+        foo:
+      
+       
+      
+        bar
+      
+      
+               
+      
+        bar:
+      
+       
+      
+        foo
+      
+    
+  
+
+`; diff --git a/server/sonar-web/design-system/src/components/icons/PencilIcon.tsx b/server/sonar-web/design-system/src/components/icons/PencilIcon.tsx new file mode 100644 index 00000000000..5fec48bae65 --- /dev/null +++ b/server/sonar-web/design-system/src/components/icons/PencilIcon.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 { PencilIcon as PencilOcticonIcon } from '@primer/octicons-react'; +import { OcticonHoc } from './Icon'; + +export const PencilIcon = OcticonHoc(PencilOcticonIcon); diff --git a/server/sonar-web/design-system/src/components/icons/index.ts b/server/sonar-web/design-system/src/components/icons/index.ts index ce7c413ff63..6f4a7a4c893 100644 --- a/server/sonar-web/design-system/src/components/icons/index.ts +++ b/server/sonar-web/design-system/src/components/icons/index.ts @@ -49,6 +49,7 @@ export { OpenCloseIndicator } from './OpenCloseIndicator'; export { OpenNewTabIcon } from './OpenNewTabIcon'; export { OverviewQGNotComputedIcon } from './OverviewQGNotComputedIcon'; export { OverviewQGPassedIcon } from './OverviewQGPassedIcon'; +export { PencilIcon } from './PencilIcon'; export { ProjectIcon } from './ProjectIcon'; export { PullRequestIcon } from './PullRequestIcon'; export { RefreshIcon } from './RefreshIcon'; diff --git a/server/sonar-web/design-system/src/components/index.ts b/server/sonar-web/design-system/src/components/index.ts index 7fefefbfc7f..51a75a9969c 100644 --- a/server/sonar-web/design-system/src/components/index.ts +++ b/server/sonar-web/design-system/src/components/index.ts @@ -23,6 +23,7 @@ export * from './Avatar'; export { Badge } from './Badge'; export { BarChart } from './BarChart'; export * from './Card'; +export * from './CodeSnippet'; export * from './CoverageIndicator'; export * from './DatePicker'; export * from './DateRangePicker'; diff --git a/server/sonar-web/design-system/src/theme/light.ts b/server/sonar-web/design-system/src/theme/light.ts index 7a3756efbde..4854e46adb7 100644 --- a/server/sonar-web/design-system/src/theme/light.ts +++ b/server/sonar-web/design-system/src/theme/light.ts @@ -43,6 +43,17 @@ const danger = { darker: COLORS.red[800], }; +const codeSnippetLight = { + annotations: [34, 84, 192], + body: [51, 53, 60], + constants: [126, 83, 5], + comments: [109, 111, 119], + keyword: [152, 29, 150], + string: [32, 105, 31], + 'keyword-light': [28, 28, 163], // Not used currently in code snippet + 'preprocessing-directive': [47, 103, 48], +}; + export const lightTheme = { id: 'light-theme', highlightTheme: 'atom-one-light.css', @@ -170,6 +181,15 @@ export const lightTheme = { codeSnippetBackground: COLORS.blueGrey[25], codeSnippetBorder: COLORS.blueGrey[100], codeSnippetHighlight: secondary.default, + codeSnippetBody: codeSnippetLight.body, + codeSnippetAnnotations: codeSnippetLight.annotations, + codeSnippetComments: codeSnippetLight.comments, + codeSnippetConstants: codeSnippetLight.constants, + codeSnippetKeyword: codeSnippetLight.keyword, + codeSnippetString: codeSnippetLight.string, + codeSnippetKeywordLight: codeSnippetLight['keyword-light'], + codeSnippetPreprocessingDirective: codeSnippetLight['preprocessing-directive'], + codeSnippetInline: COLORS.blueGrey[500], // code viewer codeLineIssueIndicator: COLORS.blueGrey[400], // Should be blueGrey[300], to be changed once code viewer is reworked diff --git a/server/sonar-web/yarn.lock b/server/sonar-web/yarn.lock index 1847ec4d86a..90e4d51539d 100644 --- a/server/sonar-web/yarn.lock +++ b/server/sonar-web/yarn.lock @@ -6146,6 +6146,7 @@ __metadata: eslint-plugin-import: 2.27.5 eslint-plugin-local-rules: 1.3.2 eslint-plugin-typescript-sort-keys: 2.3.0 + highlight.js: 11.7.0 history: 5.3.0 jest: 29.5.0 postcss: 8.4.21 @@ -7839,6 +7840,13 @@ __metadata: languageName: node linkType: hard +"highlight.js@npm:11.7.0": + version: 11.7.0 + resolution: "highlight.js@npm:11.7.0" + checksum: 19e3fb8b56f4b361b057a8523b989dfeb6479bbd1e29cec3fac6fa5c78d09927d5fa61b7dba6631fdb57cfdca9b3084aa4da49405ceaf4a67f67beae2ed5b77d + languageName: node + linkType: hard + "history@npm:5.3.0": version: 5.3.0 resolution: "history@npm:5.3.0" -- 2.39.5