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();
+ };
+}