feat: enhance DocumentNavigator with dragging and compact header design
Some checks failed
Test Suite / security-scan (push) Has been cancelled
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled

- Add draggable functionality when expanded - click and drag the ☰ icon to reposition
- Implement automatic position reset to original location when collapsed
- Create compact header design with 40px height matching collapsed icon state
- Remove duplicate icons and filter out navigation-related headings from content
- Add visual feedback with cursor changes (grab/grabbing) during drag operations
- Include viewport boundary constraints to prevent dragging outside browser window
- Optimize header spacing and typography for clean, professional appearance
- Maintain consistent UX across both viewing and edit modes

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-10 20:10:11 +01:00
parent b963940144
commit 2d9175ec05

View File

@@ -1182,6 +1182,9 @@ class CleanDocumentManager:
const documentNavigator = {{
navElement: null,
isExpanded: false,
isDragging: false,
dragOffset: {{ x: 0, y: 0 }},
originalPosition: {{ top: '80px', left: '20px' }},
createControl: function() {{
console.log("📋 Creating DocumentNavigator control for view mode...");
@@ -1192,6 +1195,7 @@ class CleanDocumentManager:
<button class="navigator-toggle" aria-label="Document Navigation">☰</button>
<div class="navigator-panel" style="display: none;">
<div class="navigator-header">
<span class="navigator-icon">☰</span>
<h3>Contents</h3>
<button class="navigator-close">✕</button>
</div>
@@ -1230,8 +1234,12 @@ class CleanDocumentManager:
// Handle click to build navigation on-demand
toggleBtn.addEventListener('click', () => {{
console.log("📋 Navigator toggle clicked - building navigation...");
this.buildNavigation();
if (this.isExpanded) {{
this.collapse();
}} else {{
console.log("📋 Navigator toggle clicked - building navigation...");
this.buildNavigation();
}}
}});
// Close button handler
@@ -1262,9 +1270,69 @@ class CleanDocumentManager:
buildNavigation: function() {{
const panel = this.navElement.querySelector('.navigator-panel');
const content = this.navElement.querySelector('.navigator-content');
const header = this.navElement.querySelector('.navigator-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('.navigator-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('h3');
if (title) {{
title.style.cssText = `
margin: 0;
font-size: 0.9rem;
font-weight: 600;
flex-grow: 1;
line-height: 1;
`;
}}
const closeBtn = header.querySelector('.navigator-close');
if (closeBtn) {{
closeBtn.style.cssText = `
background: none;
border: none;
font-size: 14px;
cursor: pointer;
color: #666;
padding: 0;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
`;
}}
// Build navigation content from current DOM
const headings = document.querySelectorAll('h1, h2, h3');
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) {{
@@ -1292,22 +1360,74 @@ class CleanDocumentManager:
content.innerHTML = navHtml;
}}
// Style the content area to work with compact header
content.style.cssText = `
padding: 0.5rem;
max-height: calc(80vh - 40px);
overflow-y: auto;
`;
// Show panel
this.expand();
}},
}},
expand: function() {{
this.isExpanded = true;
const panel = this.navElement.querySelector('.navigator-panel');
const toggleBtn = this.navElement.querySelector('.navigator-toggle');
this.navElement.style.width = '280px';
panel.style.display = 'block';
toggleBtn.style.display = 'none';
}},
collapse: function() {{
this.isExpanded = false;
const panel = this.navElement.querySelector('.navigator-panel');
const toggleBtn = this.navElement.querySelector('.navigator-toggle');
panel.style.display = 'none';
this.navElement.style.width = '40px';
toggleBtn.style.display = 'block';
// Reset position to original location
this.navElement.style.top = this.originalPosition.top;
this.navElement.style.left = this.originalPosition.left;
}},
setupDragHandlers: function(dragElement) {{
dragElement.addEventListener('mousedown', (e) => {{
this.isDragging = true;
const rect = this.navElement.getBoundingClientRect();
this.dragOffset.x = e.clientX - rect.left;
this.dragOffset.y = e.clientY - rect.top;
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.navElement.offsetWidth;
const maxY = window.innerHeight - this.navElement.offsetHeight;
const boundedX = Math.max(0, Math.min(newX, maxX));
const boundedY = Math.max(0, Math.min(newY, maxY));
this.navElement.style.left = boundedX + 'px';
this.navElement.style.top = boundedY + 'px';
}});
document.addEventListener('mouseup', () => {{
if (this.isDragging) {{
this.isDragging = false;
dragElement.style.cursor = 'grab';
}}
}});
}}
}};
@@ -1384,6 +1504,9 @@ document.addEventListener('DOMContentLoaded', function() {
const documentNavigator = {
navElement: null,
isExpanded: false,
isDragging: false,
dragOffset: { x: 0, y: 0 },
originalPosition: { top: '80px', left: '20px' },
createControl: function() {
console.log("📋 Creating DocumentNavigator control for edit mode...");
@@ -1394,6 +1517,7 @@ document.addEventListener('DOMContentLoaded', function() {
<button class="navigator-toggle" aria-label="Document Navigation">☰</button>
<div class="navigator-panel" style="display: none;">
<div class="navigator-header">
<span class="navigator-icon">☰</span>
<h3>Contents</h3>
<button class="navigator-close">✕</button>
</div>
@@ -1432,8 +1556,12 @@ document.addEventListener('DOMContentLoaded', function() {
// Handle click to build navigation on-demand
toggleBtn.addEventListener('click', () => {
console.log("📋 Navigator toggle clicked - building navigation...");
this.buildNavigation();
if (this.isExpanded) {
this.collapse();
} else {
console.log("📋 Navigator toggle clicked - building navigation...");
this.buildNavigation();
}
});
// Close button handler
@@ -1449,9 +1577,69 @@ document.addEventListener('DOMContentLoaded', function() {
buildNavigation: function() {
const panel = this.navElement.querySelector('.navigator-panel');
const content = this.navElement.querySelector('.navigator-content');
const header = this.navElement.querySelector('.navigator-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('.navigator-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('h3');
if (title) {
title.style.cssText = `
margin: 0;
font-size: 0.9rem;
font-weight: 600;
flex-grow: 1;
line-height: 1;
`;
}
const closeBtn = header.querySelector('.navigator-close');
if (closeBtn) {
closeBtn.style.cssText = `
background: none;
border: none;
font-size: 14px;
cursor: pointer;
color: #666;
padding: 0;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
`;
}
// Build navigation content from current DOM
const headings = document.querySelectorAll('h1, h2, h3');
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) {
@@ -1479,6 +1667,13 @@ document.addEventListener('DOMContentLoaded', function() {
content.innerHTML = navHtml;
}
// Style the content area to work with compact header
content.style.cssText = `
padding: 0.5rem;
max-height: calc(80vh - 40px);
overflow-y: auto;
`;
// Show panel
this.expand();
},
@@ -1486,15 +1681,60 @@ document.addEventListener('DOMContentLoaded', function() {
expand: function() {
this.isExpanded = true;
const panel = this.navElement.querySelector('.navigator-panel');
const toggleBtn = this.navElement.querySelector('.navigator-toggle');
this.navElement.style.width = '300px';
panel.style.display = 'block';
toggleBtn.style.display = 'none';
},
collapse: function() {
this.isExpanded = false;
const panel = this.navElement.querySelector('.navigator-panel');
const toggleBtn = this.navElement.querySelector('.navigator-toggle');
panel.style.display = 'none';
this.navElement.style.width = '40px';
toggleBtn.style.display = 'block';
// Reset position to original location
this.navElement.style.top = this.originalPosition.top;
this.navElement.style.left = this.originalPosition.left;
},
setupDragHandlers: function(dragElement) {
dragElement.addEventListener('mousedown', (e) => {
this.isDragging = true;
const rect = this.navElement.getBoundingClientRect();
this.dragOffset.x = e.clientX - rect.left;
this.dragOffset.y = e.clientY - rect.top;
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.navElement.offsetWidth;
const maxY = window.innerHeight - this.navElement.offsetHeight;
const boundedX = Math.max(0, Math.min(newX, maxX));
const boundedY = Math.max(0, Math.min(newY, maxY));
this.navElement.style.left = boundedX + 'px';
this.navElement.style.top = boundedY + 'px';
});
document.addEventListener('mouseup', () => {
if (this.isDragging) {
this.isDragging = false;
dragElement.style.cursor = 'grab';
}
});
}
};