From 475707848e6325823196a1f84fc841507eb122d5 Mon Sep 17 00:00:00 2001 From: Anoop M D Date: Fri, 2 Jan 2026 22:47:19 +0530 Subject: [PATCH] style: enhance syntax highlighting in GQL Docs and Doc Gen (#6640) * style: enhance syntax highlighting and theme integration in QueryEditor and GenerateDocs components * fix: fixed generate code theming issues --- .../RequestPane/QueryEditor/StyledWrapper.js | 64 +++-- .../RequestTabPanel/StyledWrapper.js | 87 +++++++ .../CodeView/StyledWrapper.js | 7 +- .../GenerateCodeItem/CodeView/index.js | 2 +- .../CodeViewToolbar/StyledWrapper.js | 6 +- .../GenerateCodeItem/StyledWrapper.js | 7 +- .../GenerateDocumentation/StyledWrapper.js | 52 ++-- .../Collection/GenerateDocumentation/index.js | 245 ++++++++---------- 8 files changed, 272 insertions(+), 198 deletions(-) diff --git a/packages/bruno-app/src/components/RequestPane/QueryEditor/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/QueryEditor/StyledWrapper.js index ce8bca94f..df8c80407 100644 --- a/packages/bruno-app/src/components/RequestPane/QueryEditor/StyledWrapper.js +++ b/packages/bruno-app/src/components/RequestPane/QueryEditor/StyledWrapper.js @@ -22,28 +22,43 @@ const StyledWrapper = styled.div` } } - .cm-s-monokai span.cm-property, - .cm-s-monokai span.cm-attribute { - color: #9cdcfe !important; - } - - .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, span.cm-variable-2 { + 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; + } + span.cm-tag { + color: ${(props) => props.theme.codemirror.tokens.tag} !important; + } + span.cm-tag.cm-bracket { + color: ${(props) => props.theme.codemirror.tokens.tagBracket} !important; + } } + /* Variable validation colors */ .cm-variable-valid { color: ${(props) => props.theme.codemirror.variable.valid}; } @@ -51,17 +66,10 @@ const StyledWrapper = styled.div` color: ${(props) => props.theme.codemirror.variable.invalid}; } + .CodeMirror-search-hint { display: inline; } - - .cm-s-default span.cm-property { - color: #1f61a0 !important; - } - - .cm-s-default span.cm-variable { - color: #397d13 !important; - } `; export default StyledWrapper; diff --git a/packages/bruno-app/src/components/RequestTabPanel/StyledWrapper.js b/packages/bruno-app/src/components/RequestTabPanel/StyledWrapper.js index 7485e7f62..7bf6bb5f8 100644 --- a/packages/bruno-app/src/components/RequestTabPanel/StyledWrapper.js +++ b/packages/bruno-app/src/components/RequestTabPanel/StyledWrapper.js @@ -96,6 +96,93 @@ const StyledWrapper = styled.div` div.doc-explorer-rhs { display: flex; } + + // GraphQL docs color overrides + .doc-explorer-back { + color: ${(props) => props.theme.textLink}; + + &:before { + border-left-color: ${(props) => props.theme.textLink}; + border-top-color: ${(props) => props.theme.textLink}; + } + } + + .doc-explorer-contents { + border-top-color: ${(props) => props.theme.border.border2}; + } + + .doc-type-description code, + .doc-category code { + color: ${(props) => props.theme.codemirror.tokens.keyword}; + background-color: ${(props) => props.theme.background.surface0}; + border-color: ${(props) => props.theme.border.border1}; + } + + .doc-category-title { + border-bottom-color: ${(props) => props.theme.border.border1}; + color: ${(props) => props.theme.colors.text.muted}; + } + + .doc-category-item { + color: ${(props) => props.theme.colors.text.subtext2}; + } + + .keyword { + color: ${(props) => props.theme.codemirror.tokens.property}; + } + + .type-name { + color: ${(props) => props.theme.codemirror.tokens.atom}; + } + + .field-name { + color: ${(props) => props.theme.codemirror.tokens.property}; + } + + .field-short-description { + color: ${(props) => props.theme.colors.text.muted}; + } + + .enum-value { + color: ${(props) => props.theme.textLink}; + } + + .arg-name { + color: ${(props) => props.theme.colors.text.purple}; + } + + .arg-default-value { + color: ${(props) => props.theme.colors.text.green}; + } + + .doc-deprecation { + background: ${(props) => props.theme.status.warning.background}; + box-shadow: inset 0 0 1px ${(props) => props.theme.status.warning.border}; + color: ${(props) => props.theme.colors.text.muted}; + + &:before { + color: ${(props) => props.theme.status.warning.text}; + } + } + + .show-btn { + border-color: ${(props) => props.theme.border.border2}; + background: ${(props) => props.theme.background.surface0}; + color: ${(props) => props.theme.text}; + } + + .search-box { + border-bottom-color: ${(props) => props.theme.border.border1}; + } + + .search-box-clear { + background-color: ${(props) => props.theme.overlay.overlay1}; + color: ${(props) => props.theme.colors.text.white}; + + &:hover { + background-color: ${(props) => props.theme.overlay.overlay2}; + } + } } `; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/StyledWrapper.js index f54b8450a..746ee8165 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/StyledWrapper.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/StyledWrapper.js @@ -10,12 +10,15 @@ const StyledWrapper = styled.div` .CodeMirror { height: 100%; font-size: ${(props) => props.theme.font.size.sm}; + background: ${(props) => props.theme.modal.bg}; line-height: 1.5; padding: 0; + background: transparent !important; + border: none; .CodeMirror-gutters { - background: ${(props) => props.theme.codemirror.gutter.bg}; - border-right: 1px solid ${(props) => props.theme.codemirror.border}; + background: transparent !important; + border-right: none; } .CodeMirror-linenumber { diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/index.js index a33d41749..fc0ccf841 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/index.js @@ -47,7 +47,7 @@ const CodeView = ({ language, item }) => { onCopy={() => toast.success('Copied to clipboard!')} >
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeViewToolbar/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeViewToolbar/StyledWrapper.js index 617cb5ff2..5b60dadb0 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeViewToolbar/StyledWrapper.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeViewToolbar/StyledWrapper.js @@ -5,9 +5,7 @@ const StyledWrapper = styled.div` display: flex; justify-content: space-between; align-items: center; - padding: 8px 12px; - background: ${(props) => props.theme.requestTabPanel.card.bg}; - border-bottom: 1px solid ${(props) => props.theme.requestTabPanel.card.border}; + background: ${(props) => props.theme.modal.bg}; gap: 12px; flex-shrink: 0; } @@ -45,6 +43,8 @@ const StyledWrapper = styled.div` cursor: pointer; transition: all 0.2s ease; appearance: none; + outline: none; + box-shadow: none; &:hover { border-color: ${(props) => props.theme.input.focusBorder}; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/StyledWrapper.js index 9c02ee4d2..ea77fe0fc 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/StyledWrapper.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/StyledWrapper.js @@ -1,11 +1,10 @@ import styled from 'styled-components'; const StyledWrapper = styled.div` - margin: -1.5rem -1rem; height: 50vh; display: flex; flex-direction: column; - background-color: ${(props) => props.theme.background.base}; + background-color: ${(props) => props.theme.modal.bg}; .code-generator { display: flex; @@ -15,9 +14,11 @@ const StyledWrapper = styled.div` .editor-container { flex: 1; + min-height: 0; overflow: hidden; position: relative; - background: ${(props) => props.theme.bg}; + background: ${(props) => props.theme.modal.bg}; + margin-top: 0.5rem; } .error-message { diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/GenerateDocumentation/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/GenerateDocumentation/StyledWrapper.js index a761d48d0..6c55d5200 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/GenerateDocumentation/StyledWrapper.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/GenerateDocumentation/StyledWrapper.js @@ -1,47 +1,39 @@ import styled from 'styled-components'; -import { rgba } from 'polished'; const StyledWrapper = styled.div` - .info-card { - border-radius: ${(props) => props.theme.border.radius.base}; - background-color: ${(props) => rgba(props.theme.accents.primary, 0.06)}; - border: 1px solid ${(props) => rgba(props.theme.accents.primary, 0.2)}; - - .info-icon { - color: ${(props) => props.theme.accents.primary}; - } - - .info-title { - font-weight: 500; + .content { + .title { + font-size: ${(props) => props.theme.font.size.base}; color: ${(props) => props.theme.text}; } - .info-description { + .description { font-size: ${(props) => props.theme.font.size.sm}; color: ${(props) => props.theme.colors.text.muted}; + line-height: 1.6; + } + + .features { + li { + font-size: ${(props) => props.theme.font.size.sm}; + color: ${(props) => props.theme.text}; + } + + .check-icon { + color: ${(props) => props.theme.colors.text.green}; + } + } + + .note { + font-size: ${(props) => props.theme.font.size.xs}; + color: ${(props) => props.theme.colors.text.muted}; line-height: 1.5; } } - .feature-item { - border-radius: ${(props) => props.theme.border.radius.base}; - background-color: ${(props) => props.theme.background.base}; - border: 1px solid ${(props) => props.theme.border.border0}; - font-size: ${(props) => props.theme.font.size.sm}; - color: ${(props) => props.theme.text}; - - .feature-icon { - color: ${(props) => props.theme.colors.text.green}; - } - } - - .note-section { - border-radius: ${(props) => props.theme.border.radius.base}; - background-color: ${(props) => rgba(props.theme.colors.text.warning, 0.06)}; - border: 1px solid ${(props) => rgba(props.theme.colors.text.warning, 0.2)}; + .text-warning { font-size: ${(props) => props.theme.font.size.sm}; color: ${(props) => props.theme.colors.text.warning}; - line-height: 1.5; } `; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/GenerateDocumentation/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/GenerateDocumentation/index.js index f04b6b02a..6a12d0ef4 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/GenerateDocumentation/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/GenerateDocumentation/index.js @@ -1,59 +1,92 @@ -import React from 'react'; -import Modal from 'components/Modal'; -import { IconCheck, IconInfoCircle, IconAlertTriangle, IconLoader2 } from '@tabler/icons'; -import StyledWrapper from './StyledWrapper'; -import { cloneDeep } from 'lodash'; -import { transformCollectionToSaveToExportAsFile } from 'utils/collections/index'; +import React, { useCallback, useMemo } from 'react'; import { useSelector } from 'react-redux'; -import { findCollectionByUid, areItemsLoading } from 'utils/collections/index'; -import { brunoToOpenCollection } from '@usebruno/converters'; -import { sanitizeName } from 'utils/common/regex'; +import { cloneDeep } from 'lodash'; import * as FileSaver from 'file-saver'; import jsyaml from 'js-yaml'; import toast from 'react-hot-toast'; +import { IconBook, IconCheck, IconAlertTriangle, IconLoader2 } from '@tabler/icons'; + +import Modal from 'components/Modal'; +import StyledWrapper from './StyledWrapper'; import { useApp } from 'providers/App'; +import { transformCollectionToSaveToExportAsFile, findCollectionByUid, areItemsLoading } from 'utils/collections/index'; +import { brunoToOpenCollection } from '@usebruno/converters'; +import { sanitizeName } from 'utils/common/regex'; import { escapeHtml } from 'utils/response'; +const CDN_BASE_URL = 'https://cdn.opencollection.com'; + +const FEATURES = [ + 'Standalone HTML file - no server required', + 'Interactive API playground', + 'Host on any static file server' +]; + +const escapeForTemplate = (content) => + content.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$'); + +const buildHtmlDocument = (collectionName, yamlContent) => ` + + + + + ${collectionName} - API Documentation + + + + + +
+ + +`; + +const CollectionNotFound = ({ onClose }) => ( + + +
+ + Collection not found. It may have been deleted or is no longer available. +
+
+
+); + const GenerateDocumentation = ({ onClose, collectionUid }) => { const { version } = useApp(); - const collection = useSelector((state) => findCollectionByUid(state.collections.collections, collectionUid)); - const isCollectionLoading = collection ? areItemsLoading(collection) : false; + const collection = useSelector((state) => + findCollectionByUid(state.collections.collections, collectionUid) + ); - if (!collection) { - return ( - - -
-
- - Collection not found. It may have been deleted or is no longer available. -
-
-
-
- ); - } + const isLoading = useMemo( + () => (collection ? areItemsLoading(collection) : false), + [collection] + ); - const generateHtmlDocumentation = () => { + const handleGenerate = useCallback(() => { try { const collectionCopy = cloneDeep(collection); const transformedCollection = transformCollectionToSaveToExportAsFile(collectionCopy); const openCollection = brunoToOpenCollection(transformedCollection); - if (!openCollection.extensions) { - openCollection.extensions = {}; - } - if (!openCollection.extensions.bruno) { - openCollection.extensions.bruno = {}; - } - openCollection.extensions.bruno.exportedAt = new Date().toISOString(); - openCollection.extensions.bruno.exportedUsing = version ? `Bruno/${version}` : 'Bruno'; + openCollection.extensions = { + ...openCollection.extensions, + bruno: { + ...openCollection.extensions?.bruno, + exportedAt: new Date().toISOString(), + exportedUsing: version ? `Bruno/${version}` : 'Bruno' + } + }; const yamlContent = jsyaml.dump(openCollection, { indent: 2, @@ -62,117 +95,67 @@ const GenerateDocumentation = ({ onClose, collectionUid }) => { sortKeys: false }); - const escapedYaml = yamlContent - .replace(/\\/g, '\\\\') - .replace(/`/g, '\\`') - .replace(/\$/g, '\\$'); + const htmlContent = buildHtmlDocument( + escapeHtml(collection.name), + escapeForTemplate(yamlContent) + ); - const escapedCollectionName = escapeHtml(collection.name); + const fileName = `${sanitizeName(collection.name)}-documentation.html`; + FileSaver.saveAs(new Blob([htmlContent], { type: 'text/html' }), fileName); - const htmlContent = ` - - - - - ${escapedCollectionName} - API Documentation - - - - - -
- - -`; - - const sanitizedName = sanitizeName(collection.name); - const fileName = `${sanitizedName}-documentation.html`; - const fileBlob = new Blob([htmlContent], { type: 'text/html' }); - - FileSaver.saveAs(fileBlob, fileName); toast.success('Documentation generated successfully'); onClose(); } catch (error) { console.error('Error generating documentation:', error); toast.error('Failed to generate documentation'); } - }; + }, [collection, version, onClose]); + + if (!collection) { + return ; + } return ( -
-
-
- {isCollectionLoading ? ( - - ) : ( - - )} -
-
-
- {isCollectionLoading ? 'Loading collection...' : 'Interactive API Documentation'} -
-
- {isCollectionLoading - ? 'Please wait while the collection is being loaded.' - : 'Generate a standalone HTML file containing interactive documentation for your API collection. This file can be hosted anywhere or shared with your team.'} -
-
+ {isLoading ? ( +
+ + Loading collection...
+ ) : ( +
+

+ + Interactive API Documentation +

+

+ Generate a standalone HTML file containing interactive documentation for your API collection. + This file can be hosted anywhere or shared with your team. +

- {!isCollectionLoading && ( - <> -
-
- - Standalone HTML file - no server required -
-
- - Interactive API playground -
-
- - Host on any static file server -
-
+
    + {FEATURES.map((feature) => ( +
  • + + {feature} +
  • + ))} +
-
- - - The generated file uses OpenCollection CDN for rendering. An internet connection is required when viewing the documentation. - -
- - )} -
+

+ The generated file does not embed all assets. It loads OpenCollection’s JavaScript and CSS files from a CDN when viewing docs, which requires an internet connection. +

+
+ )} );