- Implemented WysiwygModalManager for managing modals related to code blocks and block settings. - Created WysiwygSelection for handling text selection across Shadow DOM boundaries. - Introduced WysiwygShortcuts for managing keyboard shortcuts and slash menu items. - Developed wysiwygStyles for consistent styling of the WYSIWYG editor. - Defined types for blocks, slash menu items, and shortcut patterns in wysiwyg.types.ts.
		
			
				
	
	
		
			716 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			716 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import { BaseBlockHandler, type IBlockEventHandlers } from '../block.base.js';
 | |
| import type { IBlock } from '../../wysiwyg.types.js';
 | |
| import { cssManager } from '@design.estate/dees-element';
 | |
| import { WysiwygSelection } from '../../wysiwyg.selection.js';
 | |
| import hlight from 'highlight.js';
 | |
| import { cssGeistFontFamily, cssMonoFontFamily } from '../../../00fonts.js';
 | |
| import { PROGRAMMING_LANGUAGES } from '../../wysiwyg.constants.js';
 | |
| 
 | |
| /**
 | |
|  * CodeBlockHandler with improved architecture
 | |
|  * 
 | |
|  * Key features:
 | |
|  * 1. Simple DOM structure
 | |
|  * 2. Line number handling
 | |
|  * 3. Syntax highlighting only when not focused (grey text while editing)
 | |
|  * 4. Clean event handling
 | |
|  * 5. Copy button functionality
 | |
|  */
 | |
