feat: complete clean editor implementation with comprehensive UI framework

Major architectural improvements and feature enhancements:

## Core Features Added
-  Custom status modal system replacing browser alerts with theme-consistent branding
-  HTML generation dogtag with timestamp and username linking
-  All document links now open in new tabs without triggering edit mode
-  Comprehensive UI framework documentation (UserInterfaceFramework.md)

## Architecture Improvements
- 🔧 Complete cleanup of document_manager.py - removed 2000+ lines of legacy code
- 🔧 Clean wrapper implementation maintaining backward compatibility
- 🔧 Enhanced database integration with proper front matter parsing
- 🔧 Improved AST processing and cache file generation

## UI/UX Enhancements
- 🎨 Theme-aware modal dialogs with proper CSS styling and accessibility
- 🎨 Consistent CSS class naming conventions across all UI components
- 🎨 Enhanced link behavior for better document navigation
- 🎨 Professional status information display

## Developer Experience
- 📝 Comprehensive UI component documentation for future development
- 🧪 Updated test suite to work with clean implementation
- 🧪 Fixed multiple test compatibility issues
- 🧪 Enhanced error handling and validation

## Technical Details
- Added store_document method to CleanDocumentManager
- Enhanced ingest_file method with proper title extraction
- Implemented theme-consistent modal overlay patterns
- Added --nodogtag CLI option for clean output when needed
- Fixed CSS escape sequences and JavaScript syntax issues

This release establishes a solid foundation for the clean editor architecture
while maintaining full backward compatibility with existing functionality.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-28 03:50:21 +01:00
parent 3e16793615
commit 86689c451c
9 changed files with 879 additions and 2300 deletions

View File

