From a582e894cb1eaff7b26b33ca1a036a65bde3a7f0 Mon Sep 17 00:00:00 2001 From: Sebastian Reinado Date: Thu, 12 Mar 2026 11:46:24 -0300 Subject: [PATCH] feat: Add collapsible code blocks for blocks over 15 lines - Code blocks with more than 15 lines are automatically collapsible - Add expand/collapse button at top-left corner - Shows gradient overlay when collapsed to indicate more content - Addresses issue #2425 --- browser/components/MarkdownPreview.js | 124 ++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/browser/components/MarkdownPreview.js b/browser/components/MarkdownPreview.js index 4d2633190..7be18458c 100755 --- a/browser/components/MarkdownPreview.js +++ b/browser/components/MarkdownPreview.js @@ -516,6 +516,130 @@ class MarkdownPreview extends React.Component { el.innerHTML = '' el.parentNode.className += ` ${codeBlockThemeClassName}` + // Check if code block is long enough to be collapsible (15+ lines) + const lineCount = (content.match(/\n/g) || []).length + 1 + const MAX_LINES_BEFORE_COLLAPSE = 15 + const codeBlockContainer = el.parentNode + + if (lineCount > MAX_LINES_BEFORE_COLLAPSE) { + // Add collapsible class and CSS + codeBlockContainer.classList.add('collapsible-code-block') + codeBlockContainer.classList.add('collapsed') + + // Create expand/collapse button + const collapseIcon = document.createElement('button') + collapseIcon.className = 'collapse-expand-button' + collapseIcon.innerHTML = '▼' + collapseIcon.title = 'Click to expand/collapse' + collapseIcon.style.cssText = ` + position: absolute; + top: 5px; + left: 5px; + z-index: 10; + border-radius: 3px; + cursor: pointer; + font-size: 10px; + width: 20px; + height: 20px; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid; + ` + + collapseIcon.onclick = e => { + e.preventDefault() + e.stopPropagation() + if (codeBlockContainer.classList.contains('collapsed')) { + codeBlockContainer.classList.remove('collapsed') + codeBlockContainer.classList.add('expanded') + collapseIcon.innerHTML = '▲' + } else { + codeBlockContainer.classList.remove('expanded') + codeBlockContainer.classList.add('collapsed') + collapseIcon.innerHTML = '▼' + } + } + + // Insert button at the beginning of the code block + codeBlockContainer.style.position = 'relative' + codeBlockContainer.appendChild(collapseIcon) + + // Add inline styles for collapse behavior + if (!this.getWindow().document.getElementById('collapsible-code-styles')) { + const styleEl = document.createElement('style') + styleEl.id = 'collapsible-code-styles' + styleEl.textContent = ` + .collapsible-code-block.collapsed { + max-height: 300px !important; + overflow: hidden !important; + position: relative; + } + .collapsible-code-block.collapsed::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 30px; + pointer-events: none; + } + /* Light theme gradient */ + [data-theme="default"] .collapsible-code-block.collapsed::after, + [data-theme="white"] .collapsible-code-block.collapsed::after { + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.9)); + } + /* Dark theme gradient */ + [data-theme="dark"] .collapsible-code-block.collapsed::after, + [data-theme="dracula"] .collapsible-code-block.collapsed::after, + [data-theme="monokai"] .collapsible-code-block.collapsed::after, + [data-theme="nord"] .collapsible-code-block.collapsed::after, + [data-theme="solarized-dark"] .collapsible-code-block.collapsed::after, + [data-theme="vulcan"] .collapsible-code-block.collapsed::after { + background: linear-gradient(to bottom, transparent, rgba(0, 0, 0, 0.7)); + } + .collapsible-code-block.expanded { + max-height: none !important; + } + .collapsible-code-block.expanded::after { + display: none; + } + /* Button styles for light themes */ + [data-theme="default"] .collapse-expand-button, + [data-theme="white"] .collapse-expand-button { + background: rgba(255, 255, 255, 0.8); + border-color: #ccc; + color: #333; + } + [data-theme="default"] .collapse-expand-button:hover, + [data-theme="white"] .collapse-expand-button:hover { + background: rgba(255, 255, 255, 0.9); + } + /* Button styles for dark themes */ + [data-theme="dark"] .collapse-expand-button, + [data-theme="dracula"] .collapse-expand-button, + [data-theme="monokai"] .collapse-expand-button, + [data-theme="nord"] .collapse-expand-button, + [data-theme="solarized-dark"] .collapse-expand-button, + [data-theme="vulcan"] .collapse-expand-button { + background: rgba(0, 0, 0, 0.6); + border-color: #666; + color: #fff; + } + [data-theme="dark"] .collapse-expand-button:hover, + [data-theme="dracula"] .collapse-expand-button:hover, + [data-theme="monokai"] .collapse-expand-button:hover, + [data-theme="nord"] .collapse-expand-button:hover, + [data-theme="solarized-dark"] .collapse-expand-button:hover, + [data-theme="vulcan"] .collapse-expand-button:hover { + background: rgba(0, 0, 0, 0.8); + } + ` + this.getWindow().document.head.appendChild(styleEl) + } + } + CodeMirror.runMode(content, syntax.mime, el, { tabSize: indentSize })