feat: restore modern Abstract Control class system with compass positioning
- Replace old DocumentNavigator with sophisticated 507-line Control architecture - Implement compass-based positioning system (N, NNE, NE, ENE, E, ESE, SE, SSE, S, SSW, SW, WSW, W, WNW, NW, NNW) - Add four specialized controls with precise positioning: * ContentsControl (upper left - nw): Table of contents navigation * StatusControl (right - e): Document statistics and change tracking * DebugControl (lower right - se): Debug messages and system info * EditControl (upper right - ne): Document editing tools - Integrate external JavaScript files following GUARDRAILS.md principles - Add drag & drop, resize handles, expand/collapse, and hover behaviors - Implement Fail Fast error handling with safe operation wrappers - Preserve backup HTML files for reference and recovery validation - Generate 144KB functional HTML vs previous 12KB broken output 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
4130
history/GUARDRAILS-edit-mode-dbde13e-2025-11-11-00-29-34.html
Normal file
4130
history/GUARDRAILS-edit-mode-dbde13e-2025-11-11-00-29-34.html
Normal file
File diff suppressed because it is too large
Load Diff
3832
history/GUARDRAILS-fully-recovered-2025-11-12-01-03-25.html
Normal file
3832
history/GUARDRAILS-fully-recovered-2025-11-12-01-03-25.html
Normal file
File diff suppressed because it is too large
Load Diff
743
history/GUARDRAILS-static-dbde13e-2025-11-11-00-29-34.html
Normal file
743
history/GUARDRAILS-static-dbde13e-2025-11-11-00-29-34.html
Normal file
@@ -0,0 +1,743 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Development Guardrails</title>
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
line-height: 1.6;
|
||||
color: #333333;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
#markdown-content {
|
||||
min-height: 200px;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: #333333;
|
||||
|
||||
}
|
||||
pre {
|
||||
background-color: #f6f8fa;
|
||||
color: #333333;
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
border: 1px solid #d0d7de;
|
||||
}
|
||||
code {
|
||||
background-color: #f6f8fa;
|
||||
color: #333333;
|
||||
padding: 0.2em 0.4em;
|
||||
border-radius: 3px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
blockquote {
|
||||
border-left: 4px solid #dfe2e5;
|
||||
margin: 0;
|
||||
padding-left: 1rem;
|
||||
color: #6a737d;
|
||||
}
|
||||
table {
|
||||
font-size: 0.85em;
|
||||
border-collapse: collapse;
|
||||
margin: 1rem 0;
|
||||
width: 100%;
|
||||
border: 1px solid #d0d7de;
|
||||
}
|
||||
th, td {
|
||||
font-size: inherit;
|
||||
border: 1px solid #d0d7de;
|
||||
padding: 0.5rem;
|
||||
text-align: left;
|
||||
}
|
||||
th {
|
||||
background-color: #f6f8fa;
|
||||
font-weight: 600;
|
||||
}
|
||||
img {
|
||||
max-width: 12cm;
|
||||
max-height: 20cm;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: 1rem auto;
|
||||
}</style>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"
|
||||
onload="window.markitectMarkedLoaded = true"
|
||||
onerror="window.markitectMarkedError = true"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="markdown-content"></div>
|
||||
|
||||
<script>
|
||||
const markdownContent = "# Development Guardrails\n\n## JavaScript Code Principles\n\n### 1. No Inline JavaScript in Python\n**NEVER write JavaScript code directly from Python code**\n\n\u274c **Wrong:**\n```python\nscript = f\"\"\"\nfunction myFunction() {{\n console.log(\"Hello {name}\");\n}}\n\"\"\"\n```\n\n\u2705 **Correct:**\n```python\n# Load from external files only\ncomponents = [\n 'js/core/section-manager.js',\n 'js/components/debug-panel.js',\n 'js/components/document-controls.js'\n]\n```\n\n### 2. Why This Rule Exists\n- **Quoting Problems**: String escaping in Python corrupts JavaScript\n- **Syntax Errors**: Template literals and complex JS break when embedded\n- **Maintainability**: JS code should be in .js files for proper tooling\n- **Architecture**: Follows the established modular component system\n\n### 3. Proper Approach\n1. Create separate `.js` files in `markitect/static/js/components/`\n2. Load them via `_get_clean_editor_scripts()`\n3. Wire up components in the initialization script only\n\n## Testing and Validation\n\n### 1. Always Validate Generated HTML\n- Check that HTML files actually render content\n- Validate JavaScript syntax before deployment\n- Test both viewing and editing modes\n\n### 2. Detect JavaScript Errors Programmatically\n- Run syntax validation on generated JS\n- Check for common error patterns\n- Fail fast when JS is malformed\n\n### 3. Manual Testing Backup\n- If automated checks pass but functionality fails\n- Open generated HTML in browser\n- Check console for runtime errors\n- Report specific error messages\n\n## Architecture Principles\n\n### 1. Separation of Concerns\n- Python: File generation, template management\n- JavaScript: UI components, interaction logic\n- HTML: Structure and content only\n\n### 2. Modular Component System\n- Each UI component in separate file\n- Lazy loading where appropriate\n- Clear dependency management\n\n### 3. Error Handling\n- Graceful degradation when components fail\n- Clear error messages for debugging\n- Fallback modes when possible\n\n## Breaking These Rules\n\nIf you find yourself writing JavaScript in Python strings:\n1. **STOP** - Step back and reconsider\n2. Create a proper component file instead\n3. Use the existing component loading system\n4. Add validation to catch the issue early\n\nThese guardrails exist because we've seen the problems when they're violated.";
|
||||
const markdownContentWithDogtag = "# Development Guardrails\n\n## JavaScript Code Principles\n\n### 1. No Inline JavaScript in Python\n**NEVER write JavaScript code directly from Python code**\n\n\u274c **Wrong:**\n```python\nscript = f\"\"\"\nfunction myFunction() {{\n console.log(\"Hello {name}\");\n}}\n\"\"\"\n```\n\n\u2705 **Correct:**\n```python\n# Load from external files only\ncomponents = [\n 'js/core/section-manager.js',\n 'js/components/debug-panel.js',\n 'js/components/document-controls.js'\n]\n```\n\n### 2. Why This Rule Exists\n- **Quoting Problems**: String escaping in Python corrupts JavaScript\n- **Syntax Errors**: Template literals and complex JS break when embedded\n- **Maintainability**: JS code should be in .js files for proper tooling\n- **Architecture**: Follows the established modular component system\n\n### 3. Proper Approach\n1. Create separate `.js` files in `markitect/static/js/components/`\n2. Load them via `_get_clean_editor_scripts()`\n3. Wire up components in the initialization script only\n\n## Testing and Validation\n\n### 1. Always Validate Generated HTML\n- Check that HTML files actually render content\n- Validate JavaScript syntax before deployment\n- Test both viewing and editing modes\n\n### 2. Detect JavaScript Errors Programmatically\n- Run syntax validation on generated JS\n- Check for common error patterns\n- Fail fast when JS is malformed\n\n### 3. Manual Testing Backup\n- If automated checks pass but functionality fails\n- Open generated HTML in browser\n- Check console for runtime errors\n- Report specific error messages\n\n## Architecture Principles\n\n### 1. Separation of Concerns\n- Python: File generation, template management\n- JavaScript: UI components, interaction logic\n- HTML: Structure and content only\n\n### 2. Modular Component System\n- Each UI component in separate file\n- Lazy loading where appropriate\n- Clear dependency management\n\n### 3. Error Handling\n- Graceful degradation when components fail\n- Clear error messages for debugging\n- Fallback modes when possible\n\n## Breaking These Rules\n\nIf you find yourself writing JavaScript in Python strings:\n1. **STOP** - Step back and reconsider\n2. Create a proper component file instead\n3. Use the existing component loading system\n4. Add validation to catch the issue early\n\nThese guardrails exist because we've seen the problems when they're violated.\n\n---\n*-- html from markdown by <a href=\"https://coulomb.social/open/MarkiTect\" target=\"_blank\">MarkiTect</a> on 2025-11-12 00:38:01 by <a href=\"https://coulomb.social/open/worsch\" target=\"_blank\">worsch</a>*";
|
||||
const dogtagContent = "\n\n---\n*-- html from markdown by <a href=\"https://coulomb.social/open/MarkiTect\" target=\"_blank\">MarkiTect</a> on 2025-11-12 00:38:01 by <a href=\"https://coulomb.social/open/worsch\" target=\"_blank\">worsch</a>*";
|
||||
window.markitectBase64References = {};
|
||||
|
||||
|
||||
|
||||
|
||||
// Always render content first (graceful degradation)
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log("Rendering content...");
|
||||
|
||||
// Check if modular components are being used
|
||||
if (typeof SectionManager !== 'undefined') {
|
||||
console.log("✓ Modular components detected - skipping direct content rendering");
|
||||
console.log("✓ Content will be rendered by modular architecture");
|
||||
return;
|
||||
}
|
||||
|
||||
const contentDiv = document.getElementById('markdown-content');
|
||||
|
||||
// Step 1: Ensure content is always displayed (fallback for non-modular mode)
|
||||
if (contentDiv) {
|
||||
if (typeof marked !== 'undefined') {
|
||||
try {
|
||||
const html = marked.parse(markdownContentWithDogtag);
|
||||
// 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) {
|
||||
contentDiv.innerHTML = '<p>Error rendering markdown: ' + error.message + '</p>';
|
||||
console.error("Content rendered with errors");
|
||||
console.error("Markdown parsing failed:", error.message);
|
||||
}
|
||||
} else {
|
||||
// Fallback: display raw markdown with basic formatting
|
||||
const fallbackHtml = markdownContent
|
||||
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
|
||||
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
|
||||
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
|
||||
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
||||
.replace(/^- (.*$)/gim, '<li>$1</li>')
|
||||
.replace(/\n\n/g, '<br><br>')
|
||||
.replace(/\n/g, '<br>');
|
||||
contentDiv.innerHTML = '<div style="white-space: pre-wrap;">' + fallbackHtml + '</div>';
|
||||
console.warn("Content rendered with fallback parser");
|
||||
console.warn("CDN library failed to load - using basic fallback rendering");
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Initialize edit/insert capabilities if enabled
|
||||
if ((typeof MARKITECT_EDIT_MODE !== 'undefined' && MARKITECT_EDIT_MODE) ||
|
||||
(typeof MARKITECT_INSERT_MODE !== 'undefined' && MARKITECT_INSERT_MODE)) {
|
||||
const mode = (typeof MARKITECT_INSERT_MODE !== 'undefined' && MARKITECT_INSERT_MODE) ? 'insert' : 'edit';
|
||||
console.log(`Initializing clean ${mode} capabilities...`);
|
||||
try {
|
||||
console.log("Creating clean editor instance...");
|
||||
initializeCleanEditor();
|
||||
if (mode === 'insert') {
|
||||
console.log("✓ Clean insert mode active - click any section to edit (headings 1-3 protected)");
|
||||
} else {
|
||||
console.log("✓ Clean edit mode active - click any section to edit");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Clean ${mode} mode failed to initialize:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Initialize document scroll indicators (always available)
|
||||
try {
|
||||
initializeScrollIndicators();
|
||||
} catch (error) {
|
||||
console.error("Scroll indicators failed to initialize:", error);
|
||||
}
|
||||
|
||||
// Step 4: Define abstract Control class for UI controls
|
||||
const Control = {
|
||||
// Abstract control properties
|
||||
element: null,
|
||||
isExpanded: false,
|
||||
isHeaderOnly: false, // New state for header-only mode
|
||||
isDragging: false,
|
||||
isResizing: false, // New state for resizing mode
|
||||
dragOffset: { x: 0, y: 0 },
|
||||
resizeStartSize: { width: 280, height: 'auto' },
|
||||
originalPosition: { top: '80px', left: '20px' },
|
||||
defaultSize: { width: 280, minWidth: 200, minHeight: 150 },
|
||||
|
||||
// Configuration properties (to be overridden by subclasses)
|
||||
config: {
|
||||
icon: '?',
|
||||
title: 'Control',
|
||||
className: 'control',
|
||||
defaultContent: 'Template only',
|
||||
ariaLabel: 'Control',
|
||||
position: 'w' // Default compass position: west (middle-left)
|
||||
},
|
||||
|
||||
// Compass positioning system (top-aligned for proper expansion)
|
||||
compassPositions: {
|
||||
// North positions (top)
|
||||
'n': { top: '20px', left: '50%', transform: 'translateX(-50%)' },
|
||||
'nne': { top: '20px', left: '65%', transform: 'translateX(-50%)' },
|
||||
'ne': { top: '20px', right: '20px' },
|
||||
'ene': { top: '80px', right: '20px' }, // Top-aligned instead of center
|
||||
|
||||
// East positions (right)
|
||||
'e': { top: '50vh', right: '20px', transform: 'translateY(-20px)' }, // Anchor at icon level
|
||||
'ese': { top: 'calc(65vh - 20px)', right: '20px' }, // Top-aligned
|
||||
'se': { bottom: '20px', right: '20px' },
|
||||
'sse': { bottom: '20px', right: '35%', transform: 'translateX(50%)' },
|
||||
|
||||
// South positions (bottom)
|
||||
's': { bottom: '20px', left: '50%', transform: 'translateX(-50%)' },
|
||||
'ssw': { bottom: '20px', left: '35%', transform: 'translateX(-50%)' },
|
||||
'sw': { bottom: '20px', left: '20px' },
|
||||
'wsw': { bottom: '80px', left: '20px' }, // Top-aligned instead of center
|
||||
|
||||
// West positions (left) - top-aligned for proper expansion
|
||||
'w': { top: '50vh', left: '20px', transform: 'translateY(-20px)' }, // Anchor at icon level
|
||||
'wnw': { top: '80px', left: '20px' }, // Top-aligned instead of center
|
||||
'nw': { top: '20px', left: '20px' },
|
||||
'nnw': { top: '20px', left: '35%', transform: 'translateX(-50%)' }
|
||||
},
|
||||
|
||||
// Get expansion direction based on compass position
|
||||
getExpansionDirection: function() {
|
||||
const pos = this.config.position;
|
||||
const rightBorderPositions = ['ne', 'ene', 'e', 'ese', 'se'];
|
||||
const bottomBorderPositions = ['sw', 'ssw', 's', 'sse', 'se'];
|
||||
|
||||
return {
|
||||
header: rightBorderPositions.includes(pos) ? 'left' : 'right',
|
||||
body: bottomBorderPositions.includes(pos) ? 'up' : 'down'
|
||||
};
|
||||
},
|
||||
|
||||
// Calculate position styles based on compass direction
|
||||
getPositionStyles: function() {
|
||||
const compassPos = this.compassPositions[this.config.position] || this.compassPositions['w'];
|
||||
return {
|
||||
position: 'fixed',
|
||||
top: compassPos.top || 'auto',
|
||||
right: compassPos.right || 'auto',
|
||||
bottom: compassPos.bottom || 'auto',
|
||||
left: compassPos.left || 'auto',
|
||||
transform: compassPos.transform || 'none',
|
||||
zIndex: 1000
|
||||
};
|
||||
},
|
||||
|
||||
// Abstract methods (to be implemented by subclasses)
|
||||
buildContent: function() {
|
||||
const content = this.element.querySelector('.control-content');
|
||||
content.innerHTML = `<p style="padding: 1rem; color: #666;">${this.config.defaultContent}</p>`;
|
||||
},
|
||||
|
||||
// Concrete methods (shared by all controls)
|
||||
createControl: function() {
|
||||
console.log(`🎛️ Creating ${this.config.title} control...`);
|
||||
|
||||
this.element = document.createElement('div');
|
||||
this.element.className = this.config.className;
|
||||
this.element.innerHTML = `
|
||||
<button class="control-toggle" aria-label="${this.config.ariaLabel}">${this.config.icon}</button>
|
||||
<div class="control-panel" style="display: none;">
|
||||
<div class="control-header">
|
||||
<span class="control-icon">${this.config.icon}</span>
|
||||
<span class="control-title">${this.config.title}</span>
|
||||
<button class="control-close">✕</button>
|
||||
</div>
|
||||
<div class="control-content">Loading...</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Position using compass direction
|
||||
const positionStyles = this.getPositionStyles();
|
||||
this.element.style.cssText = `
|
||||
position: ${positionStyles.position};
|
||||
top: ${positionStyles.top};
|
||||
right: ${positionStyles.right};
|
||||
bottom: ${positionStyles.bottom};
|
||||
left: ${positionStyles.left};
|
||||
transform: ${positionStyles.transform};
|
||||
z-index: ${positionStyles.zIndex};
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border: 1px solid #e1e5e9;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
backdrop-filter: blur(8px);
|
||||
width: 40px;
|
||||
transition: all 0.3s ease;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
`;
|
||||
|
||||
// Store original position for reset
|
||||
this.originalPosition = {
|
||||
top: positionStyles.top,
|
||||
right: positionStyles.right,
|
||||
bottom: positionStyles.bottom,
|
||||
left: positionStyles.left,
|
||||
transform: positionStyles.transform
|
||||
};
|
||||
|
||||
// Style toggle button
|
||||
const toggleBtn = this.element.querySelector('.control-toggle');
|
||||
toggleBtn.style.cssText = `
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
transition: color 0.2s ease;
|
||||
`;
|
||||
|
||||
// Handle click to build content on-demand
|
||||
toggleBtn.addEventListener('click', () => {
|
||||
if (this.isExpanded) {
|
||||
this.collapse();
|
||||
} else {
|
||||
console.log(`🎛️ ${this.config.title} toggle clicked - building content...`);
|
||||
this.buildContent();
|
||||
}
|
||||
});
|
||||
|
||||
// Close button handler
|
||||
const closeBtn = this.element.querySelector('.control-close');
|
||||
closeBtn.addEventListener('click', () => {
|
||||
this.collapse();
|
||||
});
|
||||
|
||||
// Responsive behavior
|
||||
window.addEventListener('resize', () => {
|
||||
if (window.innerWidth <= 768) {
|
||||
this.element.style.display = 'none';
|
||||
} else {
|
||||
this.element.style.display = '';
|
||||
}
|
||||
});
|
||||
|
||||
document.body.appendChild(this.element);
|
||||
|
||||
// Hide on mobile
|
||||
if (window.innerWidth <= 768) {
|
||||
this.element.style.display = 'none';
|
||||
}
|
||||
|
||||
console.log(`🎛️ ${this.config.title} control created`);
|
||||
},
|
||||
|
||||
styleHeader: function() {
|
||||
const header = this.element.querySelector('.control-header');
|
||||
|
||||
// Style the header to show icon, title, and close button in one line
|
||||
// Match the height of the collapsed icon state (40px)
|
||||
header.style.cssText = `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 40px;
|
||||
padding: 0 1rem;
|
||||
border-bottom: 1px solid #eee;
|
||||
margin-bottom: 0;
|
||||
`;
|
||||
|
||||
const icon = header.querySelector('.control-icon');
|
||||
if (icon) {
|
||||
icon.style.cssText = `
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
margin-right: 0.5rem;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
`;
|
||||
|
||||
// Make icon draggable
|
||||
this.setupDragHandlers(icon);
|
||||
}
|
||||
|
||||
const title = header.querySelector('.control-title');
|
||||
if (title) {
|
||||
title.style.cssText = `
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
flex-grow: 1;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
`;
|
||||
|
||||
// Add click handler to toggle header-only mode
|
||||
title.addEventListener('click', () => {
|
||||
this.toggleHeaderOnly();
|
||||
});
|
||||
}
|
||||
|
||||
const closeBtn = header.querySelector('.control-close');
|
||||
if (closeBtn) {
|
||||
closeBtn.style.cssText = `
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
color: #6c757d;
|
||||
padding: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: color 0.2s ease;
|
||||
`;
|
||||
}
|
||||
},
|
||||
|
||||
styleContent: function() {
|
||||
const content = this.element.querySelector('.control-content');
|
||||
const expansion = this.getExpansionDirection();
|
||||
|
||||
// Style the content area based on expansion direction
|
||||
let contentStyles = `
|
||||
padding: 0.5rem;
|
||||
overflow-y: auto;
|
||||
`;
|
||||
|
||||
if (expansion.body === 'up') {
|
||||
// Body expands upward (for bottom border positions)
|
||||
contentStyles += `
|
||||
max-height: calc(80vh - 40px);
|
||||
`;
|
||||
content.parentElement.style.flexDirection = 'column-reverse';
|
||||
} else {
|
||||
// Body expands downward (default)
|
||||
contentStyles += `
|
||||
max-height: calc(80vh - 40px);
|
||||
`;
|
||||
content.parentElement.style.flexDirection = 'column';
|
||||
}
|
||||
|
||||
content.style.cssText = contentStyles;
|
||||
},
|
||||
|
||||
expand: function() {
|
||||
this.isExpanded = true;
|
||||
const panel = this.element.querySelector('.control-panel');
|
||||
const toggleBtn = this.element.querySelector('.control-toggle');
|
||||
|
||||
// Get expansion direction based on compass position
|
||||
const expansion = this.getExpansionDirection();
|
||||
|
||||
// Apply expansion styling based on direction
|
||||
if (expansion.header === 'left') {
|
||||
// Header expands to the left (for right border positions)
|
||||
this.element.style.width = '280px';
|
||||
this.element.style.transformOrigin = 'top right';
|
||||
} else {
|
||||
// Header expands to the right (default)
|
||||
this.element.style.width = '280px';
|
||||
this.element.style.transformOrigin = 'top left';
|
||||
}
|
||||
|
||||
panel.style.display = 'block';
|
||||
toggleBtn.style.display = 'none';
|
||||
|
||||
this.styleHeader();
|
||||
this.styleContent();
|
||||
this.addResizeHandle();
|
||||
},
|
||||
|
||||
collapse: function() {
|
||||
this.isExpanded = false;
|
||||
this.isHeaderOnly = false; // Reset header-only state
|
||||
const panel = this.element.querySelector('.control-panel');
|
||||
const toggleBtn = this.element.querySelector('.control-toggle');
|
||||
panel.style.display = 'none';
|
||||
|
||||
// Reset size to default
|
||||
this.element.style.width = '40px';
|
||||
this.element.style.height = 'auto';
|
||||
|
||||
// Remove resize handle
|
||||
this.removeResizeHandle();
|
||||
|
||||
toggleBtn.style.display = 'block';
|
||||
|
||||
// Reset position to original compass location
|
||||
this.element.style.top = this.originalPosition.top;
|
||||
this.element.style.right = this.originalPosition.right;
|
||||
this.element.style.bottom = this.originalPosition.bottom;
|
||||
this.element.style.left = this.originalPosition.left;
|
||||
this.element.style.transform = this.originalPosition.transform;
|
||||
},
|
||||
|
||||
toggleHeaderOnly: function() {
|
||||
if (!this.isExpanded) {
|
||||
// If collapsed, first expand normally
|
||||
this.buildContent();
|
||||
return;
|
||||
}
|
||||
|
||||
const content = this.element.querySelector('.control-content');
|
||||
|
||||
if (this.isHeaderOnly) {
|
||||
// Show content area (go to full expanded mode)
|
||||
this.isHeaderOnly = false;
|
||||
content.style.display = 'block';
|
||||
console.log(`🎛️ ${this.config.title} expanded to full view`);
|
||||
} else {
|
||||
// Hide content area (go to header-only mode)
|
||||
this.isHeaderOnly = true;
|
||||
content.style.display = 'none';
|
||||
console.log(`🎛️ ${this.config.title} collapsed to header only`);
|
||||
}
|
||||
},
|
||||
|
||||
setupDragHandlers: function(dragElement) {
|
||||
dragElement.addEventListener('mousedown', (e) => {
|
||||
this.isDragging = true;
|
||||
const rect = this.element.getBoundingClientRect();
|
||||
const iconRect = dragElement.getBoundingClientRect();
|
||||
|
||||
// Calculate offset relative to the icon position, not the element
|
||||
this.dragOffset.x = e.clientX - rect.left;
|
||||
this.dragOffset.y = iconRect.top - rect.top + (iconRect.height / 2); // Keep mouse at icon center
|
||||
|
||||
dragElement.style.cursor = 'grabbing';
|
||||
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
if (!this.isDragging || !this.isExpanded) return;
|
||||
|
||||
const newX = e.clientX - this.dragOffset.x;
|
||||
const newY = e.clientY - this.dragOffset.y;
|
||||
|
||||
// Keep within viewport bounds
|
||||
const maxX = window.innerWidth - this.element.offsetWidth;
|
||||
const maxY = window.innerHeight - this.element.offsetHeight;
|
||||
|
||||
const boundedX = Math.max(0, Math.min(newX, maxX));
|
||||
const boundedY = Math.max(0, Math.min(newY, maxY));
|
||||
|
||||
this.element.style.left = boundedX + 'px';
|
||||
this.element.style.top = boundedY + 'px';
|
||||
});
|
||||
|
||||
document.addEventListener('mouseup', () => {
|
||||
if (this.isDragging) {
|
||||
this.isDragging = false;
|
||||
dragElement.style.cursor = 'grab';
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Add resize handle to expanded control
|
||||
addResizeHandle: function() {
|
||||
// Remove existing resize handle if any
|
||||
this.removeResizeHandle();
|
||||
|
||||
const resizeHandle = document.createElement('div');
|
||||
resizeHandle.className = 'control-resize-handle';
|
||||
// Create small circle for resize handle
|
||||
resizeHandle.innerHTML = '';
|
||||
resizeHandle.style.cssText = `
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
right: 2px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
cursor: nw-resize;
|
||||
display: none;
|
||||
user-select: none;
|
||||
z-index: 1001;
|
||||
background: #6c757d;
|
||||
border-radius: 50%;
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.2s ease;
|
||||
`;
|
||||
|
||||
this.element.appendChild(resizeHandle);
|
||||
this.setupResizeHandlers(resizeHandle);
|
||||
this.setupHoverBehavior();
|
||||
},
|
||||
|
||||
// Setup hover behavior for resize handle and close button
|
||||
setupHoverBehavior: function() {
|
||||
const resizeHandle = this.element.querySelector('.control-resize-handle');
|
||||
const closeBtn = this.element.querySelector('.control-close');
|
||||
|
||||
if (resizeHandle && closeBtn) {
|
||||
// Show/hide on control hover
|
||||
this.element.addEventListener('mouseenter', () => {
|
||||
resizeHandle.style.display = 'flex';
|
||||
closeBtn.style.display = 'block';
|
||||
});
|
||||
|
||||
this.element.addEventListener('mouseleave', () => {
|
||||
resizeHandle.style.display = 'none';
|
||||
closeBtn.style.display = 'none';
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Remove resize handle
|
||||
removeResizeHandle: function() {
|
||||
const existingHandle = this.element.querySelector('.control-resize-handle');
|
||||
if (existingHandle) {
|
||||
existingHandle.remove();
|
||||
}
|
||||
},
|
||||
|
||||
// Set up resize event handlers
|
||||
setupResizeHandlers: function(resizeHandle) {
|
||||
resizeHandle.addEventListener('mousedown', (e) => {
|
||||
this.isResizing = true;
|
||||
const rect = this.element.getBoundingClientRect();
|
||||
this.resizeStartSize = {
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
startX: e.clientX,
|
||||
startY: e.clientY
|
||||
};
|
||||
|
||||
resizeHandle.style.cursor = 'nw-resize';
|
||||
resizeHandle.style.color = '#28a745';
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation(); // Prevent triggering drag
|
||||
});
|
||||
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
if (!this.isResizing || !this.isExpanded) return;
|
||||
|
||||
const deltaX = e.clientX - this.resizeStartSize.startX;
|
||||
const deltaY = e.clientY - this.resizeStartSize.startY;
|
||||
|
||||
const newWidth = Math.max(this.defaultSize.minWidth, this.resizeStartSize.width + deltaX);
|
||||
const newHeight = Math.max(this.defaultSize.minHeight, this.resizeStartSize.height + deltaY);
|
||||
|
||||
// Check viewport bounds
|
||||
const maxWidth = window.innerWidth - this.element.offsetLeft;
|
||||
const maxHeight = window.innerHeight - this.element.offsetTop;
|
||||
|
||||
const boundedWidth = Math.min(newWidth, maxWidth - 20);
|
||||
const boundedHeight = Math.min(newHeight, maxHeight - 20);
|
||||
|
||||
this.element.style.width = boundedWidth + 'px';
|
||||
this.element.style.height = boundedHeight + 'px';
|
||||
|
||||
// Ensure content areas resize properly
|
||||
this.updateContentSize();
|
||||
});
|
||||
|
||||
document.addEventListener('mouseup', () => {
|
||||
if (this.isResizing) {
|
||||
this.isResizing = false;
|
||||
resizeHandle.style.cursor = 'nw-resize';
|
||||
resizeHandle.style.color = '#6c757d';
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Update content area sizes during resize
|
||||
updateContentSize: function() {
|
||||
const content = this.element.querySelector('.control-content');
|
||||
if (content) {
|
||||
// Adjust content height to fit the resized control
|
||||
const headerHeight = 40; // Header is 40px
|
||||
const padding = 16; // Account for padding
|
||||
const controlHeight = this.element.offsetHeight;
|
||||
const availableHeight = controlHeight - headerHeight - padding;
|
||||
|
||||
content.style.maxHeight = Math.max(100, availableHeight) + 'px';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Step 5: Initialize ContentsControl (new implementation based on Control class)
|
||||
try {
|
||||
const contentsControl = Object.create(Control);
|
||||
|
||||
// Configure for contents navigation
|
||||
contentsControl.config = {
|
||||
icon: '☰',
|
||||
title: 'Contents',
|
||||
className: 'contents-control',
|
||||
defaultContent: 'No headings found',
|
||||
ariaLabel: 'Document Navigation',
|
||||
position: 'wnw' // West-north-west positioning
|
||||
};
|
||||
|
||||
// Override buildContent method for navigation functionality
|
||||
contentsControl.buildContent = function() {
|
||||
const content = this.element.querySelector('.control-content');
|
||||
|
||||
// Build navigation content from current DOM
|
||||
const allHeadings = document.querySelectorAll('h1, h2, h3');
|
||||
// Filter out headings that contain "Contents" or similar navigation-related text
|
||||
const headings = Array.from(allHeadings).filter(heading => {
|
||||
const text = heading.textContent.trim().toLowerCase();
|
||||
return !text.includes('contents') && !text.includes('table of contents') && !text.includes('navigation');
|
||||
});
|
||||
console.log("📋 Found headings for navigation:", headings.length);
|
||||
|
||||
if (headings.length === 0) {
|
||||
content.innerHTML = '<p style="padding: 1rem; color: #666;">No headings found</p>';
|
||||
} else {
|
||||
let navHtml = '';
|
||||
headings.forEach((heading, index) => {
|
||||
if (!heading.id) {
|
||||
heading.id = `heading-${index + 1}`;
|
||||
}
|
||||
const level = parseInt(heading.tagName.substring(1));
|
||||
const indent = (level - 1) * 1;
|
||||
navHtml += `
|
||||
<a href="#${heading.id}"
|
||||
style="display: block; padding: 0.5rem; margin-left: ${indent}rem;
|
||||
text-decoration: none; color: #333; font-size: 0.9rem;
|
||||
border-radius: 4px; cursor: pointer;"
|
||||
onmouseover="this.style.backgroundColor='#f5f5f5'"
|
||||
onmouseout="this.style.backgroundColor=''"
|
||||
onclick="event.preventDefault(); document.getElementById('${heading.id}').scrollIntoView({behavior: 'smooth'}); if (window.innerWidth <= 768) setTimeout(() => contentsControl.collapse(), 500);">
|
||||
${heading.textContent.trim()}
|
||||
</a>
|
||||
`;
|
||||
});
|
||||
content.innerHTML = navHtml;
|
||||
}
|
||||
|
||||
// Show panel
|
||||
this.expand();
|
||||
};
|
||||
|
||||
// Initialize the ContentsControl
|
||||
contentsControl.createControl();
|
||||
|
||||
// Make globally available for mobile collapse
|
||||
window.contentsControl = contentsControl;
|
||||
} catch (error) {
|
||||
console.error("ContentsControl failed to initialize:", error);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// Handle CDN loading errors
|
||||
window.addEventListener('load', function() {
|
||||
if (window.markitectMarkedError) {
|
||||
console.error("CDN library failed to load - network or firewall blocking marked.js");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -36,7 +36,33 @@ This historical documentation serves multiple purposes:
|
||||
|
||||
Files are organized by type and chronologically when applicable. GAMEPLAN files represent strategic planning phases, while diary entries document actual achievements and milestones.
|
||||
|
||||
## Reference Files (2025-11-12)
|
||||
|
||||
**CRITICAL STABLE STATE CAPTURE**
|
||||
|
||||
Due to a refactoring session that became overly complex and violated GUARDRAILS.md principles, we captured reference files from the last stable commit before the failed attempt:
|
||||
|
||||
**Commit:** `dbde13e` - "feat: enhance control system with improved UI and debug functionality"
|
||||
**Date:** 2025-11-11 00:29:34 +0100
|
||||
|
||||
### Files:
|
||||
- `GUARDRAILS-edit-mode-dbde13e-2025-11-11-00-29-34.html` - Edit mode output
|
||||
- `GUARDRAILS-static-dbde13e-2025-11-11-00-29-34.html` - Static mode output
|
||||
|
||||
### What This Represents:
|
||||
This is the reference point for what "working edit mode" should look like. Any future attempts to restore or fix edit mode functionality should be tested against these reference files.
|
||||
|
||||
### Key Characteristics:
|
||||
- Edit mode message: "✓ Rendered with interactive editing capabilities"
|
||||
- Should contain working UI controls
|
||||
- Should display content properly
|
||||
- Should have functional section editing
|
||||
|
||||
### Critical Lesson:
|
||||
**Always commit stable functionality before attempting refactoring!** This mistake of not having a clear stable baseline made recovery unnecessarily difficult.
|
||||
|
||||
---
|
||||
|
||||
*Organized as part of Issue #47: GAMEPLAN and DIARY files consolidation*
|
||||
*Created: October 1, 2025*
|
||||
*Created: October 1, 2025*
|
||||
*Updated: November 12, 2025 - Added critical stable state references*
|
||||
@@ -1306,7 +1306,7 @@ MISSING: {len(missing_components)} components
|
||||
return ""
|
||||
|
||||
def _get_clean_editor_scripts(self) -> str:
|
||||
"""Load the modular editor JavaScript components for edit/insert modes."""
|
||||
"""Load the modular editor JavaScript components from external files."""
|
||||
from pathlib import Path
|
||||
|
||||
# Define the modular components to load in order
|
||||
@@ -1314,7 +1314,12 @@ MISSING: {len(missing_components)} components
|
||||
'js/core/section-manager.js',
|
||||
'js/components/debug-panel.js',
|
||||
'js/components/document-controls.js',
|
||||
'js/components/dom-renderer.js'
|
||||
'js/components/dom-renderer.js',
|
||||
'js/controls/control-base.js',
|
||||
'js/controls/contents-control.js',
|
||||
'js/controls/status-control.js',
|
||||
'js/controls/debug-control.js',
|
||||
'js/controls/edit-control.js'
|
||||
]
|
||||
|
||||
base_path = Path(__file__).parent / 'static'
|
||||
@@ -1339,45 +1344,154 @@ MISSING: {len(missing_components)} components
|
||||
# Add initialization script to wire up the components
|
||||
initialization_script = """
|
||||
// === Component Initialization ===
|
||||
function initializeCleanEditor() {
|
||||
console.log('🚀 Initializing Clean Editor Components...');
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Create container for the markdown content
|
||||
const container = document.getElementById('markdown-content') || document.body;
|
||||
|
||||
try {
|
||||
// Initialize components
|
||||
const sectionManager = new SectionManager();
|
||||
const domRenderer = new DOMRenderer(sectionManager, container);
|
||||
const debugPanel = new DebugPanel();
|
||||
const documentControls = new DocumentControls();
|
||||
// Initialize components
|
||||
const sectionManager = new SectionManager();
|
||||
const domRenderer = new DOMRenderer(sectionManager, container);
|
||||
const debugPanel = new DebugPanel();
|
||||
const documentControls = new DocumentControls();
|
||||
|
||||
// Create document controls
|
||||
documentControls.create();
|
||||
// Create document controls
|
||||
documentControls.create();
|
||||
|
||||
console.log('✓ Clean Editor initialized successfully');
|
||||
console.log('✓ Click on any section to start editing');
|
||||
// Step 4: Initialize modern Control-based architecture with compass positioning
|
||||
console.log("🎛️ Initializing modern Control system with compass positioning...");
|
||||
|
||||
// Make components globally available for debugging
|
||||
window.editorComponents = {
|
||||
sectionManager,
|
||||
domRenderer,
|
||||
debugPanel,
|
||||
documentControls
|
||||
};
|
||||
// ContentsControl (positioned upper left - nw)
|
||||
const contentsControl = new ContentsControl();
|
||||
contentsControl.control.config.position = 'nw'; // Upper left
|
||||
contentsControl.createControl();
|
||||
window.contentsControl = contentsControl;
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Clean Editor initialization failed:', error);
|
||||
throw error;
|
||||
// StatusControl (positioned right - e)
|
||||
const statusControl = new StatusControl();
|
||||
statusControl.control.config.position = 'e'; // Right
|
||||
statusControl.createControl();
|
||||
window.statusControl = statusControl;
|
||||
|
||||
// DebugControl (positioned lower right - se)
|
||||
const debugControl = new DebugControl();
|
||||
debugControl.control.config.position = 'se'; // Lower right
|
||||
debugControl.createControl();
|
||||
window.debugControl = debugControl;
|
||||
|
||||
// EditControl (positioned upper right - ne)
|
||||
const editControl = new EditControl();
|
||||
editControl.control.config.position = 'ne'; // Upper right
|
||||
editControl.createControl();
|
||||
window.editControl = editControl;
|
||||
|
||||
console.log("🎛️ Modern Control system initialized with compass positioning");
|
||||
|
||||
// Wire up event handlers
|
||||
documentControls.setEventHandlers({
|
||||
'save-document': () => {
|
||||
console.log('Save document clicked');
|
||||
try {
|
||||
// Get current markdown content from section manager
|
||||
const currentMarkdown = sectionManager.getDocumentMarkdown();
|
||||
|
||||
// Create filename with timestamp suffix following the established convention
|
||||
const now = new Date();
|
||||
const timestamp = now.toISOString().slice(0, 19).replace(/:/g, '-').replace('T', '-');
|
||||
|
||||
// Extract original filename from config or use default
|
||||
const originalFilename = window.editorConfig?.originalFilename || 'document';
|
||||
const editedFilename = `${originalFilename}-edited-${timestamp}.md`;
|
||||
|
||||
// Create and download the file
|
||||
const blob = new Blob([currentMarkdown], { type: 'text/markdown' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = editedFilename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
// Log success to debug panel
|
||||
debugPanel.addMessage(`Document saved as: ${editedFilename}`, 'SUCCESS');
|
||||
console.log(`Document successfully saved as: ${editedFilename}`);
|
||||
|
||||
} catch (error) {
|
||||
debugPanel.addMessage(`Save failed: ${error.message}`, 'ERROR');
|
||||
console.error('Save error:', error);
|
||||
}
|
||||
},
|
||||
'reset-all': () => {
|
||||
console.log('Reset all clicked');
|
||||
// Hide any open editors
|
||||
domRenderer.hideCurrentEditor();
|
||||
// Reset all sections to original state
|
||||
const allSections = Array.from(sectionManager.sections.values());
|
||||
allSections.forEach(section => {
|
||||
section.resetToOriginal();
|
||||
});
|
||||
// Re-render all sections
|
||||
domRenderer.renderAllSections(allSections);
|
||||
debugPanel.addMessage(`Reset all sections to original state`, 'INFO');
|
||||
},
|
||||
'show-status': () => {
|
||||
const status = sectionManager.getDocumentStatus();
|
||||
alert(`Document Status:\\nTotal Sections: ${status.totalSections}\\nEditing Sections: ${status.editingSections}`);
|
||||
},
|
||||
'toggle-debug': () => {
|
||||
debugPanel.toggle();
|
||||
}
|
||||
});
|
||||
|
||||
// Set up debug panel integration
|
||||
sectionManager.on('sections-created', (data) => {
|
||||
debugPanel.addMessage(`Created ${data.count} sections`, 'INFO');
|
||||
});
|
||||
|
||||
sectionManager.on('edit-started', (data) => {
|
||||
debugPanel.addMessage(`Edit started for section: ${data.sectionId}`, 'DEBUG');
|
||||
});
|
||||
|
||||
sectionManager.on('changes-accepted', (data) => {
|
||||
debugPanel.addMessage(`Changes accepted for section: ${data.sectionId}`, 'SUCCESS');
|
||||
// Re-render the section to show updated content
|
||||
const section = sectionManager.sections.get(data.sectionId);
|
||||
if (section) {
|
||||
const sectionElement = domRenderer.findSectionElement(data.sectionId);
|
||||
if (sectionElement) {
|
||||
const newElement = domRenderer.renderSection(section);
|
||||
sectionElement.parentNode.replaceChild(newElement, sectionElement);
|
||||
debugPanel.addMessage(`DOM updated for section: ${data.sectionId}`, 'INFO');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
sectionManager.on('changes-cancelled', (data) => {
|
||||
debugPanel.addMessage(`Changes cancelled for section: ${data.sectionId}`, 'WARNING');
|
||||
});
|
||||
|
||||
// Initialize with markdown content
|
||||
const markdownToRender = markdownContent || '';
|
||||
if (markdownToRender.trim()) {
|
||||
const sections = sectionManager.createSectionsFromMarkdown(markdownToRender);
|
||||
domRenderer.renderAllSections(sections);
|
||||
debugPanel.addMessage(`Initialized with ${sections.length} sections`, 'INFO');
|
||||
} else {
|
||||
debugPanel.addMessage('No markdown content to initialize', 'WARNING');
|
||||
}
|
||||
}
|
||||
|
||||
function initializeScrollIndicators() {
|
||||
// Placeholder for scroll indicators - can be implemented later
|
||||
console.log('📍 Scroll indicators initialized');
|
||||
}
|
||||
// Make components globally available for debugging
|
||||
window.markitectComponents = {
|
||||
sectionManager,
|
||||
domRenderer,
|
||||
debugPanel,
|
||||
documentControls
|
||||
};
|
||||
|
||||
console.log('Markitect modular editor initialized successfully');
|
||||
});
|
||||
"""
|
||||
|
||||
combined_script.append(initialization_script)
|
||||
return '\n'.join(combined_script)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user