2025-06-24 22:45:50 +00:00
|
|
|
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';
|
2025-06-26 11:41:58 +00:00
|
|
|
import hlight from 'highlight.js';
|
2025-06-24 22:45:50 +00:00
|
|
|
|
2025-06-26 11:41:58 +00:00
|
|
|
/**
|
|
|
|
* New CodeBlockHandler with improved architecture
|
|
|
|
*
|
|
|
|
* Key improvements:
|
|
|
|
* 1. Simpler DOM structure
|
|
|
|
* 2. Better line number handling
|
|
|
|
* 3. Non-intrusive syntax highlighting
|
|
|
|
* 4. Cleaner event handling
|
|
|
|
*/
|
2025-06-24 22:45:50 +00:00
|
|
|
export class CodeBlockHandler extends BaseBlockHandler {
|
|
|
|
type = 'code';
|
|
|
|
|
2025-06-26 11:41:58 +00:00
|
|
|
private highlightTimer: any = null;
|
2025-06-24 22:45:50 +00:00
|
|
|
|
|
|
|
render(block: IBlock, isSelected: boolean): string {
|
2025-06-26 11:41:58 +00:00
|
|
|
const language = block.metadata?.language || 'javascript';
|
|
|
|
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>`;
|
|
|
|
}
|
2025-06-24 22:45:50 +00:00
|
|
|
|
|
|
|
return `
|
2025-06-26 11:41:58 +00:00
|
|
|
<div class="code-block-container${isSelected ? ' selected' : ''}" data-language="${language}">
|
|
|
|
<div class="code-header">
|
|
|
|
<span class="language-label">${language}</span>
|
2025-06-26 11:57:04 +00:00
|
|
|
<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>
|
2025-06-26 11:41:58 +00:00
|
|
|
</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>
|
2025-06-24 22:45:50 +00:00
|
|
|
</div>
|
|
|
|
`;
|
|
|
|
}
|
|
|
|
|
|
|
|
setup(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void {
|
2025-06-26 11:41:58 +00:00
|
|
|
const editor = element.querySelector('.code-editor') as HTMLElement;
|
|
|
|
const container = element.querySelector('.code-block-container') as HTMLElement;
|
2025-06-26 11:57:04 +00:00
|
|
|
const copyButton = element.querySelector('.copy-button') as HTMLButtonElement;
|
2025-06-24 22:45:50 +00:00
|
|
|
|
2025-06-26 11:41:58 +00:00
|
|
|
if (!editor || !container) return;
|
|
|
|
|
2025-06-26 11:57:04 +00:00
|
|
|
// 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);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2025-06-26 11:41:58 +00:00
|
|
|
// Track if we're currently editing
|
|
|
|
let isEditing = false;
|
|
|
|
|
|
|
|
// Focus handler
|
|
|
|
editor.addEventListener('focus', () => {
|
|
|
|
isEditing = true;
|
|
|
|
container.classList.add('editing');
|
|
|
|
handlers.onFocus();
|
|
|
|
});
|
|
|
|
|
|
|
|
// Blur handler
|
|
|
|
editor.addEventListener('blur', () => {
|
|
|
|
isEditing = false;
|
|
|
|
container.classList.remove('editing');
|
|
|
|
// Apply final highlighting on blur
|
|
|
|
this.applyHighlighting(element, block);
|
|
|
|
handlers.onBlur();
|
|
|
|
});
|
2025-06-24 22:45:50 +00:00
|
|
|
|
|
|
|
// Input handler
|
2025-06-26 11:41:58 +00:00
|
|
|
editor.addEventListener('input', (e) => {
|
2025-06-24 22:45:50 +00:00
|
|
|
handlers.onInput(e as InputEvent);
|
|
|
|
|
2025-06-26 11:41:58 +00:00
|
|
|
// Update line numbers
|
|
|
|
this.updateLineNumbers(element);
|
|
|
|
|
|
|
|
// Clear any pending highlight
|
|
|
|
clearTimeout(this.highlightTimer);
|
|
|
|
|
|
|
|
// Schedule highlighting (only if not actively editing)
|
|
|
|
if (!isEditing) {
|
|
|
|
this.highlightTimer = setTimeout(() => {
|
|
|
|
this.applyHighlighting(element, block);
|
|
|
|
}, 500);
|
2025-06-24 22:45:50 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// Keydown handler
|
2025-06-26 11:41:58 +00:00
|
|
|
editor.addEventListener('keydown', (e) => {
|
|
|
|
// Handle Tab
|
2025-06-24 22:45:50 +00:00
|
|
|
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'));
|
2025-06-26 11:41:58 +00:00
|
|
|
this.updateLineNumbers(element);
|
2025-06-24 22:45:50 +00:00
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
handlers.onKeyDown(e);
|
|
|
|
});
|
|
|
|
|
2025-06-26 11:41:58 +00:00
|
|
|
// Paste handler - plain text only
|
|
|
|
editor.addEventListener('paste', (e) => {
|
2025-06-24 22:45:50 +00:00
|
|
|
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'));
|
2025-06-26 11:41:58 +00:00
|
|
|
this.updateLineNumbers(element);
|
2025-06-24 22:45:50 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
2025-06-26 11:41:58 +00:00
|
|
|
|
|
|
|
// Composition handlers
|
|
|
|
editor.addEventListener('compositionstart', () => handlers.onCompositionStart());
|
|
|
|
editor.addEventListener('compositionend', () => handlers.onCompositionEnd());
|
|
|
|
|
|
|
|
// Initial syntax highlighting if content exists
|
|
|
|
if (block.content && !isEditing) {
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
this.applyHighlighting(element, block);
|
|
|
|
});
|
|
|
|
}
|
2025-06-24 22:45:50 +00:00
|
|
|
}
|
|
|
|
|
2025-06-26 11:41:58 +00:00
|
|
|
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 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 || 'javascript';
|
|
|
|
|
|
|
|
// Apply highlighting
|
|
|
|
try {
|
|
|
|
const result = hlight.highlight(content, {
|
|
|
|
language: language,
|
|
|
|
ignoreIllegals: true
|
|
|
|
});
|
2025-06-24 22:45:50 +00:00
|
|
|
|
2025-06-26 11:41:58 +00:00
|
|
|
// 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);
|
|
|
|
});
|
|
|
|
}
|
2025-06-24 22:45:50 +00:00
|
|
|
}
|
2025-06-26 11:41:58 +00:00
|
|
|
} catch (error) {
|
|
|
|
// If highlighting fails, keep plain text
|
|
|
|
console.warn('Syntax highlighting failed:', error);
|
|
|
|
}
|
2025-06-24 22:45:50 +00:00
|
|
|
}
|
|
|
|
|
2025-06-26 11:41:58 +00:00
|
|
|
private escapeHtml(text: string): string {
|
|
|
|
const div = document.createElement('div');
|
|
|
|
div.textContent = text;
|
|
|
|
return div.innerHTML;
|
2025-06-24 22:45:50 +00:00
|
|
|
}
|
|
|
|
|
2025-06-26 11:41:58 +00:00
|
|
|
getContent(element: HTMLElement): string {
|
|
|
|
const editor = element.querySelector('.code-editor') as HTMLElement;
|
|
|
|
return editor?.textContent || '';
|
|
|
|
}
|
2025-06-24 22:45:50 +00:00
|
|
|
|
2025-06-26 11:41:58 +00:00
|
|
|
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') || 'javascript'
|
|
|
|
}
|
|
|
|
};
|
|
|
|
this.applyHighlighting(element, block);
|
2025-06-24 22:45:50 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-06-26 11:41:58 +00:00
|
|
|
getCursorPosition(element: HTMLElement): number | null {
|
|
|
|
const editor = element.querySelector('.code-editor') as HTMLElement;
|
|
|
|
if (!editor) return null;
|
2025-06-24 22:45:50 +00:00
|
|
|
|
2025-06-26 11:41:58 +00:00
|
|
|
const selection = window.getSelection();
|
|
|
|
if (!selection || selection.rangeCount === 0) return null;
|
2025-06-24 22:45:50 +00:00
|
|
|
|
2025-06-26 11:41:58 +00:00
|
|
|
const range = selection.getRangeAt(0);
|
|
|
|
if (!editor.contains(range.startContainer)) return null;
|
2025-06-24 22:45:50 +00:00
|
|
|
|
2025-06-26 11:41:58 +00:00
|
|
|
const preCaretRange = document.createRange();
|
|
|
|
preCaretRange.selectNodeContents(editor);
|
|
|
|
preCaretRange.setEnd(range.startContainer, range.startOffset);
|
2025-06-24 22:45:50 +00:00
|
|
|
|
2025-06-26 11:41:58 +00:00
|
|
|
return preCaretRange.toString().length;
|
2025-06-24 22:45:50 +00:00
|
|
|
}
|
|
|
|
|
2025-06-26 11:41:58 +00:00
|
|
|
setCursorToStart(element: HTMLElement): void {
|
|
|
|
const editor = element.querySelector('.code-editor') as HTMLElement;
|
|
|
|
if (editor) {
|
|
|
|
WysiwygSelection.setCursorPosition(editor, 0);
|
2025-06-24 22:45:50 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-06-26 11:41:58 +00:00
|
|
|
setCursorToEnd(element: HTMLElement): void {
|
|
|
|
const editor = element.querySelector('.code-editor') as HTMLElement;
|
|
|
|
if (editor) {
|
|
|
|
const length = editor.textContent?.length || 0;
|
|
|
|
WysiwygSelection.setCursorPosition(editor, length);
|
2025-06-24 22:45:50 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-06-26 11:41:58 +00:00
|
|
|
focus(element: HTMLElement): void {
|
|
|
|
const editor = element.querySelector('.code-editor') as HTMLElement;
|
|
|
|
editor?.focus();
|
2025-06-24 22:45:50 +00:00
|
|
|
}
|
|
|
|
|
2025-06-26 11:41:58 +00:00
|
|
|
focusWithCursor(element: HTMLElement, position: 'start' | 'end' | number = 'end'): void {
|
|
|
|
const editor = element.querySelector('.code-editor') as HTMLElement;
|
|
|
|
if (!editor) return;
|
2025-06-24 22:45:50 +00:00
|
|
|
|
2025-06-26 11:41:58 +00:00
|
|
|
editor.focus();
|
2025-06-24 22:45:50 +00:00
|
|
|
|
2025-06-26 11:41:58 +00:00
|
|
|
requestAnimationFrame(() => {
|
2025-06-24 22:45:50 +00:00
|
|
|
if (position === 'start') {
|
2025-06-26 11:41:58 +00:00
|
|
|
this.setCursorToStart(element);
|
2025-06-24 22:45:50 +00:00
|
|
|
} else if (position === 'end') {
|
2025-06-26 11:41:58 +00:00
|
|
|
this.setCursorToEnd(element);
|
2025-06-24 22:45:50 +00:00
|
|
|
} else if (typeof position === 'number') {
|
2025-06-26 11:41:58 +00:00
|
|
|
WysiwygSelection.setCursorPosition(editor, position);
|
2025-06-24 22:45:50 +00:00
|
|
|
}
|
2025-06-26 11:41:58 +00:00
|
|
|
});
|
2025-06-24 22:45:50 +00:00
|
|
|
}
|
|
|
|
|
2025-06-26 11:41:58 +00:00
|
|
|
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)
|
2025-06-24 22:45:50 +00:00
|
|
|
};
|
|
|
|
}
|
2025-06-26 11:41:58 +00:00
|
|
|
|
|
|
|
getStyles(): string {
|
|
|
|
return `
|
|
|
|
/* Code Block Container */
|
|
|
|
.code-block-container {
|
|
|
|
position: relative;
|
|
|
|
margin: 16px 0;
|
|
|
|
background: ${cssManager.bdTheme('#f6f8fa', '#0d1117')};
|
|
|
|
border: 1px solid ${cssManager.bdTheme('#d1d5da', '#30363d')};
|
|
|
|
border-radius: 8px;
|
|
|
|
overflow: hidden;
|
|
|
|
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
|
|
|
}
|
|
|
|
|
|
|
|
.code-block-container.selected {
|
|
|
|
border-color: ${cssManager.bdTheme('#0969da', '#58a6ff')};
|
|
|
|
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(9, 105, 218, 0.15)', 'rgba(88, 166, 255, 0.15)')};
|
|
|
|
}
|
|
|
|
|
|
|
|
.code-block-container.editing {
|
|
|
|
border-color: ${cssManager.bdTheme('#0969da', '#58a6ff')};
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Header */
|
|
|
|
.code-header {
|
|
|
|
background: ${cssManager.bdTheme('#f6f8fa', '#161b22')};
|
|
|
|
border-bottom: 1px solid ${cssManager.bdTheme('#d1d5da', '#30363d')};
|
|
|
|
padding: 8px 16px;
|
|
|
|
display: flex;
|
2025-06-26 11:57:04 +00:00
|
|
|
justify-content: space-between;
|
2025-06-26 11:41:58 +00:00
|
|
|
align-items: center;
|
|
|
|
}
|
|
|
|
|
|
|
|
.language-label {
|
|
|
|
font-size: 12px;
|
|
|
|
color: ${cssManager.bdTheme('#57606a', '#8b949e')};
|
|
|
|
background: ${cssManager.bdTheme('#ffffff', '#0d1117')};
|
|
|
|
padding: 2px 8px;
|
|
|
|
border-radius: 4px;
|
|
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
|
|
}
|
|
|
|
|
2025-06-26 11:57:04 +00:00
|
|
|
/* Copy Button */
|
|
|
|
.copy-button {
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
gap: 6px;
|
|
|
|
padding: 4px 12px;
|
|
|
|
background: transparent;
|
|
|
|
border: 1px solid ${cssManager.bdTheme('#d1d5da', '#30363d')};
|
|
|
|
border-radius: 6px;
|
|
|
|
color: ${cssManager.bdTheme('#57606a', '#8b949e')};
|
|
|
|
font-size: 12px;
|
|
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
|
|
cursor: pointer;
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
outline: none;
|
|
|
|
}
|
|
|
|
|
|
|
|
.copy-button:hover {
|
|
|
|
background: ${cssManager.bdTheme('#f3f4f6', '#1c2128')};
|
|
|
|
color: ${cssManager.bdTheme('#24292f', '#c9d1d9')};
|
|
|
|
border-color: ${cssManager.bdTheme('#8b949e', '#484f58')};
|
|
|
|
}
|
|
|
|
|
|
|
|
.copy-button:active {
|
|
|
|
transform: scale(0.95);
|
|
|
|
}
|
|
|
|
|
|
|
|
.copy-button.copied {
|
|
|
|
background: ${cssManager.bdTheme('#2ea043', '#238636')};
|
|
|
|
border-color: ${cssManager.bdTheme('#2ea043', '#238636')};
|
|
|
|
color: white;
|
|
|
|
}
|
|
|
|
|
|
|
|
.copy-icon {
|
|
|
|
flex-shrink: 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
.copy-text {
|
|
|
|
min-width: 45px;
|
|
|
|
text-align: center;
|
|
|
|
}
|
|
|
|
|
2025-06-26 11:41:58 +00:00
|
|
|
/* Code Body */
|
|
|
|
.code-body {
|
|
|
|
display: flex;
|
|
|
|
position: relative;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Line Numbers */
|
|
|
|
.line-numbers {
|
|
|
|
flex-shrink: 0;
|
|
|
|
padding: 16px 0;
|
|
|
|
background: ${cssManager.bdTheme('#f6f8fa', '#010409')};
|
|
|
|
border-right: 1px solid ${cssManager.bdTheme('#d1d5da', '#30363d')};
|
|
|
|
text-align: right;
|
|
|
|
user-select: none;
|
|
|
|
min-width: 50px;
|
|
|
|
}
|
|
|
|
|
|
|
|
.line-number {
|
|
|
|
padding: 0 12px;
|
|
|
|
color: ${cssManager.bdTheme('#57606a', '#484f58')};
|
|
|
|
font-family: 'SF Mono', Monaco, Consolas, monospace;
|
|
|
|
font-size: 14px;
|
|
|
|
line-height: 21px;
|
|
|
|
height: 21px;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Code Content */
|
|
|
|
.code-content {
|
|
|
|
flex: 1;
|
|
|
|
overflow-x: auto;
|
|
|
|
position: relative;
|
|
|
|
}
|
|
|
|
|
|
|
|
.code-pre {
|
|
|
|
margin: 0;
|
|
|
|
padding: 0;
|
|
|
|
background: transparent;
|
|
|
|
}
|
|
|
|
|
|
|
|
.code-editor {
|
|
|
|
display: block;
|
|
|
|
padding: 16px;
|
|
|
|
margin: 0;
|
|
|
|
font-family: 'SF Mono', Monaco, Consolas, monospace;
|
|
|
|
font-size: 14px;
|
|
|
|
line-height: 21px;
|
|
|
|
color: ${cssManager.bdTheme('#24292f', '#c9d1d9')};
|
|
|
|
background: transparent;
|
|
|
|
border: none;
|
|
|
|
outline: none;
|
|
|
|
white-space: pre-wrap;
|
|
|
|
word-wrap: break-word;
|
|
|
|
min-height: 84px;
|
|
|
|
overflow: visible;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Placeholder */
|
|
|
|
.code-editor:empty::before {
|
|
|
|
content: "// Type or paste code here...";
|
|
|
|
color: ${cssManager.bdTheme('#6a737d', '#484f58')};
|
|
|
|
pointer-events: none;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Syntax Highlighting Colors */
|
|
|
|
.code-editor .hljs-keyword {
|
|
|
|
color: ${cssManager.bdTheme('#d73a49', '#ff7b72')};
|
|
|
|
}
|
|
|
|
|
|
|
|
.code-editor .hljs-string {
|
|
|
|
color: ${cssManager.bdTheme('#032f62', '#a5d6ff')};
|
|
|
|
}
|
|
|
|
|
|
|
|
.code-editor .hljs-number {
|
|
|
|
color: ${cssManager.bdTheme('#005cc5', '#79c0ff')};
|
|
|
|
}
|
|
|
|
|
|
|
|
.code-editor .hljs-function {
|
|
|
|
color: ${cssManager.bdTheme('#6f42c1', '#d2a8ff')};
|
|
|
|
}
|
|
|
|
|
|
|
|
.code-editor .hljs-comment {
|
|
|
|
color: ${cssManager.bdTheme('#6a737d', '#8b949e')};
|
|
|
|
font-style: italic;
|
|
|
|
}
|
|
|
|
|
|
|
|
.code-editor .hljs-variable,
|
|
|
|
.code-editor .hljs-attr {
|
|
|
|
color: ${cssManager.bdTheme('#e36209', '#ffa657')};
|
|
|
|
}
|
|
|
|
|
|
|
|
.code-editor .hljs-class,
|
|
|
|
.code-editor .hljs-title {
|
|
|
|
color: ${cssManager.bdTheme('#6f42c1', '#d2a8ff')};
|
|
|
|
}
|
|
|
|
|
|
|
|
.code-editor .hljs-params {
|
|
|
|
color: ${cssManager.bdTheme('#24292f', '#c9d1d9')};
|
|
|
|
}
|
|
|
|
|
|
|
|
.code-editor .hljs-built_in {
|
|
|
|
color: ${cssManager.bdTheme('#005cc5', '#79c0ff')};
|
|
|
|
}
|
|
|
|
|
|
|
|
.code-editor .hljs-literal {
|
|
|
|
color: ${cssManager.bdTheme('#005cc5', '#79c0ff')};
|
|
|
|
}
|
|
|
|
|
|
|
|
.code-editor .hljs-meta {
|
|
|
|
color: ${cssManager.bdTheme('#735c0f', '#f2cc60')};
|
|
|
|
}
|
|
|
|
|
|
|
|
.code-editor .hljs-punctuation {
|
|
|
|
color: ${cssManager.bdTheme('#24292f', '#c9d1d9')};
|
|
|
|
}
|
|
|
|
|
|
|
|
.code-editor .hljs-tag {
|
|
|
|
color: ${cssManager.bdTheme('#22863a', '#7ee83f')};
|
|
|
|
}
|
|
|
|
|
|
|
|
.code-editor .hljs-attribute {
|
|
|
|
color: ${cssManager.bdTheme('#6f42c1', '#d2a8ff')};
|
|
|
|
}
|
|
|
|
|
|
|
|
.code-editor .hljs-selector-tag {
|
|
|
|
color: ${cssManager.bdTheme('#22863a', '#7ee83f')};
|
|
|
|
}
|
|
|
|
|
|
|
|
.code-editor .hljs-selector-class {
|
|
|
|
color: ${cssManager.bdTheme('#6f42c1', '#d2a8ff')};
|
|
|
|
}
|
|
|
|
|
|
|
|
.code-editor .hljs-selector-id {
|
|
|
|
color: ${cssManager.bdTheme('#005cc5', '#79c0ff')};
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Selection */
|
|
|
|
.code-editor::selection,
|
|
|
|
.code-editor *::selection {
|
|
|
|
background: ${cssManager.bdTheme('rgba(9, 105, 218, 0.3)', 'rgba(88, 166, 255, 0.3)')};
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Scrollbar styling */
|
|
|
|
.code-content::-webkit-scrollbar {
|
|
|
|
height: 8px;
|
|
|
|
}
|
|
|
|
|
|
|
|
.code-content::-webkit-scrollbar-track {
|
|
|
|
background: ${cssManager.bdTheme('#f6f8fa', '#010409')};
|
|
|
|
}
|
|
|
|
|
|
|
|
.code-content::-webkit-scrollbar-thumb {
|
|
|
|
background: ${cssManager.bdTheme('#d1d5da', '#30363d')};
|
|
|
|
border-radius: 4px;
|
|
|
|
}
|
|
|
|
|
|
|
|
.code-content::-webkit-scrollbar-thumb:hover {
|
|
|
|
background: ${cssManager.bdTheme('#c8c8c8', '#484f58')};
|
|
|
|
}
|
|
|
|
`;
|
|
|
|
}
|
2025-06-24 22:45:50 +00:00
|
|
|
}
|