562 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
		
		
			
		
	
	
			562 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
|  | import { BaseBlockHandler, type IBlockEventHandlers } from '../block.base.js'; | ||
|  | import type { IBlock } from '../../wysiwyg.types.js'; | ||
|  | import { cssManager } from '@design.estate/dees-element'; | ||
|  | 
 | ||
|  | /** | ||
|  |  * MarkdownBlockHandler - Handles markdown content with preview/edit toggle | ||
|  |  *  | ||
|  |  * Features: | ||
|  |  * - Live markdown preview | ||
|  |  * - Edit/preview mode toggle | ||
|  |  * - Syntax highlighting in edit mode | ||
|  |  * - Common markdown shortcuts | ||
|  |  * - Auto-save on mode switch | ||
|  |  */ | ||
|  | export class MarkdownBlockHandler extends BaseBlockHandler { | ||
|  |   type = 'markdown'; | ||
|  |    | ||
|  |   render(block: IBlock, isSelected: boolean): string { | ||
|  |     const isEditMode = block.metadata?.isEditMode ?? true; | ||
|  |     const content = block.content || ''; | ||
|  |      | ||
|  |     return `
 | ||
|  |       <div class="markdown-block-container${isSelected ? ' selected' : ''}"  | ||
|  |            data-block-id="${block.id}" | ||
|  |            data-edit-mode="${isEditMode}"> | ||
|  |         <div class="markdown-header"> | ||
|  |           <div class="markdown-icon">M↓</div> | ||
|  |           <div class="markdown-title">Markdown</div> | ||
|  |           <button class="markdown-toggle-mode" title="${isEditMode ? 'Preview' : 'Edit'}"> | ||
|  |             ${isEditMode ? '👁️' : '✏️'} | ||
|  |           </button> | ||
|  |         </div> | ||
|  |         <div class="markdown-content"> | ||
|  |           ${isEditMode ? this.renderEditor(content) : this.renderPreview(content)} | ||
|  |         </div> | ||
|  |       </div> | ||
|  |     `;
 | ||
|  |   } | ||
|  |    | ||
|  |   private renderEditor(content: string): string { | ||
|  |     return `
 | ||
|  |       <textarea class="markdown-editor"  | ||
|  |                 placeholder="Enter markdown content..." | ||
|  |                 spellcheck="false">${this.escapeHtml(content)}</textarea> | ||
|  |     `;
 | ||
|  |   } | ||
|  |    | ||
|  |   private renderPreview(content: string): string { | ||
|  |     const html = this.parseMarkdown(content); | ||
|  |     return `
 | ||
|  |       <div class="markdown-preview"> | ||
|  |         ${html || '<div class="preview-empty">No content to preview</div>'} | ||
|  |       </div> | ||
|  |     `;
 | ||
|  |   } | ||
|  |    | ||
|  |   setup(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void { | ||
|  |     const container = element.querySelector('.markdown-block-container') as HTMLElement; | ||
|  |     const toggleBtn = element.querySelector('.markdown-toggle-mode') as HTMLButtonElement; | ||
|  |      | ||
|  |     if (!container || !toggleBtn) { | ||
|  |       console.error('MarkdownBlockHandler: Could not find required elements'); | ||
|  |       return; | ||
|  |     } | ||
|  |      | ||
|  |     // Initialize metadata
 | ||
|  |     if (!block.metadata) block.metadata = {}; | ||
|  |     if (block.metadata.isEditMode === undefined) block.metadata.isEditMode = true; | ||
|  |      | ||
|  |     // Toggle mode button
 | ||
|  |     toggleBtn.addEventListener('click', (e) => { | ||
|  |       e.preventDefault(); | ||
|  |       e.stopPropagation(); | ||
|  |        | ||
|  |       // Save current content if in edit mode
 | ||
|  |       if (block.metadata.isEditMode) { | ||
|  |         const editor = container.querySelector('.markdown-editor') as HTMLTextAreaElement; | ||
|  |         if (editor) { | ||
|  |           block.content = editor.value; | ||
|  |         } | ||
|  |       } | ||
|  |        | ||
|  |       // Toggle mode
 | ||
|  |       block.metadata.isEditMode = !block.metadata.isEditMode; | ||
|  |        | ||
|  |       // Request UI update
 | ||
|  |       handlers.onRequestUpdate?.(); | ||
|  |     }); | ||
|  |      | ||
|  |     // Setup based on mode
 | ||
|  |     if (block.metadata.isEditMode) { | ||
|  |       this.setupEditor(element, block, handlers); | ||
|  |     } else { | ||
|  |       this.setupPreview(element, block, handlers); | ||
|  |     } | ||
|  |   } | ||
|  |    | ||
|  |   private setupEditor(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void { | ||
|  |     const editor = element.querySelector('.markdown-editor') as HTMLTextAreaElement; | ||
|  |     if (!editor) return; | ||
|  |      | ||
|  |     // Focus handling
 | ||
|  |     editor.addEventListener('focus', () => handlers.onFocus()); | ||
|  |     editor.addEventListener('blur', () => handlers.onBlur()); | ||
|  |      | ||
|  |     // Content changes
 | ||
|  |     editor.addEventListener('input', () => { | ||
|  |       block.content = editor.value; | ||
|  |     }); | ||
|  |      | ||
|  |     // Keyboard shortcuts
 | ||
|  |     editor.addEventListener('keydown', (e) => { | ||
|  |       // Tab handling for indentation
 | ||
|  |       if (e.key === 'Tab') { | ||
|  |         e.preventDefault(); | ||
|  |         const start = editor.selectionStart; | ||
|  |         const end = editor.selectionEnd; | ||
|  |         const value = editor.value; | ||
|  |          | ||
|  |         if (e.shiftKey) { | ||
|  |           // Unindent
 | ||
|  |           const beforeCursor = value.substring(0, start); | ||
|  |           const lastNewline = beforeCursor.lastIndexOf('\n'); | ||
|  |           const lineStart = lastNewline + 1; | ||
|  |           const lineContent = value.substring(lineStart, start); | ||
|  |            | ||
|  |           if (lineContent.startsWith('  ')) { | ||
|  |             editor.value = value.substring(0, lineStart) + lineContent.substring(2) + value.substring(start); | ||
|  |             editor.selectionStart = editor.selectionEnd = start - 2; | ||
|  |           } | ||
|  |         } else { | ||
|  |           // Indent
 | ||
|  |           editor.value = value.substring(0, start) + '  ' + value.substring(end); | ||
|  |           editor.selectionStart = editor.selectionEnd = start + 2; | ||
|  |         } | ||
|  |          | ||
|  |         block.content = editor.value; | ||
|  |         return; | ||
|  |       } | ||
|  |        | ||
|  |       // Bold shortcut (Ctrl/Cmd + B)
 | ||
|  |       if ((e.ctrlKey || e.metaKey) && e.key === 'b') { | ||
|  |         e.preventDefault(); | ||
|  |         this.wrapSelection(editor, '**', '**'); | ||
|  |         block.content = editor.value; | ||
|  |         return; | ||
|  |       } | ||
|  |        | ||
|  |       // Italic shortcut (Ctrl/Cmd + I)
 | ||
|  |       if ((e.ctrlKey || e.metaKey) && e.key === 'i') { | ||
|  |         e.preventDefault(); | ||
|  |         this.wrapSelection(editor, '_', '_'); | ||
|  |         block.content = editor.value; | ||
|  |         return; | ||
|  |       } | ||
|  |        | ||
|  |       // Link shortcut (Ctrl/Cmd + K)
 | ||
|  |       if ((e.ctrlKey || e.metaKey) && e.key === 'k') { | ||
|  |         e.preventDefault(); | ||
|  |         this.insertLink(editor); | ||
|  |         block.content = editor.value; | ||
|  |         return; | ||
|  |       } | ||
|  |        | ||
|  |       // Pass other key events to handlers
 | ||
|  |       handlers.onKeyDown(e); | ||
|  |     }); | ||
|  |      | ||
|  |     // Auto-resize
 | ||
|  |     this.autoResize(editor); | ||
|  |     editor.addEventListener('input', () => this.autoResize(editor)); | ||
|  |   } | ||
|  |    | ||
|  |   private setupPreview(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void { | ||
|  |     const container = element.querySelector('.markdown-block-container') as HTMLElement; | ||
|  |     const preview = element.querySelector('.markdown-preview') as HTMLElement; | ||
|  |      | ||
|  |     if (!container || !preview) return; | ||
|  |      | ||
|  |     // Make preview focusable
 | ||
|  |     preview.setAttribute('tabindex', '0'); | ||
|  |      | ||
|  |     // Focus handling
 | ||
|  |     preview.addEventListener('focus', () => handlers.onFocus()); | ||
|  |     preview.addEventListener('blur', () => handlers.onBlur()); | ||
|  |      | ||
|  |     // Keyboard navigation
 | ||
|  |     preview.addEventListener('keydown', (e) => { | ||
|  |       // Switch to edit mode on Enter
 | ||
|  |       if (e.key === 'Enter' && !e.shiftKey) { | ||
|  |         e.preventDefault(); | ||
|  |         block.metadata.isEditMode = true; | ||
|  |         handlers.onRequestUpdate?.(); | ||
|  |         return; | ||
|  |       } | ||
|  |        | ||
|  |       handlers.onKeyDown(e); | ||
|  |     }); | ||
|  |   } | ||
|  |    | ||
|  |   private wrapSelection(editor: HTMLTextAreaElement, before: string, after: string): void { | ||
|  |     const start = editor.selectionStart; | ||
|  |     const end = editor.selectionEnd; | ||
|  |     const selectedText = editor.value.substring(start, end); | ||
|  |     const replacement = before + (selectedText || 'text') + after; | ||
|  |      | ||
|  |     editor.value = editor.value.substring(0, start) + replacement + editor.value.substring(end); | ||
|  |      | ||
|  |     if (selectedText) { | ||
|  |       editor.selectionStart = start; | ||
|  |       editor.selectionEnd = start + replacement.length; | ||
|  |     } else { | ||
|  |       editor.selectionStart = start + before.length; | ||
|  |       editor.selectionEnd = start + before.length + 4; // 'text'.length
 | ||
|  |     } | ||
|  |      | ||
|  |     editor.focus(); | ||
|  |   } | ||
|  |    | ||
|  |   private insertLink(editor: HTMLTextAreaElement): void { | ||
|  |     const start = editor.selectionStart; | ||
|  |     const end = editor.selectionEnd; | ||
|  |     const selectedText = editor.value.substring(start, end); | ||
|  |     const linkText = selectedText || 'link text'; | ||
|  |     const replacement = `[${linkText}](url)`; | ||
|  |      | ||
|  |     editor.value = editor.value.substring(0, start) + replacement + editor.value.substring(end); | ||
|  |      | ||
|  |     // Select the URL part
 | ||
|  |     editor.selectionStart = start + linkText.length + 3; // '[linktext]('.length
 | ||
|  |     editor.selectionEnd = start + linkText.length + 6; // '[linktext](url'.length
 | ||
|  |      | ||
|  |     editor.focus(); | ||
|  |   } | ||
|  |    | ||
|  |   private autoResize(editor: HTMLTextAreaElement): void { | ||
|  |     editor.style.height = 'auto'; | ||
|  |     editor.style.height = editor.scrollHeight + 'px'; | ||
|  |   } | ||
|  |    | ||
|  |   private parseMarkdown(markdown: string): string { | ||
|  |     // Basic markdown parsing - in production, use a proper markdown parser
 | ||
|  |     let html = this.escapeHtml(markdown); | ||
|  |      | ||
|  |     // Headers
 | ||
|  |     html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>'); | ||
|  |     html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>'); | ||
|  |     html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>'); | ||
|  |      | ||
|  |     // Bold
 | ||
|  |     html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>'); | ||
|  |     html = html.replace(/__(.+?)__/g, '<strong>$1</strong>'); | ||
|  |      | ||
|  |     // Italic
 | ||
|  |     html = html.replace(/\*(.+?)\*/g, '<em>$1</em>'); | ||
|  |     html = html.replace(/_(.+?)_/g, '<em>$1</em>'); | ||
|  |      | ||
|  |     // Code blocks
 | ||
|  |     html = html.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>'); | ||
|  |      | ||
|  |     // Inline code
 | ||
|  |     html = html.replace(/`(.+?)`/g, '<code>$1</code>'); | ||
|  |      | ||
|  |     // Links
 | ||
|  |     html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>'); | ||
|  |      | ||
|  |     // Lists
 | ||
|  |     html = html.replace(/^\* (.+)$/gm, '<li>$1</li>'); | ||
|  |     html = html.replace(/^- (.+)$/gm, '<li>$1</li>'); | ||
|  |     html = html.replace(/^\d+\. (.+)$/gm, '<li>$1</li>'); | ||
|  |      | ||
|  |     // Wrap consecutive list items
 | ||
|  |     html = html.replace(/(<li>.*<\/li>\n?)+/g, (match) => { | ||
|  |       return '<ul>' + match + '</ul>'; | ||
|  |     }); | ||
|  |      | ||
|  |     // Paragraphs
 | ||
|  |     html = html.replace(/\n\n/g, '</p><p>'); | ||
|  |     html = '<p>' + html + '</p>'; | ||
|  |      | ||
|  |     // Clean up empty paragraphs
 | ||
|  |     html = html.replace(/<p><\/p>/g, ''); | ||
|  |     html = html.replace(/<p>(<h[1-3]>)/g, '$1'); | ||
|  |     html = html.replace(/(<\/h[1-3]>)<\/p>/g, '$1'); | ||
|  |     html = html.replace(/<p>(<ul>)/g, '$1'); | ||
|  |     html = html.replace(/(<\/ul>)<\/p>/g, '$1'); | ||
|  |     html = html.replace(/<p>(<pre>)/g, '$1'); | ||
|  |     html = html.replace(/(<\/pre>)<\/p>/g, '$1'); | ||
|  |      | ||
|  |     return html; | ||
|  |   } | ||
|  |    | ||
|  |   private escapeHtml(text: string): string { | ||
|  |     const div = document.createElement('div'); | ||
|  |     div.textContent = text; | ||
|  |     return div.innerHTML; | ||
|  |   } | ||
|  |    | ||
|  |   getContent(element: HTMLElement): string { | ||
|  |     const editor = element.querySelector('.markdown-editor') as HTMLTextAreaElement; | ||
|  |     if (editor) { | ||
|  |       return editor.value; | ||
|  |     } | ||
|  |      | ||
|  |     // If in preview mode, return the stored content
 | ||
|  |     const container = element.querySelector('.markdown-block-container'); | ||
|  |     const blockId = container?.getAttribute('data-block-id'); | ||
|  |     // In real implementation, would need access to block data
 | ||
|  |     return ''; | ||
|  |   } | ||
|  |    | ||
|  |   setContent(element: HTMLElement, content: string): void { | ||
|  |     const editor = element.querySelector('.markdown-editor') as HTMLTextAreaElement; | ||
|  |     if (editor) { | ||
|  |       editor.value = content; | ||
|  |       this.autoResize(editor); | ||
|  |     } | ||
|  |   } | ||
|  |    | ||
|  |   getCursorPosition(element: HTMLElement): number | null { | ||
|  |     const editor = element.querySelector('.markdown-editor') as HTMLTextAreaElement; | ||
|  |     return editor ? editor.selectionStart : null; | ||
|  |   } | ||
|  |    | ||
|  |   setCursorToStart(element: HTMLElement): void { | ||
|  |     const editor = element.querySelector('.markdown-editor') as HTMLTextAreaElement; | ||
|  |     if (editor) { | ||
|  |       editor.selectionStart = editor.selectionEnd = 0; | ||
|  |       editor.focus(); | ||
|  |     } else { | ||
|  |       this.focus(element); | ||
|  |     } | ||
|  |   } | ||
|  |    | ||
|  |   setCursorToEnd(element: HTMLElement): void { | ||
|  |     const editor = element.querySelector('.markdown-editor') as HTMLTextAreaElement; | ||
|  |     if (editor) { | ||
|  |       const length = editor.value.length; | ||
|  |       editor.selectionStart = editor.selectionEnd = length; | ||
|  |       editor.focus(); | ||
|  |     } else { | ||
|  |       this.focus(element); | ||
|  |     } | ||
|  |   } | ||
|  |    | ||
|  |   focus(element: HTMLElement): void { | ||
|  |     const editor = element.querySelector('.markdown-editor') as HTMLTextAreaElement; | ||
|  |     if (editor) { | ||
|  |       editor.focus(); | ||
|  |     } else { | ||
|  |       const preview = element.querySelector('.markdown-preview') as HTMLElement; | ||
|  |       preview?.focus(); | ||
|  |     } | ||
|  |   } | ||
|  |    | ||
|  |   focusWithCursor(element: HTMLElement, position: 'start' | 'end' | number = 'end'): void { | ||
|  |     const editor = element.querySelector('.markdown-editor') as HTMLTextAreaElement; | ||
|  |     if (editor) { | ||
|  |       if (position === 'start') { | ||
|  |         this.setCursorToStart(element); | ||
|  |       } else if (position === 'end') { | ||
|  |         this.setCursorToEnd(element); | ||
|  |       } else if (typeof position === 'number') { | ||
|  |         editor.selectionStart = editor.selectionEnd = position; | ||
|  |         editor.focus(); | ||
|  |       } | ||
|  |     } else { | ||
|  |       this.focus(element); | ||
|  |     } | ||
|  |   } | ||
|  |    | ||
|  |   getSplitContent(element: HTMLElement): { before: string; after: string } | null { | ||
|  |     const editor = element.querySelector('.markdown-editor') as HTMLTextAreaElement; | ||
|  |     if (!editor) return null; | ||
|  |      | ||
|  |     const cursorPos = editor.selectionStart; | ||
|  |     return { | ||
|  |       before: editor.value.substring(0, cursorPos), | ||
|  |       after: editor.value.substring(cursorPos) | ||
|  |     }; | ||
|  |   } | ||
|  |    | ||
|  |   getStyles(): string { | ||
|  |     return `
 | ||
|  |       /* Markdown Block Container */ | ||
|  |       .markdown-block-container { | ||
|  |         position: relative; | ||
|  |         margin: 12px 0; | ||
|  |         border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')}; | ||
|  |         border-radius: 6px; | ||
|  |         overflow: hidden; | ||
|  |         transition: all 0.15s ease; | ||
|  |         background: ${cssManager.bdTheme('#ffffff', '#111827')}; | ||
|  |       } | ||
|  |        | ||
|  |       .markdown-block-container.selected { | ||
|  |         border-color: ${cssManager.bdTheme('#9ca3af', '#6b7280')}; | ||
|  |       } | ||
|  |        | ||
|  |       /* Header */ | ||
|  |       .markdown-header { | ||
|  |         display: flex; | ||
|  |         align-items: center; | ||
|  |         gap: 8px; | ||
|  |         padding: 8px 12px; | ||
|  |         border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')}; | ||
|  |         background: ${cssManager.bdTheme('#f9fafb', '#0a0a0a')}; | ||
|  |       } | ||
|  |        | ||
|  |       .markdown-icon { | ||
|  |         font-size: 14px; | ||
|  |         font-weight: 600; | ||
|  |         opacity: 0.8; | ||
|  |       } | ||
|  |        | ||
|  |       .markdown-title { | ||
|  |         flex: 1; | ||
|  |         font-size: 13px; | ||
|  |         font-weight: 500; | ||
|  |         color: ${cssManager.bdTheme('#374151', '#e5e7eb')}; | ||
|  |       } | ||
|  |        | ||
|  |       .markdown-toggle-mode { | ||
|  |         padding: 4px 8px; | ||
|  |         background: transparent; | ||
|  |         border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')}; | ||
|  |         border-radius: 4px; | ||
|  |         font-size: 14px; | ||
|  |         cursor: pointer; | ||
|  |         transition: all 0.15s ease; | ||
|  |       } | ||
|  |        | ||
|  |       .markdown-toggle-mode:hover { | ||
|  |         background: ${cssManager.bdTheme('#f3f4f6', '#1f2937')}; | ||
|  |         border-color: ${cssManager.bdTheme('#d1d5db', '#4b5563')}; | ||
|  |       } | ||
|  |        | ||
|  |       /* Content */ | ||
|  |       .markdown-content { | ||
|  |         position: relative; | ||
|  |         min-height: 120px; | ||
|  |       } | ||
|  |        | ||
|  |       /* Editor */ | ||
|  |       .markdown-editor { | ||
|  |         width: 100%; | ||
|  |         min-height: 120px; | ||
|  |         padding: 12px; | ||
|  |         background: transparent; | ||
|  |         border: none; | ||
|  |         outline: none; | ||
|  |         resize: none; | ||
|  |         font-family: 'Monaco', 'Consolas', 'Courier New', monospace; | ||
|  |         font-size: 13px; | ||
|  |         line-height: 1.6; | ||
|  |         color: ${cssManager.bdTheme('#1f2937', '#f3f4f6')}; | ||
|  |         overflow: hidden; | ||
|  |       } | ||
|  |        | ||
|  |       .markdown-editor::placeholder { | ||
|  |         color: ${cssManager.bdTheme('#9ca3af', '#6b7280')}; | ||
|  |       } | ||
|  |        | ||
|  |       /* Preview */ | ||
|  |       .markdown-preview { | ||
|  |         padding: 12px; | ||
|  |         min-height: 96px; | ||
|  |         outline: none; | ||
|  |         font-size: 14px; | ||
|  |         line-height: 1.6; | ||
|  |         color: ${cssManager.bdTheme('#1f2937', '#f3f4f6')}; | ||
|  |       } | ||
|  |        | ||
|  |       .preview-empty { | ||
|  |         color: ${cssManager.bdTheme('#9ca3af', '#6b7280')}; | ||
|  |         font-style: italic; | ||
|  |       } | ||
|  |        | ||
|  |       /* Markdown preview styles */ | ||
|  |       .markdown-preview h1 { | ||
|  |         font-size: 24px; | ||
|  |         font-weight: 600; | ||
|  |         margin: 16px 0 8px 0; | ||
|  |         color: ${cssManager.bdTheme('#111827', '#f9fafb')}; | ||
|  |       } | ||
|  |        | ||
|  |       .markdown-preview h2 { | ||
|  |         font-size: 20px; | ||
|  |         font-weight: 600; | ||
|  |         margin: 14px 0 6px 0; | ||
|  |         color: ${cssManager.bdTheme('#111827', '#f9fafb')}; | ||
|  |       } | ||
|  |        | ||
|  |       .markdown-preview h3 { | ||
|  |         font-size: 18px; | ||
|  |         font-weight: 600; | ||
|  |         margin: 12px 0 4px 0; | ||
|  |         color: ${cssManager.bdTheme('#111827', '#f9fafb')}; | ||
|  |       } | ||
|  |        | ||
|  |       .markdown-preview p { | ||
|  |         margin: 8px 0; | ||
|  |       } | ||
|  |        | ||
|  |       .markdown-preview ul, | ||
|  |       .markdown-preview ol { | ||
|  |         margin: 8px 0; | ||
|  |         padding-left: 24px; | ||
|  |       } | ||
|  |        | ||
|  |       .markdown-preview li { | ||
|  |         margin: 4px 0; | ||
|  |       } | ||
|  |        | ||
|  |       .markdown-preview code { | ||
|  |         background: ${cssManager.bdTheme('#f3f4f6', '#1f2937')}; | ||
|  |         padding: 2px 4px; | ||
|  |         border-radius: 3px; | ||
|  |         font-family: 'Monaco', 'Consolas', 'Courier New', monospace; | ||
|  |         font-size: 0.9em; | ||
|  |       } | ||
|  |        | ||
|  |       .markdown-preview pre { | ||
|  |         background: ${cssManager.bdTheme('#f3f4f6', '#1f2937')}; | ||
|  |         padding: 12px; | ||
|  |         border-radius: 4px; | ||
|  |         overflow-x: auto; | ||
|  |         margin: 8px 0; | ||
|  |       } | ||
|  |        | ||
|  |       .markdown-preview pre code { | ||
|  |         background: transparent; | ||
|  |         padding: 0; | ||
|  |       } | ||
|  |        | ||
|  |       .markdown-preview strong { | ||
|  |         font-weight: 600; | ||
|  |         color: ${cssManager.bdTheme('#111827', '#f9fafb')}; | ||
|  |       } | ||
|  |        | ||
|  |       .markdown-preview em { | ||
|  |         font-style: italic; | ||
|  |       } | ||
|  |        | ||
|  |       .markdown-preview a { | ||
|  |         color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')}; | ||
|  |         text-decoration: none; | ||
|  |       } | ||
|  |        | ||
|  |       .markdown-preview a:hover { | ||
|  |         text-decoration: underline; | ||
|  |       } | ||
|  |        | ||
|  |       .markdown-preview blockquote { | ||
|  |         border-left: 3px solid ${cssManager.bdTheme('#e5e7eb', '#374151')}; | ||
|  |         padding-left: 12px; | ||
|  |         margin: 8px 0; | ||
|  |         color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; | ||
|  |       } | ||
|  |     `;
 | ||
|  |   } | ||
|  | } |