From fa94efaa246134fe0fc154c84b668182b7b34f56 Mon Sep 17 00:00:00 2001 From: Anoop M D Date: Fri, 28 Nov 2025 05:10:29 +0530 Subject: [PATCH] feat: design revamp --- .../components/CodeEditor/StyledWrapper.js | 104 +++++++++---- .../src/components/CodeEditor/index.js | 13 +- .../src/components/Dropdown/StyledWrapper.js | 4 +- .../RequestPane/GrpcQueryUrl/StyledWrapper.js | 10 +- .../QueryUrl/HttpMethodSelector/index.js | 4 +- .../RequestPane/QueryUrl/StyledWrapper.js | 10 +- .../RequestPane/WsQueryUrl/StyledWrapper.js | 5 +- .../src/components/Sidebar/StyledWrapper.js | 12 +- packages/bruno-app/src/themes/dark.js | 94 +++++++++--- packages/bruno-app/src/themes/light.js | 89 +++++++++-- .../src/utils/codemirror/lint-errors.js | 141 ++++++++++++++++++ 11 files changed, 406 insertions(+), 80 deletions(-) create mode 100644 packages/bruno-app/src/utils/codemirror/lint-errors.js diff --git a/packages/bruno-app/src/components/CodeEditor/StyledWrapper.js b/packages/bruno-app/src/components/CodeEditor/StyledWrapper.js index ab007c662..5552364d5 100644 --- a/packages/bruno-app/src/components/CodeEditor/StyledWrapper.js +++ b/packages/bruno-app/src/components/CodeEditor/StyledWrapper.js @@ -18,6 +18,28 @@ const StyledWrapper = styled.div` flex-direction: column-reverse; } + .CodeMirror-linenumber { + text-align: left !important; + padding-left: 3px !important; + } + + /* Override default lint highlight background when emphasizing the gutter */ + .CodeMirror-lint-line-error, + .CodeMirror-lint-line-warning { + background: none !important; + } + + /* Style line numbers when there's a lint issue */ + .CodeMirror-lint-line-error .CodeMirror-linenumber { + color: #d32f2f !important; + text-decoration: underline; + } + + .CodeMirror-lint-line-warning .CodeMirror-linenumber { + color: #f57c00 !important; + text-decoration: underline; + } + /* Removes the glow outline around the folded json */ .CodeMirror-foldmarker { text-shadow: none; @@ -73,41 +95,48 @@ const StyledWrapper = styled.div` } } - .cm-s-monokai span.cm-property, - .cm-s-monokai span.cm-attribute { - color: #9cdcfe !important; - } - - .cm-s-monokai span.cm-string { - color: #ce9178 !important; - } - - .cm-s-monokai span.cm-number { - color: #b5cea8 !important; - } - - .cm-s-monokai span.cm-atom { - color: #569cd6 !important; + .cm-s-default, .cm-s-monokai { + span.cm-def { + color: ${(props) => props.theme.codemirror.tokens.definition} !important; + } + span.cm-property { + color: ${(props) => props.theme.codemirror.tokens.property} !important; + } + span.cm-string { + color: ${(props) => props.theme.codemirror.tokens.string} !important; + } + span.cm-number { + color: ${(props) => props.theme.codemirror.tokens.number} !important; + } + span.cm-atom { + color: ${(props) => props.theme.codemirror.tokens.atom} !important; + } + span.cm-variable { + color: ${(props) => props.theme.codemirror.tokens.variable} !important; + } + span.cm-keyword { + color: ${(props) => props.theme.codemirror.tokens.keyword} !important; + } + span.cm-comment { + color: ${(props) => props.theme.codemirror.tokens.comment} !important; + } + span.cm-operator { + color: ${(props) => props.theme.codemirror.tokens.operator} !important; + } } + /* Variable validation colors */ .cm-variable-valid { - color: green; + color: #5fad89 !important; /* Soft sage */ } .cm-variable-invalid { - color: red; + color: #d17b7b !important; /* Soft coral */ } .CodeMirror-search-hint { display: inline; } - - .cm-s-default span.cm-property { - color: #1f61a0 !important; - } - - .cm-s-default span.cm-variable { - color: #397d13 !important; - } + //matching bracket fix .CodeMirror-matchingbracket { @@ -126,6 +155,31 @@ const StyledWrapper = styled.div` .cm-search-current { background: rgba(255, 193, 7, 0.4); } + + .lint-error-tooltip { + position: fixed; + z-index: 10000; + background: ${(props) => props.theme.codemirror.bg}; + border-radius: ${(props) => props.theme.border.radius.base}; + padding: 8px 12px; + max-width: 400px; + box-shadow: ${(props) => props.theme.shadow.sm}; + font-size: ${(props) => props.theme.font.size.xs}; + line-height: 1.5; + pointer-events: none; + + .lint-tooltip-message { + padding: 2px 0; + } + + .lint-tooltip-message.error { + color: ${(props) => props.theme.colors.text.danger}; + } + + .lint-tooltip-message.warning { + color: ${(props) => props.theme.colors.text.warning}; + } + } `; export default StyledWrapper; diff --git a/packages/bruno-app/src/components/CodeEditor/index.js b/packages/bruno-app/src/components/CodeEditor/index.js index 4b036aa8a..9a363a46e 100644 --- a/packages/bruno-app/src/components/CodeEditor/index.js +++ b/packages/bruno-app/src/components/CodeEditor/index.js @@ -15,6 +15,7 @@ import { JSHINT } from 'jshint'; import stripJsonComments from 'strip-json-comments'; import { getAllVariables } from 'utils/collections'; import { setupLinkAware } from 'utils/codemirror/linkAware'; +import { setupLintErrorTooltip } from 'utils/codemirror/lint-errors'; import CodeMirrorSearch from 'components/CodeMirrorSearch'; const CodeMirror = require('codemirror'); @@ -37,7 +38,8 @@ export default class CodeEditor extends React.Component { this.lintOptions = { esversion: 11, expr: true, - asi: true + asi: true, + highlightLines: true }; this.state = { @@ -64,7 +66,7 @@ export default class CodeEditor extends React.Component { matchBrackets: true, showCursorWhenSelecting: true, foldGutter: true, - gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'], + gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], lint: this.lintOptions, readOnly: this.props.readOnly, scrollbarStyle: 'overlay', @@ -207,6 +209,9 @@ export default class CodeEditor extends React.Component { ); setupLinkAware(editor); + + // Setup lint error tooltip on line number hover + this.cleanupLintErrorTooltip = setupLintErrorTooltip(editor); } } @@ -272,6 +277,10 @@ export default class CodeEditor extends React.Component { this.editor?._destroyLinkAware?.(); this.editor.off('change', this._onEdit); this.editor.off('scroll', this.onScroll); + + // Clean up lint error tooltip + this.cleanupLintErrorTooltip?.(); + this.editor = null; } } diff --git a/packages/bruno-app/src/components/Dropdown/StyledWrapper.js b/packages/bruno-app/src/components/Dropdown/StyledWrapper.js index 65a52fbaf..c9a44bd9f 100644 --- a/packages/bruno-app/src/components/Dropdown/StyledWrapper.js +++ b/packages/bruno-app/src/components/Dropdown/StyledWrapper.js @@ -12,8 +12,8 @@ const Wrapper = styled.div` font-size: ${(props) => props.theme.font.size.base}; color: ${(props) => props.theme.dropdown.color}; background-color: ${(props) => props.theme.dropdown.bg}; - box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.05); - border-radius: 10px; + box-shadow: ${(props) => props.theme.shadow.sm}; + border-radius: ${(props) => props.theme.border.radius.base}; max-height: 90vh; overflow-y: auto; max-width: unset !important; diff --git a/packages/bruno-app/src/components/RequestPane/GrpcQueryUrl/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/GrpcQueryUrl/StyledWrapper.js index 64c29be51..9c69f132e 100644 --- a/packages/bruno-app/src/components/RequestPane/GrpcQueryUrl/StyledWrapper.js +++ b/packages/bruno-app/src/components/RequestPane/GrpcQueryUrl/StyledWrapper.js @@ -2,17 +2,19 @@ import styled from 'styled-components'; const Wrapper = styled.div` height: 2.3rem; + border: ${(props) => props.theme.requestTabPanel.url.border}; + border-radius: ${(props) => props.theme.border.radius.base}; .method-selector-container { background-color: ${(props) => props.theme.requestTabPanel.url.bg}; - border-top-left-radius: 3px; - border-bottom-left-radius: 3px; + border-top-left-radius: ${(props) => props.theme.border.radius.base}; + border-bottom-left-radius: ${(props) => props.theme.border.radius.base}; } .input-container { background-color: ${(props) => props.theme.requestTabPanel.url.bg}; - border-top-right-radius: 3px; - border-bottom-right-radius: 3px; + border-top-right-radius: ${(props) => props.theme.border.radius.base}; + border-bottom-right-radius: ${(props) => props.theme.border.radius.base}; input { background-color: ${(props) => props.theme.requestTabPanel.url.bg}; diff --git a/packages/bruno-app/src/components/RequestPane/QueryUrl/HttpMethodSelector/index.js b/packages/bruno-app/src/components/RequestPane/QueryUrl/HttpMethodSelector/index.js index 67e9a42dd..a1e85d726 100644 --- a/packages/bruno-app/src/components/RequestPane/QueryUrl/HttpMethodSelector/index.js +++ b/packages/bruno-app/src/components/RequestPane/QueryUrl/HttpMethodSelector/index.js @@ -27,7 +27,7 @@ const Icon = forwardRef(function IconComponent( diff --git a/packages/bruno-app/src/components/RequestPane/QueryUrl/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/QueryUrl/StyledWrapper.js index cca562025..4930d51c7 100644 --- a/packages/bruno-app/src/components/RequestPane/QueryUrl/StyledWrapper.js +++ b/packages/bruno-app/src/components/RequestPane/QueryUrl/StyledWrapper.js @@ -2,17 +2,19 @@ import styled from 'styled-components'; const Wrapper = styled.div` height: 2.3rem; + border: ${(props) => props.theme.requestTabPanel.url.border}; + border-radius: ${(props) => props.theme.border.radius.base}; div.method-selector-container { background-color: ${(props) => props.theme.requestTabPanel.url.bg}; - border-top-left-radius: 3px; - border-bottom-left-radius: 3px; + border-top-left-radius: ${(props) => props.theme.border.radius.base}; + border-bottom-left-radius: ${(props) => props.theme.border.radius.base}; } div.input-container { background-color: ${(props) => props.theme.requestTabPanel.url.bg}; - border-top-right-radius: 3px; - border-bottom-right-radius: 3px; + border-top-right-radius: ${(props) => props.theme.border.radius.base}; + border-bottom-right-radius: ${(props) => props.theme.border.radius.base}; input { background-color: ${(props) => props.theme.requestTabPanel.url.bg}; diff --git a/packages/bruno-app/src/components/RequestPane/WsQueryUrl/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/WsQueryUrl/StyledWrapper.js index ecada4e18..7cec49328 100644 --- a/packages/bruno-app/src/components/RequestPane/WsQueryUrl/StyledWrapper.js +++ b/packages/bruno-app/src/components/RequestPane/WsQueryUrl/StyledWrapper.js @@ -3,11 +3,12 @@ import styled from 'styled-components'; const StyledWrapper = styled.div` height: 2.3rem; position: relative; + border: ${(props) => props.theme.requestTabPanel.url.border}; + border-radius: ${(props) => props.theme.border.radius.base}; .input-container { background-color: ${(props) => props.theme.requestTabPanel.url.bg}; - border-top-right-radius: 3px; - border-bottom-right-radius: 3px; + border-radius: ${(props) => props.theme.border.radius.base}; input { background-color: ${(props) => props.theme.requestTabPanel.url.bg}; diff --git a/packages/bruno-app/src/components/Sidebar/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/StyledWrapper.js index 2cec70df5..5cda45b32 100644 --- a/packages/bruno-app/src/components/Sidebar/StyledWrapper.js +++ b/packages/bruno-app/src/components/Sidebar/StyledWrapper.js @@ -53,10 +53,16 @@ const Wrapper = styled.div` right: -3px; transition: opacity 0.2s ease; - &:hover div.drag-request-border { - width: 2px; + div.drag-request-border { + width: 1px; height: 100%; - border-left: solid 1px ${(props) => props.theme.sidebar.dragbar}; + border-left: solid 1px ${(props) => props.theme.sidebar.dragbar.border}; + } + + &:hover div.drag-request-border { + width: 1px; + height: 100%; + border-left: solid 1px ${(props) => props.theme.sidebar.dragbar.activeBorder}; } } `; diff --git a/packages/bruno-app/src/themes/dark.js b/packages/bruno-app/src/themes/dark.js index ab9a09166..f7fa30b9c 100644 --- a/packages/bruno-app/src/themes/dark.js +++ b/packages/bruno-app/src/themes/dark.js @@ -1,8 +1,31 @@ +const colors = { + BRAND: '#546de5', + TEXT: '#d4d4d4', + TEXT_LINK: '#569cd6', + BACKGROUND: '#1e1e1e', + + GRAY_1: '#666666', + GRAY_2: '#444444', + GRAY_3: '#252526', + + CODEMIRROR_TOKENS: { + DEFINITION: '#9ccc9c', // Softer, brighter sage — better contrast + PROPERTY: '#7dcfff', // Soft sky blue, high clarity without being loud + STRING: '#d7ba7d', // VSCode-like warm string tone + NUMBER: '#4ec9b0', // Standard teal with higher clarity + ATOM: '#c586c0', // Brighter lavender, matches VSCode purple + VARIABLE: '#4fc1ff', // Clear aqua-blue (used widely in dark themes) + KEYWORD: '#c58679', // Coral-ish but muted to avoid eye strain + COMMENT: '#6a9955', // Greenish-slate — very readable & subtle + OPERATOR: '#d4d4d4' // Light gray — consistent with dark mode operators + } +}; + const darkTheme = { - brand: '#546de5', - text: '#d4d4d4', - textLink: '#569cd6', - bg: '#1e1e1e', + brand: colors.BRAND, + text: colors.TEXT, + textLink: colors.TEXT_LINK, + bg: colors.BACKGROUND, font: { size: { @@ -15,13 +38,29 @@ const darkTheme = { } }, + shadow: { + sm: '0 1px 3px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(0, 0, 0, 0.3)', + md: '0 2px 8px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(0, 0, 0, 0.4)', + lg: '0 2px 12px rgba(0, 0, 0, 0.7), 0 0 0 1px rgba(0, 0, 0, 0.4)' + }, + + border: { + radius: { + sm: '4px', + base: '6px', + md: '8px', + lg: '10px', + xl: '12px' + } + }, + colors: { text: { green: 'rgb(11 178 126)', danger: '#f06f57', muted: '#9d9d9d', purple: '#cd56d6', - yellow: '#f59e0b' + yellow: '#d9a342' }, bg: { danger: '#d03544' @@ -57,8 +96,11 @@ const darkTheme = { sidebar: { color: '#ccc', muted: '#9d9d9d', - bg: '#252526', - dragbar: '#666666', + bg: colors.GRAY_3, + dragbar: { + border: 'transparent', + activeBorder: colors.GRAY_1 + }, badge: { bg: '#3D3D3D' @@ -98,8 +140,8 @@ const darkTheme = { shadow: 'rgb(0 0 0 / 36%) 0px 2px 8px', separator: '#444', labelBg: '#4a4949', - selectedBg: '#F59E0B14', - selectedColor: '#F59E0B', + selectedBg: '#d9a34214', + selectedColor: '#d9a342', mutedText: '#9B9B9B', primaryText: '#D4D4D4', secondaryText: '#9CA3AF', @@ -118,16 +160,17 @@ const darkTheme = { head: '#d69956' }, grpc: '#6366f1', - ws: '#f59e0b', + ws: '#d9a342', gql: '#e535ab' }, requestTabPanel: { url: { - bg: '#3D3D3D', + bg: colors.BACKGROUND, icon: 'rgb(204, 204, 204)', iconDanger: '#fa5343', - errorHoverBg: '#4a2a2a' + errorHoverBg: '#4a2a2a', + border: `solid 1px ${colors.GRAY_2}` }, dragbar: { border: '#444', @@ -252,7 +295,7 @@ const darkTheme = { tabs: { active: { color: '#CCCCCC', - border: '#F59E0B' + border: '#d9a342' }, secondary: { active: { @@ -287,14 +330,14 @@ const darkTheme = { }, codemirror: { - bg: '#1e1e1e', - border: '#373737', + bg: colors.BACKGROUND, + border: colors.BACKGROUND, placeholder: { color: '#a2a2a2', opacity: 0.5 }, gutter: { - bg: '#262626' + bg: colors.BACKGROUND }, variable: { valid: 'rgb(11 178 126)', @@ -313,6 +356,17 @@ const darkTheme = { editorBorder: '#3D3D3D' } }, + tokens: { + definition: colors.CODEMIRROR_TOKENS.DEFINITION, + property: colors.CODEMIRROR_TOKENS.PROPERTY, + string: colors.CODEMIRROR_TOKENS.STRING, + number: colors.CODEMIRROR_TOKENS.NUMBER, + atom: colors.CODEMIRROR_TOKENS.ATOM, + variable: colors.CODEMIRROR_TOKENS.VARIABLE, + keyword: colors.CODEMIRROR_TOKENS.KEYWORD, + comment: colors.CODEMIRROR_TOKENS.COMMENT, + operator: colors.CODEMIRROR_TOKENS.OPERATOR + }, searchLineHighlightCurrent: 'rgba(120,120,120,0.18)', searchMatch: '#FFD700', searchMatchActive: '#FFFF00' @@ -346,7 +400,7 @@ const darkTheme = { tooltip: { bg: '#1f1f1f', color: '#ffffff', - shortcutColor: '#f59e0b' + shortcutColor: '#d9a342' }, infoTip: { @@ -463,7 +517,7 @@ const darkTheme = { hoverBg: 'rgba(255, 255, 255, 0.05)', selected: { bg: 'rgba(245, 158, 11, 0.2)', - border: '#f59e0b' + border: '#d9a342' }, text: '#d4d4d4', secondaryText: '#9d9d9d', @@ -492,8 +546,8 @@ const darkTheme = { }, examples: { - buttonBg: '#F59E0B1A', - buttonColor: '#F59E0B', + buttonBg: '#d9a3421A', + buttonColor: '#d9a342', buttonText: '#fff', buttonIconColor: '#fff', border: '#444', diff --git a/packages/bruno-app/src/themes/light.js b/packages/bruno-app/src/themes/light.js index e16ee1339..2b8cf7033 100644 --- a/packages/bruno-app/src/themes/light.js +++ b/packages/bruno-app/src/themes/light.js @@ -1,8 +1,33 @@ +const colors = { + BRAND: '#546de5', + TEXT: 'rgb(52, 52, 52)', + TEXT_LINK: '#1663bb', + BACKGROUND: '#fff', + WHITE: '#fff', + BLACK: '#000', + GRAY_1: '#f8f8f8', + GRAY_2: '#eaeaea', + GRAY_3: '#e5e5e5', + GRAY_4: '#cbcbcb', + + CODEMIRROR_TOKENS: { + DEFINITION: '#566f4e', // Deep moss + PROPERTY: '#4b7bbb', // Muted azure + STRING: '#a06e3b', // Warm bronze + NUMBER: '#3d8b7c', // Muted jade + ATOM: '#8169ad', // Soft plum + VARIABLE: '#3f7b6f', // Deep teal + KEYWORD: '#b95d6a', // Muted ruby + COMMENT: '#8997aa', // Cool gray + OPERATOR: '#6b7a8f' // Slate blue + } +}; + const lightTheme = { - brand: '#546de5', - text: 'rgb(52, 52, 52)', - textLink: '#1663bb', - bg: '#fff', + brand: colors.BRAND, + text: colors.TEXT, + textLink: colors.TEXT_LINK, + bg: colors.BACKGROUND, font: { size: { @@ -15,10 +40,27 @@ const lightTheme = { } }, + shadow: { + sm: '0 1px 3px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(0, 0, 0, 0.05)', + md: '0 2px 8px rgba(0, 0, 0, 0.14), 0 0 0 1px rgba(0, 0, 0, 0.06)', + lg: '0 2px 12px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.05)' + }, + + border: { + radius: { + sm: '4px', + base: '6px', + md: '8px', + lg: '10px', + xl: '12px' + } + }, + colors: { text: { green: '#047857', danger: '#B91C1C', + warning: '#f57c00', muted: '#838383', purple: '#8e44ad', yellow: '#d97706' @@ -57,11 +99,14 @@ const lightTheme = { sidebar: { color: 'rgb(52, 52, 52)', muted: '#4b5563', - bg: '#F3F3F3', - dragbar: 'rgb(200, 200, 200)', + bg: colors.GRAY_1, + dragbar: { + border: colors.GRAY_3, + activeBorder: colors.GRAY_4 + }, badge: { - bg: '#e1e1e1' + bg: '#eaeaea' }, search: { @@ -71,11 +116,11 @@ const lightTheme = { collection: { item: { - bg: '#e1e1e1', - hoverBg: '#e7e7e7', - indentBorder: 'solid 1px #e1e1e1', + bg: colors.GRAY_2, + hoverBg: colors.GRAY_2, + indentBorder: `solid 1px ${colors.GRAY_3}`, active: { - indentBorder: 'solid 1px #d0d0d0' + indentBorder: `solid 1px ${colors.GRAY_3}` } } }, @@ -124,10 +169,11 @@ const lightTheme = { requestTabPanel: { url: { - bg: '#f3f3f3', + bg: colors.WHITE, icon: '#515151', iconDanger: '#d91f11', - errorHoverBg: '#fef2f2' + errorHoverBg: '#fef2f2', + border: `solid 1px ${colors.GRAY_3}` }, dragbar: { border: '#efefef', @@ -288,14 +334,14 @@ const lightTheme = { }, codemirror: { - bg: 'white', - border: '#efefef', + bg: colors.WHITE, + border: colors.WHITE, placeholder: { color: '#a2a2a2', opacity: 0.75 }, gutter: { - bg: '#f3f3f3' + bg: colors.WHITE }, variable: { valid: '#047857', @@ -314,6 +360,17 @@ const lightTheme = { editorBorder: '#EFEFEF' } }, + tokens: { + definition: colors.CODEMIRROR_TOKENS.DEFINITION, + property: colors.CODEMIRROR_TOKENS.PROPERTY, + string: colors.CODEMIRROR_TOKENS.STRING, + number: colors.CODEMIRROR_TOKENS.NUMBER, + atom: colors.CODEMIRROR_TOKENS.ATOM, + variable: colors.CODEMIRROR_TOKENS.VARIABLE, + keyword: colors.CODEMIRROR_TOKENS.KEYWORD, + comment: colors.CODEMIRROR_TOKENS.COMMENT, + operator: colors.CODEMIRROR_TOKENS.OPERATOR + }, searchLineHighlightCurrent: 'rgba(120,120,120,0.10)', searchMatch: '#B8860B', searchMatchActive: '#DAA520' diff --git a/packages/bruno-app/src/utils/codemirror/lint-errors.js b/packages/bruno-app/src/utils/codemirror/lint-errors.js new file mode 100644 index 000000000..e813c247b --- /dev/null +++ b/packages/bruno-app/src/utils/codemirror/lint-errors.js @@ -0,0 +1,141 @@ +/** + * Lint Error Tooltip for CodeMirror + * Shows lint errors in a popover when hovering over line numbers + */ + +let activeTooltip = null; + +/** + * Get lint errors for a specific line from the editor's lint state + * @param {CodeMirror} editor - The CodeMirror editor instance + * @param {number} lineNumber - The 0-indexed line number + * @returns {Array} Array of lint error annotations + */ +function getLintErrorsForLine(editor, lineNumber) { + if (!editor) return []; + + const errors = []; + const lintState = editor.state.lint; + + if (lintState && lintState.marked) { + lintState.marked.forEach((mark) => { + if (mark.__annotation) { + // Use annotation's from position directly (mark.find() can return null if lines array is empty) + const annotationLine = mark.__annotation.from?.line; + + if (annotationLine === lineNumber) { + // Avoid duplicate messages + if (!errors.find((e) => e.message === mark.__annotation.message)) { + errors.push(mark.__annotation); + } + } + } + }); + } + + return errors; +} + +/** + * Show the lint error tooltip next to the target element + * @param {Array} errors - Array of lint error annotations + * @param {HTMLElement} targetElement - The element to position the tooltip near + * @param {HTMLElement} container - The container to append the tooltip to + */ +function showLintTooltip(errors, targetElement, container) { + hideLintTooltip(); + + const tooltip = document.createElement('div'); + tooltip.className = 'lint-error-tooltip'; + + errors.forEach((error, index) => { + const errorDiv = document.createElement('div'); + errorDiv.className = `lint-tooltip-message ${error.severity || 'error'}`; + errorDiv.textContent = error.message; + tooltip.appendChild(errorDiv); + }); + + container.appendChild(tooltip); + activeTooltip = tooltip; + + // Position the tooltip + const rect = targetElement.getBoundingClientRect(); + tooltip.style.left = `${rect.right + 8}px`; + tooltip.style.top = `${rect.top + (rect.height / 2)}px`; + tooltip.style.transform = 'translateY(-50%)'; +} + +/** + * Hide and remove the active lint error tooltip + */ +function hideLintTooltip() { + if (activeTooltip) { + activeTooltip.remove(); + activeTooltip = null; + } +} + +/** + * Setup lint error tooltip functionality for a CodeMirror editor + * Shows lint errors when hovering over line numbers + * + * @param {CodeMirror} editor - The CodeMirror editor instance + * @returns {Function} Cleanup function to remove event listeners + */ +export function setupLintErrorTooltip(editor) { + const wrapper = editor.getWrapperElement(); + // Get the StyledWrapper container (parent of CodeMirror wrapper) + const container = wrapper.closest('.graphiql-container') || wrapper.parentElement; + + const handleMouseOver = (e) => { + const target = e.target; + + // Check if hovering over a line number element + if (target.classList.contains('CodeMirror-linenumber')) { + const lineNumber = parseInt(target.textContent, 10) - 1; // 0-indexed + + if (isNaN(lineNumber) || lineNumber < 0) { + hideLintTooltip(); + return; + } + + const lintErrors = getLintErrorsForLine(editor, lineNumber); + + if (lintErrors.length > 0) { + showLintTooltip(lintErrors, target, container); + } else { + hideLintTooltip(); + } + } else if (!target.closest('.lint-error-tooltip')) { + hideLintTooltip(); + } + }; + + const handleMouseOut = (e) => { + const relatedTarget = e.relatedTarget; + // Don't hide if moving to another line number or the tooltip + if (relatedTarget + && (relatedTarget.classList?.contains('CodeMirror-linenumber') + || relatedTarget.closest?.('.lint-error-tooltip'))) { + return; + } + hideLintTooltip(); + }; + + const handleScroll = () => { + hideLintTooltip(); + }; + + // Add event listeners + wrapper.addEventListener('mouseover', handleMouseOver); + wrapper.addEventListener('mouseout', handleMouseOut); + editor.on('scroll', handleScroll); + + // Return cleanup function + return () => { + wrapper.removeEventListener('mouseover', handleMouseOver); + wrapper.removeEventListener('mouseout', handleMouseOut); + editor.off('scroll', handleScroll); + hideLintTooltip(); + }; +}