| export class CodeBlockHandler extends BaseBlockHandler {
 | |
|   type = 'code';
 | |
|   
 | |
|   private highlightTimer: any = null;
 | |
|   
 | |
|   render(block: IBlock, isSelected: boolean): string {
 | |
|     const language = block.metadata?.language || 'typescript';
 | |
|     const content = block.content || '';
 | |
|     const lineCount = content.split('\n').length;
 | |
|     
 | |
|     // Generate line numbers
 | |
|     let lineNumbersHtml = '';
 | |
|     for (let i = 1; i <= lineCount; i++) {
 | |
|       lineNumbersHtml += `<div class="line-number">${i}</div>`;
 | |
|     }
 | |
|     
 | |
|     // Generate language options
 | |
|     const languageOptions = PROGRAMMING_LANGUAGES.map(lang => {
 | |
|       const value = lang.toLowerCase();
 | |
|       return `<option value="${value}" ${value === language ? 'selected' : ''}>${lang}</option>`;
 | |
|     }).join('');
 | |
|     
 | |
|     return `
 | |
|       <div class="code-block-container${isSelected ? ' selected' : ''}" data-language="${language}">
 | |
|         <div class="code-header">
 | |
|           <select class="language-selector" data-block-id="${block.id}">
 | |
|             ${languageOptions}
 | |
|           </select>
 | |
|           <button class="copy-button" title="Copy code">
 | |
|             <svg class="copy-icon" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
 | |
|               <path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 010 1.5h-1.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 019.25 16h-7.5A1.75 1.75 0 010 14.25v-7.5z"></path>
 | |
|               <path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0114.25 11h-7.5A1.75 1.75 0 015 9.25v-7.5zm1.75-.25a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25h-7.5z"></path>
 | |
|             </svg>
 | |
|             <span class="copy-text">Copy</span>
 | |
|           </button>
 | |
|         </div>
 | |
|         <div class="code-body">
 | |
|           <div class="line-numbers">${lineNumbersHtml}</div>
 | |
|           <div class="code-content">
 | |
|             <pre class="code-pre"><code class="code-editor" 
 | |
|                  contenteditable="true"
 | |
|                  data-block-id="${block.id}"
 | |
|                  data-block-type="${block.type}"
 | |
|                  spellcheck="false">${this.escapeHtml(content)}</code></pre>
 | |
|           </div>
 | |
|         </div>
 | |
|       </div>
 | |
|     `;
 | |
|   }
 | |
|   
 | |
|   setup(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void {
 | |
|     const editor = element.querySelector('.code-editor') as HTMLElement;
 | |
|     const container = element.querySelector('.code-block-container') as HTMLElement;
 | |
|     const copyButton = element.querySelector('.copy-button') as HTMLButtonElement;
 | |
|     const languageSelector = element.querySelector('.language-selector') as HTMLSelectElement;
 | |
|     
 | |
|     if (!editor || !container) return;
 | |
|     
 | |
|     // Setup language selector
 | |
|     if (languageSelector) {
 | |
|       languageSelector.addEventListener('change', (e) => {
 | |
|         const newLanguage = (e.target as HTMLSelectElement).value;
 | |
|         block.metadata = { ...block.metadata, language: newLanguage };
 | |
|         container.setAttribute('data-language', newLanguage);
 | |
|         
 | |
|         // Update the syntax highlighting if content exists and not focused
 | |
|         if (block.content && document.activeElement !== editor) {
 | |
|           this.applyHighlighting(element, block);
 | |
|         }
 | |
|         
 | |
|         // Notify about the change
 | |
|         if (handlers.onInput) {
 | |
|           handlers.onInput(new InputEvent('input'));
 | |
|         }
 | |
|       });
 | |
|     }
 | |
|     
 | |
|     // Setup copy button
 | |
|     if (copyButton) {
 | |
|       copyButton.addEventListener('click', async () => {
 | |
|         const content = editor.textContent || '';
 | |
|         
 | |
|         try {
 | |
|           await navigator.clipboard.writeText(content);
 | |
|           
 | |
|           // Show feedback
 | |
|           const copyText = copyButton.querySelector('.copy-text') as HTMLElement;
 | |
|           const originalText = copyText.textContent;
 | |
|           copyText.textContent = 'Copied!';
 | |
|           copyButton.classList.add('copied');
 | |
|           
 | |
|           // Reset after 2 seconds
 | |
|           setTimeout(() => {
 | |
|             copyText.textContent = originalText;
 | |
|             copyButton.classList.remove('copied');
 | |
|           }, 2000);
 | |
|         } catch (err) {
 | |
|           console.error('Failed to copy:', err);
 | |
|           // Fallback for older browsers
 | |
|           const textArea = document.createElement('textarea');
 | |
|           textArea.value = content;
 | |
|           textArea.style.position = 'fixed';
 | |
|           textArea.style.opacity = '0';
 | |
|           document.body.appendChild(textArea);
 | |
|           textArea.select();
 | |
|           try {
 | |
|             // @ts-ignore - execCommand is deprecated but needed for fallback
 | |
|             document.execCommand('copy');
 | |
|             // Show feedback
 | |
|             const copyText = copyButton.querySelector('.copy-text') as HTMLElement;
 | |
|             const originalText = copyText.textContent;
 | |
|             copyText.textContent = 'Copied!';
 | |
|             copyButton.classList.add('copied');
 | |
|             
 | |
|             setTimeout(() => {
 | |
|               copyText.textContent = originalText;
 | |
|               copyButton.classList.remove('copied');
 | |
|             }, 2000);
 | |
|           } catch (err) {
 | |
|             console.error('Fallback copy failed:', err);
 | |
|           }
 | |
|           document.body.removeChild(textArea);
 | |
|         }
 | |
|       });
 | |
|     }
 | |
|     
 | |
|     // Track if we're currently editing
 | |
|     let isEditing = false;
 | |
|     
 | |
|     // Focus handler
 | |
|     editor.addEventListener('focus', () => {
 | |
|       isEditing = true;
 | |
|       container.classList.add('editing');
 | |
|       
 | |
|       // Remove all syntax highlighting when focused
 | |
|       const content = editor.textContent || '';
 | |
|       editor.textContent = content; // This removes all HTML formatting
 | |
|       
 | |
|       // Restore cursor position after removing highlighting
 | |
|       requestAnimationFrame(() => {
 | |
|         const range = document.createRange();
 | |
|         const selection = window.getSelection();
 | |
|         if (editor.firstChild) {
 | |
|           range.setStart(editor.firstChild, 0);
 | |
|           range.collapse(true);
 | |
|           selection?.removeAllRanges();
 | |
|           selection?.addRange(range);
 | |
|         }
 | |
|       });
 | |
|       
 | |
|       handlers.onFocus();
 | |
|     });
 | |
|     
 | |
|     // Blur handler
 | |
|     editor.addEventListener('blur', () => {
 | |
|       isEditing = false;
 | |
|       container.classList.remove('editing');
 | |
|       // Apply final highlighting on blur
 | |
|       this.applyHighlighting(element, block);
 | |
|       handlers.onBlur();
 | |
|     });
 | |
|     
 | |
|     // Input handler
 | |
|     editor.addEventListener('input', (e) => {
 | |
|       handlers.onInput(e as InputEvent);
 | |
|       
 | |
|       // Update line numbers
 | |
|       this.updateLineNumbers(element);
 | |
|       
 | |
|       // Clear any pending highlight timer (no highlighting while editing)
 | |
|       clearTimeout(this.highlightTimer);
 | |
|     });
 | |
|     
 | |
|     // Keydown handler
 | |
|     editor.addEventListener('keydown', (e) => {
 | |
|       // Handle Tab key for code blocks
 | |
|       if (e.key === 'Tab') {
 | |
|         e.preventDefault();
 | |
|         const selection = window.getSelection();
 | |
|         if (selection && selection.rangeCount > 0) {
 | |
|           const range = selection.getRangeAt(0);
 | |
|           const textNode = document.createTextNode('  ');
 | |
|           range.insertNode(textNode);
 | |
|           range.setStartAfter(textNode);
 | |
|           range.setEndAfter(textNode);
 | |
|           selection.removeAllRanges();
 | |
|           selection.addRange(range);
 | |
|           handlers.onInput(new InputEvent('input'));
 | |
|           this.updateLineNumbers(element);
 | |
|         }
 | |
|         return;
 | |
|       }
 | |
|       
 | |
|       // Check cursor position for navigation keys
 | |
|       if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) {
 | |
|         const cursorPos = this.getCursorPosition(element);
 | |
|         const textLength = editor.textContent?.length || 0;
 | |
|         
 | |
|         // For ArrowLeft at position 0 or ArrowRight at end, let parent handle navigation
 | |
|         if ((e.key === 'ArrowLeft' && cursorPos === 0) || 
 | |
|             (e.key === 'ArrowRight' && cursorPos === textLength)) {
 | |
|           // Pass to parent handler for inter-block navigation
 | |
|           handlers.onKeyDown(e);
 | |
|           return;
 | |
|         }
 | |
|         
 | |
|         // For ArrowUp/Down, check if we're at first/last line
 | |
|         if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
 | |
|           const lines = (editor.textContent || '').split('\n');
 | |
|           const currentLine = this.getCurrentLineIndex(editor);
 | |
|           
 | |
|           if ((e.key === 'ArrowUp' && currentLine === 0) ||
 | |
|               (e.key === 'ArrowDown' && currentLine === lines.length - 1)) {
 | |
|             // Let parent handle navigation to prev/next block
 | |
|             handlers.onKeyDown(e);
 | |
|             return;
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|       
 | |
|       // Pass other keys to parent handler
 | |
|       handlers.onKeyDown(e);
 | |
|     });
 | |
|     
 | |
|     // Paste handler - plain text only
 | |
|     editor.addEventListener('paste', (e) => {
 | |
|       e.preventDefault();
 | |
|       const text = e.clipboardData?.getData('text/plain');
 | |
|       if (text) {
 | |
|         const selection = window.getSelection();
 | |
|         if (selection && selection.rangeCount > 0) {
 | |
|           const range = selection.getRangeAt(0);
 | |
|           range.deleteContents();
 | |
|           const textNode = document.createTextNode(text);
 | |
|           range.insertNode(textNode);
 | |
|           range.setStartAfter(textNode);
 | |
|           range.setEndAfter(textNode);
 | |
|           selection.removeAllRanges();
 | |
|           selection.addRange(range);
 | |
|           handlers.onInput(new InputEvent('input'));
 | |
|           this.updateLineNumbers(element);
 | |
|         }
 | |
|       }
 | |
|     });
 | |
|     
 | |
|     // Composition handlers
 | |
|     editor.addEventListener('compositionstart', () => handlers.onCompositionStart());
 | |
|     editor.addEventListener('compositionend', () => handlers.onCompositionEnd());
 | |
|     
 | |
|     // Initial syntax highlighting if content exists and not focused
 | |
|     if (block.content && document.activeElement !== editor) {
 | |
|       requestAnimationFrame(() => {
 | |
|         this.applyHighlighting(element, block);
 | |
|       });
 | |
|     }
 | |
|   }
 | |
|   
 | |
|   private updateLineNumbers(element: HTMLElement): void {
 | |
|     const editor = element.querySelector('.code-editor') as HTMLElement;
 | |
|     const lineNumbersContainer = element.querySelector('.line-numbers') as HTMLElement;
 | |
|     
 | |
|     if (!editor || !lineNumbersContainer) return;
 | |
|     
 | |
|     const content = editor.textContent || '';
 | |
|     const lines = content.split('\n');
 | |
|     const lineCount = lines.length || 1;
 | |
|     
 | |
|     let lineNumbersHtml = '';
 | |
|     for (let i = 1; i <= lineCount; i++) {
 | |
|       lineNumbersHtml += `<div class="line-number">${i}</div>`;
 | |
|     }
 | |
|     
 | |
|     lineNumbersContainer.innerHTML = lineNumbersHtml;
 | |
|   }
 | |
|   
 | |
|   private getCurrentLineIndex(editor: HTMLElement): number {
 | |
|     const selection = window.getSelection();
 | |
|     if (!selection || selection.rangeCount === 0) return 0;
 | |
|     
 | |
|     const range = selection.getRangeAt(0);
 | |
|     const preCaretRange = range.cloneRange();
 | |
|     preCaretRange.selectNodeContents(editor);
 | |
|     preCaretRange.setEnd(range.startContainer, range.startOffset);
 | |
|     
 | |
|     const textBeforeCursor = preCaretRange.toString();
 | |
|     const linesBeforeCursor = textBeforeCursor.split('\n');
 | |
|     
 | |
|     return linesBeforeCursor.length - 1; // 0-indexed
 | |
|   }
 | |
|   
 | |
|   private applyHighlighting(element: HTMLElement, block: IBlock): void {
 | |
|     const editor = element.querySelector('.code-editor') as HTMLElement;
 | |
|     if (!editor) return;
 | |
|     
 | |
|     // Store cursor position
 | |
|     const cursorPos = this.getCursorPosition(element);
 | |
|     
 | |
|     // Get plain text content
 | |
|     const content = editor.textContent || '';
 | |
|     const language = block.metadata?.language || 'typescript';
 | |
|     
 | |
|     // Apply highlighting
 | |
|     try {
 | |
|       const result = hlight.highlight(content, { 
 | |
|         language: language,
 | |
|         ignoreIllegals: true 
 | |
|       });
 | |
|       
 | |
|       // Only update if we have valid highlighted content
 | |
|       if (result.value) {
 | |
|         editor.innerHTML = result.value;
 | |
|         
 | |
|         // Restore cursor position if editor is focused
 | |
|         if (document.activeElement === editor && cursorPos !== null) {
 | |
|           requestAnimationFrame(() => {
 | |
|             WysiwygSelection.setCursorPosition(editor, cursorPos);
 | |
|           });
 | |
|         }
 | |
|       }
 | |
|     } catch (error) {
 | |
|       // If highlighting fails, keep plain text
 | |
|       console.warn('Syntax highlighting failed:', error);
 | |
|     }
 | |
|   }
 | |
|   
 | |
|   private escapeHtml(text: string): string {
 | |
|     const div = document.createElement('div');
 | |
|     div.textContent = text;
 | |
|     return div.innerHTML;
 | |
|   }
 | |
|   
 | |
|   getContent(element: HTMLElement): string {
 | |
|     const editor = element.querySelector('.code-editor') as HTMLElement;
 | |
|     return editor?.textContent || '';
 | |
|   }
 | |
|   
 | |
|   setContent(element: HTMLElement, content: string): void {
 | |
|     const editor = element.querySelector('.code-editor') as HTMLElement;
 | |
|     if (!editor) return;
 | |
|     
 | |
|     editor.textContent = content;
 | |
|     this.updateLineNumbers(element);
 | |
|     
 | |
|     // Apply highlighting if not focused
 | |
|     if (document.activeElement !== editor) {
 | |
|       const block: IBlock = {
 | |
|         id: editor.dataset.blockId || '',
 | |
|         type: 'code',
 | |
|         content: content,
 | |
|         metadata: { 
 | |
|           language: element.querySelector('.code-block-container')?.getAttribute('data-language') || 'typescript'
 | |
|         }
 | |
|       };
 | |
|       this.applyHighlighting(element, block);
 | |
|     }
 | |
|   }
 | |
|   
 | |
|   getCursorPosition(element: HTMLElement): number | null {
 | |
|     const editor = element.querySelector('.code-editor') as HTMLElement;
 | |
|     if (!editor) return null;
 | |
|     
 | |
|     const selection = window.getSelection();
 | |
|     if (!selection || selection.rangeCount === 0) return null;
 | |
|     
 | |
|     const range = selection.getRangeAt(0);
 | |
|     if (!editor.contains(range.startContainer)) return null;
 | |
|     
 | |
|     const preCaretRange = document.createRange();
 | |
|     preCaretRange.selectNodeContents(editor);
 | |
|     preCaretRange.setEnd(range.startContainer, range.startOffset);
 | |
|     
 | |
|     return preCaretRange.toString().length;
 | |
|   }
 | |
|   
 | |
|   setCursorToStart(element: HTMLElement): void {
 | |
|     const editor = element.querySelector('.code-editor') as HTMLElement;
 | |
|     if (editor) {
 | |
|       WysiwygSelection.setCursorPosition(editor, 0);
 | |
|     }
 | |
|   }
 | |
|   
 | |
|   setCursorToEnd(element: HTMLElement): void {
 | |
|     const editor = element.querySelector('.code-editor') as HTMLElement;
 | |
|     if (editor) {
 | |
|       const length = editor.textContent?.length || 0;
 | |
|       WysiwygSelection.setCursorPosition(editor, length);
 | |
|     }
 | |
|   }
 | |
|   
 | |
|   focus(element: HTMLElement): void {
 | |
|     const editor = element.querySelector('.code-editor') as HTMLElement;
 | |
|     editor?.focus();
 | |
|   }
 | |
|   
 | |
|   focusWithCursor(element: HTMLElement, position: 'start' | 'end' | number = 'end'): void {
 | |
|     const editor = element.querySelector('.code-editor') as HTMLElement;
 | |
|     if (!editor) return;
 | |
|     
 | |
|     editor.focus();
 | |
|     
 | |
|     requestAnimationFrame(() => {
 | |
|       if (position === 'start') {
 | |
|         this.setCursorToStart(element);
 | |
|       } else if (position === 'end') {
 | |
|         this.setCursorToEnd(element);
 | |
|       } else if (typeof position === 'number') {
 | |
|         WysiwygSelection.setCursorPosition(editor, position);
 | |
|       }
 | |
|     });
 | |
|   }
 | |
|   
 | |
|   getSplitContent(element: HTMLElement): { before: string; after: string } | null {
 | |
|     const position = this.getCursorPosition(element);
 | |
|     if (position === null) return null;
 | |
|     
 | |
|     const content = this.getContent(element);
 | |
|     return {
 | |
|       before: content.substring(0, position),
 | |
|       after: content.substring(position)
 | |
|     };
 | |
|   }
 | |
|   
 | |
|   getStyles(): string {
 | |
|     return `
 | |
|       /* Code Block Container - Minimalist shadcn style */
 | |
|       .code-block-container {
 | |
|         position: relative;
 | |
|         margin: 12px 0;
 | |
|         background: transparent;
 | |
|         border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
 | |
|         border-radius: 6px;
 | |
|         overflow: hidden;
 | |
|         transition: all 0.15s ease;
 | |
|       }
 | |
|       
 | |
|       .code-block-container.selected {
 | |
|         border-color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
 | |
|       }
 | |
|       
 | |
|       .code-block-container.editing {
 | |
|         border-color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
 | |
|         background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
 | |
|       }
 | |
|       
 | |
|       /* Header - Simplified */
 | |
|       .code-header {
 | |
|         background: transparent;
 | |
|         border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
 | |
|         padding: 8px 12px;
 | |
|         display: flex;
 | |
|         justify-content: space-between;
 | |
|         align-items: center;
 | |
|       }
 | |
|       
 | |
|       .language-selector {
 | |
|         font-size: 12px;
 | |
|         color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
 | |
|         font-weight: 500;
 | |
|         text-transform: uppercase;
 | |
|         letter-spacing: 0.05em;
 | |
|         font-family: ${cssGeistFontFamily};
 | |
|         background: transparent;
 | |
|         border: 1px solid transparent;
 | |
|         border-radius: 4px;
 | |
|         padding: 4px 8px;
 | |
|         cursor: pointer;
 | |
|         transition: all 0.15s ease;
 | |
|         outline: none;
 | |
|       }
 | |
|       
 | |
|       .language-selector:hover {
 | |
|         background: ${cssManager.bdTheme('#f9fafb', '#1f2937')};
 | |
|         border-color: ${cssManager.bdTheme('#e5e7eb', '#374151')};
 | |
|         color: ${cssManager.bdTheme('#374151', '#e5e7eb')};
 | |
|       }
 | |
|       
 | |
|       .language-selector:focus {
 | |
|         border-color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
 | |
|       }
 | |
|       
 | |
|       /* Copy Button - Minimal */
 | |
|       .copy-button {
 | |
|         display: flex;
 | |
|         align-items: center;
 | |
|         gap: 4px;
 | |
|         padding: 4px 8px;
 | |
|         background: transparent;
 | |
|         border: 1px solid transparent;
 | |
|         border-radius: 4px;
 | |
|         color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
 | |
|         font-size: 12px;
 | |
|         font-family: ${cssGeistFontFamily};
 | |
|         cursor: pointer;
 | |
|         transition: all 0.15s ease;
 | |
|         outline: none;
 | |
|       }
 | |
|       
 | |
|       .copy-button:hover {
 | |
|         background: ${cssManager.bdTheme('#f9fafb', '#1f2937')};
 | |
|         border-color: ${cssManager.bdTheme('#e5e7eb', '#374151')};
 | |
|         color: ${cssManager.bdTheme('#374151', '#e5e7eb')};
 | |
|       }
 | |
|       
 | |
|       .copy-button:active {
 | |
|         transform: scale(0.98);
 | |
|       }
 | |
|       
 | |
|       .copy-button.copied {
 | |
|         color: ${cssManager.bdTheme('#059669', '#10b981')};
 | |
|       }
 | |
|       
 | |
|       .copy-icon {
 | |
|         flex-shrink: 0;
 | |
|         opacity: 0.7;
 | |
|       }
 | |
|       
 | |
|       .copy-button:hover .copy-icon {
 | |
|         opacity: 1;
 | |
|       }
 | |
|       
 | |
|       .copy-text {
 | |
|         min-width: 40px;
 | |
|         text-align: center;
 | |
|       }
 | |
|       
 | |
|       /* Code Body */
 | |
|       .code-body {
 | |
|         display: flex;
 | |
|         position: relative;
 | |
|         background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
 | |
|       }
 | |
|       
 | |
|       /* Line Numbers - Subtle */
 | |
|       .line-numbers {
 | |
|         flex-shrink: 0;
 | |
|         padding: 12px 0;
 | |
|         background: transparent;
 | |
|         text-align: right;
 | |
|         user-select: none;
 | |
|         min-width: 40px;
 | |
|         border-right: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
 | |
|       }
 | |
|       
 | |
|       .line-number {
 | |
|         padding: 0 12px 0 8px;
 | |
|         color: ${cssManager.bdTheme('#9ca3af', '#4b5563')};
 | |
|         font-family: ${cssMonoFontFamily};
 | |
|         font-size: 13px;
 | |
|         line-height: 20px;
 | |
|         height: 20px;
 | |
|       }
 | |
|       
 | |
|       /* Code Content */
 | |
|       .code-content {
 | |
|         flex: 1;
 | |
|         overflow-x: auto;
 | |
|         position: relative;
 | |
|       }
 | |
|       
 | |
|       .code-pre {
 | |
|         margin: 0;
 | |
|         padding: 0;
 | |
|         background: transparent;
 | |
|       }
 | |
|       
 | |
|       .code-editor {
 | |
|         display: block;
 | |
|         padding: 12px 16px;
 | |
|         margin: 0;
 | |
|         font-family: ${cssMonoFontFamily};
 | |
|         font-size: 13px;
 | |
|         line-height: 20px;
 | |
|         color: ${cssManager.bdTheme('#111827', '#f9fafb')};
 | |
|         background: transparent;
 | |
|         border: none;
 | |
|         outline: none;
 | |
|         white-space: pre-wrap;
 | |
|         word-wrap: break-word;
 | |
|         min-height: 60px;
 | |
|         overflow: visible;
 | |
|       }
 | |
|       
 | |
|       /* Placeholder */
 | |
|       .code-editor:empty::before {
 | |
|         content: "// Type or paste code here...";
 | |
|         color: ${cssManager.bdTheme('#9ca3af', '#4b5563')};
 | |
|         pointer-events: none;
 | |
|       }
 | |
|       
 | |
|       /* When editing (focused), show grey text without highlighting */
 | |
|       .code-block-container.editing .code-editor {
 | |
|         color: ${cssManager.bdTheme('#6b7280', '#9ca3af')} !important;
 | |
|       }
 | |
|       
 | |
|       .code-block-container.editing .code-editor * {
 | |
|         color: inherit !important;
 | |
|       }
 | |
|       
 | |
|       /* Syntax Highlighting - Muted colors */
 | |
|       .code-editor .hljs-keyword {
 | |
|         color: ${cssManager.bdTheme('#dc2626', '#f87171')};
 | |
|         font-weight: 500;
 | |
|       }
 | |
|       
 | |
|       .code-editor .hljs-string {
 | |
|         color: ${cssManager.bdTheme('#059669', '#10b981')};
 | |
|       }
 | |
|       
 | |
|       .code-editor .hljs-number {
 | |
|         color: ${cssManager.bdTheme('#7c3aed', '#a78bfa')};
 | |
|       }
 | |
|       
 | |
|       .code-editor .hljs-function {
 | |
|         color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
 | |
|       }
 | |
|       
 | |
|       .code-editor .hljs-comment {
 | |
|         color: ${cssManager.bdTheme('#6b7280', '#6b7280')};
 | |
|         font-style: italic;
 | |
|       }
 | |
|       
 | |
|       .code-editor .hljs-variable,
 | |
|       .code-editor .hljs-attr {
 | |
|         color: ${cssManager.bdTheme('#ea580c', '#fb923c')};
 | |
|       }
 | |
|       
 | |
|       .code-editor .hljs-class,
 | |
|       .code-editor .hljs-title {
 | |
|         color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
 | |
|         font-weight: 500;
 | |
|       }
 | |
|       
 | |
|       .code-editor .hljs-params {
 | |
|         color: ${cssManager.bdTheme('#374151', '#e5e7eb')};
 | |
|       }
 | |
|       
 | |
|       .code-editor .hljs-built_in {
 | |
|         color: ${cssManager.bdTheme('#7c3aed', '#a78bfa')};
 | |
|       }
 | |
|       
 | |
|       .code-editor .hljs-literal {
 | |
|         color: ${cssManager.bdTheme('#7c3aed', '#a78bfa')};
 | |
|       }
 | |
|       
 | |
|       .code-editor .hljs-meta {
 | |
|         color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
 | |
|       }
 | |
|       
 | |
|       .code-editor .hljs-punctuation {
 | |
|         color: ${cssManager.bdTheme('#374151', '#d1d5db')};
 | |
|       }
 | |
|       
 | |
|       .code-editor .hljs-tag {
 | |
|         color: ${cssManager.bdTheme('#dc2626', '#f87171')};
 | |
|       }
 | |
|       
 | |
|       .code-editor .hljs-attribute {
 | |
|         color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
 | |
|       }
 | |
|       
 | |
|       .code-editor .hljs-selector-tag {
 | |
|         color: ${cssManager.bdTheme('#dc2626', '#f87171')};
 | |
|       }
 | |
|       
 | |
|       .code-editor .hljs-selector-class {
 | |
|         color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
 | |
|       }
 | |
|       
 | |
|       .code-editor .hljs-selector-id {
 | |
|         color: ${cssManager.bdTheme('#7c3aed', '#a78bfa')};
 | |
|       }
 | |
|       
 | |
|       /* Selection */
 | |
|       .code-editor::selection,
 | |
|       .code-editor *::selection {
 | |
|         background: ${cssManager.bdTheme('rgba(99, 102, 241, 0.2)', 'rgba(99, 102, 241, 0.3)')};
 | |
|       }
 | |
|       
 | |
|       /* Scrollbar styling - Minimal */
 | |
|       .code-content::-webkit-scrollbar {
 | |
|         height: 6px;
 | |
|       }
 | |
|       
 | |
|       .code-content::-webkit-scrollbar-track {
 | |
|         background: transparent;
 | |
|       }
 | |
|       
 | |
|       .code-content::-webkit-scrollbar-thumb {
 | |
|         background: ${cssManager.bdTheme('#d1d5db', '#4b5563')};
 | |
|         border-radius: 3px;
 | |
|       }
 | |
|       
 | |
|       .code-content::-webkit-scrollbar-thumb:hover {
 | |
|         background: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
 | |
|       }
 | |
|     `;
 | |
|   }
 | |
| } |