@@ -16,8 +16,47 @@ class CleanDocumentManager:
def __init__(self, db_manager=None):
self.db_manager = db_manager
def store_document(self, file_path: str, content: str, ast: list = None, front_matter: dict = None):
"""Store a document in the database."""
if self.db_manager:
from pathlib import Path
filename = Path(file_path).name
return self.db_manager.store_markdown_file(filename, content)
def get_file(self, file_path: str) -> Dict[str, Any]:
"""
Retrieve a markdown file from the database.
Args:
file_path: Path to the markdown file to retrieve
Returns:
Dictionary containing file content and metadata
Raises:
FileNotFoundError: If file is not found in database
"""
if not self.db_manager:
raise ValueError("Database manager not initialized")
# Get file from database
file_data = self.db_manager.get_markdown_file(file_path)
if file_data is None:
raise FileNotFoundError(f"File '{file_path}' not found in database")
return {
'content': file_data.get('content', ''),
'metadata': {
'filename': file_data.get('filename', file_path),
'front_matter': file_data.get('front_matter'),
'size': len(file_data.get('content', '')),
'modified': file_data.get('modified')
}
}
def render_file(self, input_file: str, output_file: str, template: str = None, css: str = None,
edit_mode: bool = False, editor_theme: str = 'github', keyboard_shortcuts: bool = True) -> Dict[str, Any]:
edit_mode: bool = False, editor_theme: str = 'github', keyboard_shortcuts: bool = True, nodogtag: bool = False) -> Dict[str, Any]:
"""
Render a markdown file to HTML with optional clean editing capabilities.
"""
@@ -49,7 +88,8 @@ class CleanDocumentManager:
editor_theme=editor_theme,
keyboard_shortcuts=keyboard_shortcuts,
original_filename=original_filename,
version_info=version_info
version_info=version_info,
nodogtag=nodogtag
)
# Write HTML file
@@ -73,51 +113,17 @@ class CleanDocumentManager:
def _get_version_info(self) -> dict:
"""Get repository name and version information."""
version_info = {
from .__version__ import get_version_info
version_info = get_version_info()
# Transform to the format expected by the editor
return {
'repo_name': 'Markitect',
'version': '0.3.0',
'git_info': ''
'version': version_info['full_version'],
'git_info': '' # Already included in full_version
}
try:
# Try to get version from package metadata
from importlib.metadata import version as get_version
version_info['version'] = get_version('markitect')
except Exception:
pass
try:
# Try to get git information
import subprocess
from pathlib import Path
# Get git commit hash and status
try:
git_hash = subprocess.check_output(
['git', 'rev-parse', '--short', 'HEAD'],
cwd=Path(__file__).parent,
stderr=subprocess.DEVNULL
).decode().strip()
# Check if there are uncommitted changes
try:
subprocess.check_output(
['git', 'diff-index', '--quiet', 'HEAD', '--'],
cwd=Path(__file__).parent,
stderr=subprocess.DEVNULL
)
git_status = ''
except subprocess.CalledProcessError:
git_status = '-modified'
version_info['git_info'] = f" (git:{git_hash}{git_status})"
except (subprocess.CalledProcessError, FileNotFoundError):
pass
except Exception:
pass
return version_info
def _get_template_css(self, template: str = None) -> str:
"""Generate layered theme CSS styles."""
# Import layered theme functions
@@ -310,10 +316,29 @@ class CleanDocumentManager:
.markitect-edit-mode .ui-edit-floater-header {{
color: {props.get('editor_text_color', '#212529')};
}}
.markitect-edit-mode .ui-edit-floater-header h3 {{
color: {props.get('editor_text_color', '#212529')};
}}
.markitect-edit-mode .ui-edit-inline-panel {{
background: {props['editor_panel_bg']};
border: 1px solid {props.get('editor_panel_border', '#dee2e6')};
box-shadow: 0 2px 8px {props.get('editor_shadow', 'rgba(0,0,0,0.1)')};
color: {props.get('editor_text_color', '#212529')};
border-radius: 8px;
padding: 12px;
margin: 8px 0;
}}
.markitect-edit-mode .ui-edit-button {{
background: {props.get('editor_button_bg', '#ffffff')};
color: {props.get('editor_text_color', '#212529')};
border: 1px solid {props.get('editor_panel_border', '#dee2e6')};
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
min-width: 70px;
font-weight: 500;
transition: all 0.2s;
}}
.markitect-edit-mode .ui-edit-button:hover {{
background: {props.get('editor_button_hover', '#e9ecef')};
@@ -323,17 +348,36 @@ class CleanDocumentManager:
background: {props.get('editor_button_active', '#dee2e6')};
}}
.markitect-edit-mode .ui-edit-button-accept {{
background: {props.get('editor_button_bg', '#4caf50')};
background: {props.get('editor_accept_bg', '#4caf50')};
color: white;
}}
.markitect-edit-mode .ui-edit-button-accept:hover {{
background: {props.get('editor_accept_hover', '#388e3c')};
}}
.markitect-edit-mode .ui-edit-button-cancel {{
background: {props.get('editor_button_bg', '#f44336')};
background: {props.get('editor_cancel_bg', '#f44336')};
color: white;
}}
.markitect-edit-mode .ui-edit-button-cancel:hover {{
background: {props.get('editor_cancel_hover', '#d32f2f')};
}}
.markitect-edit-mode .ui-edit-button-reset {{
background: {props.get('editor_button_bg', '#ff9800')};
background: {props.get('editor_reset_bg', '#ff9800')};
color: white;
}}
.markitect-edit-mode .ui-edit-button-reset:hover {{
background: {props.get('editor_reset_hover', '#f57c00')};
}}
.markitect-edit-mode .ui-edit-button-secondary {{
background: {props.get('editor_secondary_bg', '#6c757d')};
color: white;
}}
.markitect-edit-mode .ui-edit-button-secondary:hover {{
background: {props.get('editor_secondary_hover', '#545b62')};
}}
.markitect-edit-mode .ui-edit-section-frame {{
border: 2px solid {props.get('editor_focus_color', '#0066cc')};
box-shadow: 0 0 0 3px {props.get('editor_focus_color', '#0066cc')}33;
border: 2px solid {props.get('editor_focus_color', props.get('editor_panel_border', '#dee2e6'))};
box-shadow: 0 0 0 3px {props.get('editor_focus_color', props.get('editor_panel_border', '#dee2e6'))}33;
}}
.markitect-edit-mode .ui-edit-textarea {{
border: 1px solid {props.get('editor_panel_border', '#dee2e6')};
@@ -341,8 +385,103 @@ class CleanDocumentManager:
background: {props.get('editor_button_bg', '#ffffff')};
}}
.markitect-edit-mode .ui-edit-textarea:focus {{
border-color: {props.get('editor_focus_color', '#0066cc')};
box-shadow: 0 0 0 2px {props.get('editor_focus_color', '#0066cc')}33;
border-color: {props.get('editor_focus_color', props.get('editor_panel_border', '#dee2e6'))};
box-shadow: 0 0 0 2px {props.get('editor_focus_color', props.get('editor_panel_border', '#dee2e6'))}33;
}}
.markitect-edit-mode .ui-edit-modal-overlay {{
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s, visibility 0.3s;
}}
.markitect-edit-mode .ui-edit-modal-overlay.active {{
opacity: 1;
visibility: visible;
}}
.markitect-edit-mode .ui-edit-modal {{
background: {props['editor_panel_bg']};
border: 1px solid {props.get('editor_panel_border', '#dee2e6')};
box-shadow: 0 8px 32px {props.get('editor_shadow', 'rgba(0,0,0,0.2)')};
color: {props.get('editor_text_color', '#212529')};
border-radius: 8px;
max-width: 600px;
max-height: 80vh;
width: 90%;
overflow: hidden;
transform: scale(0.9) translateY(-20px);
transition: transform 0.3s;
}}
.markitect-edit-mode .ui-edit-modal-overlay.active .ui-edit-modal {{
transform: scale(1) translateY(0);
}}
.markitect-edit-mode .ui-edit-modal-header {{
padding: 20px 24px 16px;
border-bottom: 1px solid {props.get('editor_panel_border', '#dee2e6')};
display: flex;
justify-content: space-between;
align-items: center;
}}
.markitect-edit-mode .ui-edit-modal-title {{
margin: 0;
font-size: 18px;
font-weight: 600;
color: {props.get('editor_text_color', '#212529')};
}}
.markitect-edit-mode .ui-edit-modal-close {{
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: {props.get('editor_text_color', '#212529')};
padding: 4px;
border-radius: 4px;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s;
}}
.markitect-edit-mode .ui-edit-modal-close:hover {{
background: {props.get('editor_button_hover', '#e9ecef')};
}}
.markitect-edit-mode .ui-edit-modal-body {{
padding: 20px 24px;
overflow-y: auto;
max-height: 60vh;
}}
.markitect-edit-mode .ui-edit-modal-content {{
white-space: pre-line;
line-height: 1.5;
font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 14px;
}}
.markitect-edit-mode .ui-edit-modal-section {{
margin-bottom: 16px;
}}
.markitect-edit-mode .ui-edit-modal-section:last-child {{
margin-bottom: 0;
}}
.markitect-edit-mode .ui-edit-modal-section-title {{
font-weight: 600;
margin-bottom: 8px;
color: {props.get('editor_text_color', '#212529')};
}}
.markitect-edit-mode .ui-edit-modal-footer {{
padding: 16px 24px 20px;
border-top: 1px solid {props.get('editor_panel_border', '#dee2e6')};
text-align: right;
}}
outline: none;
}}"""
return f"<style>{base_css}{heading_css}{text_css}{element_css}{link_css}{accent_css}{ui_css}</style>"
@@ -382,11 +521,34 @@ class CleanDocumentManager:
return self._generate_layered_css(layered_props)
def _generate_html_template(self, markdown_content: str, title: str, css: str = None, template: str = None,
edit_mode: bool = False, editor_theme: str = 'github', keyboard_shortcuts: bool = True, original_filename: str = 'document', version_info: dict = None) -> str:
edit_mode: bool = False, editor_theme: str = 'github', keyboard_shortcuts: bool = True, original_filename: str = 'document', version_info: dict = None, nodogtag: bool = False) -> str:
"""Generate clean HTML template."""
# Add dogtag to markdown content if not disabled
if not nodogtag:
import datetime
import getpass
now = datetime.datetime.now()
datetime_str = now.strftime("%Y-%m-%d %H:%M:%S")
try:
username = getpass.getuser()
except:
username = "user"
# Create username link only for 'worsch', otherwise just show username
if username == 'worsch':
username_link = f'<a href="https://coulomb.social/open/worsch" target="_blank">{username}</a>'
else:
username_link = username
dogtag = f'\n\n---\n*-- html from markdown by <a href="https://coulomb.social/open/MarkiTect" target="_blank">MarkiTect</a> on {datetime_str} by {username_link}*'
markdown_content_with_dogtag = markdown_content + dogtag
else:
markdown_content_with_dogtag = markdown_content
# Escape the markdown content for JavaScript
js_markdown_content = json.dumps(markdown_content)
js_markdown_content = json.dumps(markdown_content_with_dogtag)
# Handle CSS styles
css_content = ""
@@ -413,7 +575,7 @@ class CleanDocumentManager:
body_classes = ' class="markitect-edit-mode"'
# Configuration for clean editor
version_str = f"{version_info['repo_name']} v{version_info['version']}{version_info['git_info']}" if version_info else "Markitect v0.3.0"
version_str = f"{version_info['repo_name']} v{version_info['version']}{version_info['git_info']}" if version_info else "Markitect v0.5.0.dev"
editor_config = f"""
const MARKITECT_EDIT_MODE = true;
const MARKITECT_EDITOR_CONFIG = {{
@@ -466,7 +628,10 @@ class CleanDocumentManager:
if (contentDiv) {{
if (typeof marked !== 'undefined') {{
try {{
contentDiv.innerHTML = marked.parse(markdownContent);
const html = marked.parse(markdownContent);
// Add target="_blank" to all links
const htmlWithTargetBlank = html.replace(/<a href="([^"]*)"([^>]*)>/g, '<a href="$1" target="_blank"$2>');
contentDiv.innerHTML = htmlWithTargetBlank;
console.log("✓ Content rendered successfully");
console.log('✓ Markdown rendered successfully');
}} catch (error) {{
@@ -1017,7 +1182,10 @@ class DOMRenderer {
element.setAttribute('data-section-id', section.id);
if (typeof marked !== 'undefined') {
element.innerHTML = marked.parse(section.currentMarkdown);
const html = marked.parse(section.currentMarkdown);
// Add target="_blank" to all links
const htmlWithTargetBlank = html.replace(/<a href="([^"]*)"([^>]*)>/g, '<a href="$1" target="_blank"$2>');
element.innerHTML = htmlWithTargetBlank;
} else {
element.innerHTML = `<p>${section.currentMarkdown}</p>`;
}
@@ -1029,8 +1197,8 @@ class DOMRenderer {
}
handleSectionClick(event) {
// Don't handle clicks on form elements or buttons
if (event.target.closest('textarea, button, input')) {
// Don't handle clicks on form elements, buttons, or links
if (event.target.closest('textarea, button, input, a')) {
return;
}
@@ -1062,6 +1230,7 @@ class DOMRenderer {
this.hideCurrentEditor();
const editorContainer = document.createElement('div');
editorContainer.className = 'ui-edit-inline-panel';
editorContainer.style.cssText = `
display: flex;
gap: 12px;
@@ -1076,7 +1245,6 @@ class DOMRenderer {
flex: 1;
min-height: 100px;
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
border: 2px solid #007acc;
border-radius: 6px;
padding: 12px;
font-size: 14px;
@@ -1096,36 +1264,17 @@ class DOMRenderer {
gap: 6px;
`;
const createButton = (text, color, handler) => {
const createButton = (text, className, handler) => {
const btn = document.createElement('button');
btn.textContent = text;
// Add CSS classes based on button type
btn.className = 'ui-edit-button';
if (text.includes('Accept')) {
btn.className += ' ui-edit-button-accept';
} else if (text.includes('Cancel')) {
btn.className += ' ui-edit-button-cancel';
} else if (text.includes('Reset')) {
btn.className += ' ui-edit-button-reset';
}
btn.style.cssText = `
padding: 8px 12px;
border: none;
border-radius: 4px;
color: white;
background: ${color};
cursor: pointer;
font-size: 12px;
min-width: 70px;
`;
btn.className = className;
btn.addEventListener('click', handler);
return btn;
};
controls.appendChild(createButton('✓ Accept', '#4caf50', () => this.handleAccept(sectionId)));
controls.appendChild(createButton('✗ Cancel', '#f44336', () => this.handleCancel(sectionId)));
controls.appendChild(createButton('🔄 Reset', '#ff9800', () => this.handleReset(sectionId)));
controls.appendChild(createButton('✓ Accept', 'ui-edit-button ui-edit-button-accept', () => this.handleAccept(sectionId)));
controls.appendChild(createButton('✗ Cancel', 'ui-edit-button ui-edit-button-cancel', () => this.handleCancel(sectionId)));
controls.appendChild(createButton('🔄 Reset', 'ui-edit-button ui-edit-button-reset', () => this.handleReset(sectionId)));
editorContainer.appendChild(textarea);
editorContainer.appendChild(controls);
@@ -1159,7 +1308,10 @@ class DOMRenderer {
if (!element) return;
if (typeof marked !== 'undefined') {
element.innerHTML = marked.parse(content);
const html = marked.parse(content);
// Add target="_blank" to all links
const htmlWithTargetBlank = html.replace(/<a href="([^"]*)"([^>]*)>/g, '<a href="$1" target="_blank"$2>');
element.innerHTML = htmlWithTargetBlank;
} else {
element.innerHTML = `<p>${content}</p>`;
}
@@ -1169,7 +1321,7 @@ class DOMRenderer {
}
setupSectionElement(element) {
element.className = 'ui-edit-section ui-edit-section-frame';
element.className = 'ui-edit-section';
element.style.cssText = `
margin: 16px 0;
padding: 12px;
@@ -1185,8 +1337,8 @@ class DOMRenderer {
// Create new handlers and store references
element._mouseenterHandler = () => {
element.style.backgroundColor = 'rgba(33, 150, 243, 0.05)';
element.style.borderColor = 'rgba(33, 150, 243, 0.2)';
element.style.backgroundColor = 'rgba(0, 0, 0, 0.02)';
element.style.borderColor = 'rgba(0, 0, 0, 0.1)';
};
element._mouseleaveHandler = () => {
@@ -1370,62 +1522,36 @@ class MarkitectCleanEditor {
position: fixed;
top: 20px;
right: 20px;
background: white;
border: 2px solid #007acc;
border-radius: 8px;
padding: 16px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 1000;
font-family: system-ui, -apple-system, sans-serif;
min-width: 200px;
max-width: 250px;
`;
// Add internal styling
// Add internal styling for structural layout (theme colors come from CSS)
const style = document.createElement('style');
style.textContent = `
#markitect-global-controls .control-header h3 {
.ui-edit-floater-header h3 {
margin: 0 0 8px 0;
font-size: 16px;
color: #007acc;
}
#markitect-global-controls .control-status {
.ui-edit-floater-status {
font-size: 12px;
color: #666;
margin-bottom: 12px;
}
#markitect-global-controls .control-btn {
.ui-edit-button {
display: block;
width: 100%;
margin: 6px 0;
padding: 10px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s;
}
#markitect-global-controls .control-btn.primary {
background: #007acc;
color: white;
}
#markitect-global-controls .control-btn.primary:hover {
background: #005a9f;
}
#markitect-global-controls .control-btn.warning {
background: #ff9800;
color: white;
}
#markitect-global-controls .control-btn.warning:hover {
background: #f57c00;
}
#markitect-global-controls .control-btn.secondary {
background: #6c757d;
color: white;
}
#markitect-global-controls .control-btn.secondary:hover {
background: #545b62;
border: 1px solid transparent;
}
`;
document.head.appendChild(style);
@@ -1451,13 +1577,13 @@ class MarkitectCleanEditor {
if (editing > 0) {
statusEl.textContent = `Editing ${editing} section${editing !== 1 ? 's' : ''}`;
statusEl.style.color = '#007acc';
statusEl.className = 'ui-edit-floater-status editing';
} else if (modified > 0) {
statusEl.textContent = `${modified} section${modified !== 1 ? 's' : ''} modified`;
statusEl.style.color = '#ff9800';
statusEl.style.color = '';
} else {
statusEl.textContent = 'All sections saved';
statusEl.style.color = '#28a745';
statusEl.textContent = 'All sections saved';
statusEl.style.color = '';
}
}
@@ -1572,32 +1698,158 @@ class MarkitectCleanEditor {
// Get the actual save filename that will be used
const saveFilename = this.generateSaveFilename();
const message = `${window.editorConfig.repoName} ${window.editorConfig.version}
Save file: ${saveFilename}
// Create structured content for the modal
const modalContent = {
title: `📊 ${window.editorConfig.repoName} Status`,
sections: [
{
title: 'Application Information',
content: `${window.editorConfig.version}`
},
{
title: 'File Information',
content: `Save file: ${saveFilename}
Source: ${window.editorConfig.originalFilename}
${window.location.protocol}//${window.location.host}${window.location.pathname}
Document Status:
• Total sections: ${total}
URL: ${window.location.protocol}//${window.location.host}${window.location.pathname}`
},
{
title: 'Document Status',
content: `• Total sections: ${total}
• Modified sections: ${modified}
• Currently editing: ${editing}
• Unsaved changes: ${modified > 0 || editing > 0 ? 'Yes' : 'No'}
SECTION BEHAVIOR:
• Each section is a logical unit (heading + content until next heading)
• Unsaved changes: ${modified > 0 || editing > 0 ? 'Yes' : 'No'}`
},
{
title: 'Section Behavior',
content: `• Each section is a logical unit (heading + content until next heading)
• Content with line breaks stays in one section
• To split content: Create new headings (# ## ###)
• Sections don't auto-split on line breaks
EDITING CONTROLS:
• Click any section to edit its content
• Sections don't auto-split on line breaks`
},
{
title: 'Editing Controls',
content: `• Click any section to edit its content
• Accept (✓) - Save changes to that section
• Cancel (✗) - Discard changes, return to previous state
• Reset (🔄) - Restore original content for that section
• Save Document - Download all current content
• Reset All - Restore entire document to original state`;
• Reset All - Restore entire document to original state`
}
]
};
alert(message);
this.showModal(modalContent);
}
showModal(content) {
// Remove any existing modal
const existingModal = document.querySelector('.ui-edit-modal-overlay');
if (existingModal) {
existingModal.remove();
}
// Create modal overlay
const overlay = document.createElement('div');
overlay.className = 'ui-edit-modal-overlay';
// Create modal content
const modal = document.createElement('div');
modal.className = 'ui-edit-modal';
// Create header
const header = document.createElement('div');
header.className = 'ui-edit-modal-header';
const title = document.createElement('h3');
title.className = 'ui-edit-modal-title';
title.textContent = content.title;
const closeBtn = document.createElement('button');
closeBtn.className = 'ui-edit-modal-close';
closeBtn.innerHTML = '×';
closeBtn.setAttribute('aria-label', 'Close');
header.appendChild(title);
header.appendChild(closeBtn);
// Create body
const body = document.createElement('div');
body.className = 'ui-edit-modal-body';
// Add sections
content.sections.forEach(section => {
const sectionDiv = document.createElement('div');
sectionDiv.className = 'ui-edit-modal-section';
const sectionTitle = document.createElement('div');
sectionTitle.className = 'ui-edit-modal-section-title';
sectionTitle.textContent = section.title;
const sectionContent = document.createElement('div');
sectionContent.className = 'ui-edit-modal-content';
sectionContent.textContent = section.content;
sectionDiv.appendChild(sectionTitle);
sectionDiv.appendChild(sectionContent);
body.appendChild(sectionDiv);
});
// Create footer with close button
const footer = document.createElement('div');
footer.className = 'ui-edit-modal-footer';
const footerCloseBtn = document.createElement('button');
footerCloseBtn.className = 'ui-edit-button ui-edit-button-accept';
footerCloseBtn.textContent = 'Close';
footer.appendChild(footerCloseBtn);
// Assemble modal
modal.appendChild(header);
modal.appendChild(body);
modal.appendChild(footer);
overlay.appendChild(modal);
// Add to page
document.body.appendChild(overlay);
// Close handlers
const closeModal = () => {
overlay.classList.remove('active');
setTimeout(() => {
if (overlay.parentNode) {
overlay.parentNode.removeChild(overlay);
}
}, 300);
};
closeBtn.addEventListener('click', closeModal);
footerCloseBtn.addEventListener('click', closeModal);
// Close on overlay click (but not modal content)
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
closeModal();
}
});
// Close on Escape key
const handleKeydown = (e) => {
if (e.key === 'Escape') {
closeModal();
document.removeEventListener('keydown', handleKeydown);
}
};
document.addEventListener('keydown', handleKeydown);
// Show modal with animation
requestAnimationFrame(() => {
overlay.classList.add('active');
});
// Focus management
setTimeout(() => {
closeBtn.focus();
}, 100);
}
showMessage(message, type = 'info') {
@@ -1666,4 +1918,4 @@ if (typeof module !== 'undefined' && module.exports) {
} else {
window.MarkitectEditor = { Section, SectionManager, DOMRenderer, MarkitectCleanEditor };
}
"""
"""