scradar
Version:
CSS-first scroll interaction library with progress-based animations
490 lines (451 loc) • 17.5 kB
JavaScript
export default class ScradarDebug {
constructor(scradar) {
this.scradar = scradar;
this.overlay = null;
this.isCollapsed = localStorage.getItem('scradar-debug-collapsed') === 'true';
this.isTargetsCollapsed = localStorage.getItem('scradar-targets-collapsed') === 'true';
this.collapsedTargets = JSON.parse(localStorage.getItem('scradar-targets-individual') || '[]');
this.isHidden = localStorage.getItem('scradar-debug-hidden') === 'true';
this.performanceMetrics = {
updateCount: 0,
lastUpdateTime: 0,
avgUpdateTime: 0,
maxUpdateTime: 0,
elementsCount: 0
};
this.init();
}
init() {
if (document.getElementById('scradar-debug-overlay')) return;
this.overlay = document.createElement('div');
this.overlay.id = 'scradar-debug-overlay';
this.overlay.innerHTML = `
<style>
#scradar-debug-overlay {
position: fixed;
top: 10px;
right: 10px;
z-index: 999999;
background: rgba(0, 0, 0, 0.85);
color: #fff;
padding: 15px;
border-radius: 8px;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 12px;
min-width: 300px;
max-width: 420px;
max-height: 80vh;
overflow-y: auto;
pointer-events: all;
user-select: text;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
transition: all 0.3s ease;
}
#scradar-debug-overlay.collapsed {
min-width: 300px;
max-width: 300px;
max-height: 60px;
overflow: hidden;
}
#scradar-debug-overlay::-webkit-scrollbar {
width: 6px;
}
#scradar-debug-overlay::-webkit-scrollbar-track {
background: rgba(255,255,255,0.1);
}
#scradar-debug-overlay::-webkit-scrollbar-thumb {
background: rgba(255,255,255,0.3);
border-radius: 3px;
}
#scradar-debug-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 1px solid rgba(255,255,255,0.2);
cursor: pointer;
}
#scradar-debug-title {
font-weight: bold;
font-size: 14px;
color: #4fc3f7;
display: flex;
align-items: center;
gap: 8px;
}
#scradar-debug-toggle {
font-size: 12px;
color: #81c784;
transition: transform 0.3s ease;
}
#scradar-debug-overlay.collapsed #scradar-debug-toggle {
transform: rotate(-90deg);
}
.scradar-debug-performance {
background: rgba(76, 175, 80, 0.1);
border: 1px solid rgba(76, 175, 80, 0.3);
padding: 8px;
border-radius: 4px;
margin-bottom: 10px;
}
.scradar-debug-warning {
background: rgba(255, 193, 7, 0.1);
border: 1px solid rgba(255, 193, 7, 0.3);
padding: 8px;
border-radius: 4px;
margin-bottom: 10px;
color: #ffc107;
}
.scradar-debug-error {
background: rgba(244, 67, 54, 0.1);
border: 1px solid rgba(244, 67, 54, 0.3);
padding: 8px;
border-radius: 4px;
margin-bottom: 10px;
color: #f44336;
}
#scradar-debug-close {
cursor: pointer;
padding: 2px 8px;
background: rgba(255,255,255,0.1);
border-radius: 4px;
transition: background 0.2s;
}
#scradar-debug-close:hover {
background: rgba(255,255,255,0.2);
}
.scradar-debug-section {
margin-bottom: 15px;
}
.scradar-debug-section-header {
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
padding: 5px 0;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.scradar-debug-section-header:hover {
background: rgba(255,255,255,0.05);
}
.scradar-debug-section-content {
transition: all 0.3s ease;
overflow: hidden;
}
.scradar-debug-section-content.collapsed {
max-height: 0;
opacity: 0;
}
.scradar-debug-label {
color: #81c784;
font-weight: bold;
margin-bottom: 5px;
}
.scradar-debug-value {
color: #ffd54f;
font-weight: bold;
}
.scradar-debug-target {
background: rgba(255,255,255,0.05);
padding: 8px;
margin-bottom: 8px;
border-radius: 4px;
border-left: 3px solid #4fc3f7;
}
.scradar-debug-target-header {
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
margin-bottom: 4px;
}
.scradar-debug-target-header:hover {
background: rgba(255,255,255,0.05);
border-radius: 2px;
padding: 2px;
margin: -2px;
}
.scradar-debug-target-title {
color: #4fc3f7;
font-weight: bold;
}
.scradar-debug-target-toggle {
font-size: 10px;
color: #81c784;
transition: transform 0.3s ease;
}
.scradar-debug-target-content {
transition: all 0.3s ease;
overflow: hidden;
}
.scradar-debug-target-content.collapsed {
max-height: 0;
opacity: 0;
}
.scradar-debug-progress {
display: grid;
grid-template-columns: auto 1fr;
gap: 4px 8px;
font-size: 11px;
}
.scradar-debug-progress-bar {
height: 3px;
background: rgba(255,255,255,0.1);
border-radius: 2px;
overflow: hidden;
margin-top: 2px;
}
.scradar-debug-progress-fill {
height: 100%;
background: #4fc3f7;
transition: width 0.1s;
}
.scradar-debug-shortcuts {
font-size: 10px;
color: #aaa;
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid rgba(255,255,255,0.1);
}
</style>
<div id="scradar-debug-header">
<div id="scradar-debug-title">
🎯 Scradar Debug
<span id="scradar-debug-toggle">▼</span>
</div>
<div id="scradar-debug-close">✕</div>
</div>
<div id="scradar-debug-content">
<div class="scradar-debug-performance">
<div class="scradar-debug-label">⚡ Performance</div>
<div class="scradar-debug-progress">
<span>Elements:</span>
<span class="scradar-debug-value" id="debug-elements">-</span>
<span>Updates:</span>
<span class="scradar-debug-value" id="debug-updates">-</span>
<span>Avg Update:</span>
<span class="scradar-debug-value" id="debug-avg">-</span>
<span>Max Update:</span>
<span class="scradar-debug-value" id="debug-max">-</span>
</div>
</div>
<div class="scradar-debug-section">
<div class="scradar-debug-section-header" id="global-header">
<div class="scradar-debug-label">🌍 Global</div>
<span class="scradar-debug-toggle">▼</span>
</div>
<div class="scradar-debug-section-content" id="global-content">
<div class="scradar-debug-progress">
<span>Scroll Progress:</span>
<span class="scradar-debug-value" id="debug-progress">-</span>
<span>Direction:</span>
<span class="scradar-debug-value" id="debug-direction">-</span>
<span>Boundary Target:</span>
<span class="scradar-debug-value" id="debug-target">-</span>
</div>
<div class="scradar-debug-progress-bar">
<div class="scradar-debug-progress-fill" id="debug-progress-bar"></div>
</div>
</div>
</div>
<div class="scradar-debug-section">
<div class="scradar-debug-section-header" id="targets-header">
<div class="scradar-debug-label">🎯 Targets</div>
<span class="scradar-debug-toggle">▼</span>
</div>
<div class="scradar-debug-section-content" id="targets-content">
<div id="debug-targets-list"></div>
</div>
</div>
<div class="scradar-debug-shortcuts">
<div>⌘+Shift+D: Toggle Debug</div>
<div>⌘+Shift+C: Toggle Collapse</div>
</div>
</div>
`;
document.body.append(this.overlay);
// Apply saved states
if (this.isCollapsed) {
this.overlay.classList.add('collapsed');
this.overlay.querySelector('#scradar-debug-toggle').textContent = '▶';
}
if (this.isTargetsCollapsed) {
this.overlay.querySelector('#targets-content').classList.add('collapsed');
this.overlay.querySelector('#targets-header .scradar-debug-toggle').textContent = '▶';
}
// Apply hidden state
if (this.isHidden) {
this.overlay.style.display = 'none';
}
// Event listeners
this.overlay.querySelector('#scradar-debug-close').addEventListener('click', () => {
this.overlay.style.display = 'none';
this.isHidden = true;
localStorage.setItem('scradar-debug-hidden', 'true');
});
this.overlay.querySelector('#scradar-debug-header').addEventListener('click', () => {
this.toggleCollapse();
});
this.overlay.querySelector('#global-header').addEventListener('click', () => {
this.toggleSection('global');
});
this.overlay.querySelector('#targets-header').addEventListener('click', () => {
this.toggleSection('targets');
});
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.shiftKey) {
if (e.key === 'D') {
this.isHidden = !this.isHidden;
this.overlay.style.display = this.isHidden ? 'none' : 'block';
localStorage.setItem('scradar-debug-hidden', this.isHidden);
} else if (e.key === 'C') {
this.toggleCollapse();
}
}
});
this.update();
}
toggleCollapse() {
this.isCollapsed = !this.isCollapsed;
this.overlay.classList.toggle('collapsed', this.isCollapsed);
this.overlay.querySelector('#scradar-debug-toggle').textContent =
this.isCollapsed ? '▶' : '▼';
// Save state
localStorage.setItem('scradar-debug-collapsed', this.isCollapsed);
}
toggleSection(section) {
const content = this.overlay.querySelector(`#${section}-content`);
const toggle = this.overlay.querySelector(`#${section}-header .scradar-debug-toggle`);
if (section === 'targets') {
this.isTargetsCollapsed = !this.isTargetsCollapsed;
content.classList.toggle('collapsed', this.isTargetsCollapsed);
toggle.textContent = this.isTargetsCollapsed ? '▶' : '▼';
localStorage.setItem('scradar-targets-collapsed', this.isTargetsCollapsed);
} else {
content.classList.toggle('collapsed');
toggle.textContent = content.classList.contains('collapsed') ? '▶' : '▼';
}
}
toggleTarget(targetId) {
const targetContent = this.overlay.querySelector(`#target-${targetId}-content`);
const targetToggle = this.overlay.querySelector(`#target-${targetId}-toggle`);
const isCollapsed = targetContent.classList.contains('collapsed');
targetContent.classList.toggle('collapsed', !isCollapsed);
targetToggle.textContent = isCollapsed ? '▼' : '▶';
// Update saved state
if (isCollapsed) {
this.collapsedTargets = this.collapsedTargets.filter(id => id !== targetId);
} else {
this.collapsedTargets.push(targetId);
}
localStorage.setItem('scradar-targets-individual', JSON.stringify(this.collapsedTargets));
}
update() {
if (!this.overlay || this.overlay.style.display === 'none') return;
// Performance tracking
const startTime = performance.now();
const scrollProgress = +(document.documentElement.dataset.scradarProgress || 0);
const scrollDirection = +(document.documentElement.dataset.scradarScroll || 0);
const boundaryTarget = document.documentElement.dataset.scradarTarget || '-';
// Update performance metrics
this.performanceMetrics.elementsCount = this.scradar.elements.length;
const activeElements = this.scradar.elements.filter(el => +el.dataset.scradarIn).length;
// Update performance section
this.overlay.querySelector('#debug-elements').textContent =
`${this.performanceMetrics.elementsCount} (${activeElements} active)`;
this.overlay.querySelector('#debug-updates').textContent =
this.performanceMetrics.updateCount;
this.overlay.querySelector('#debug-avg').textContent =
`${this.performanceMetrics.avgUpdateTime.toFixed(2)}ms`;
this.overlay.querySelector('#debug-max').textContent =
`${this.performanceMetrics.maxUpdateTime.toFixed(2)}ms`;
// Update global section
this.overlay.querySelector('#debug-progress').textContent =
scrollProgress.toFixed(3);
this.overlay.querySelector('#debug-direction').textContent =
scrollDirection === 1 ? '↓ Down' : scrollDirection === -1 ? '↑ Up' : '• Stop';
this.overlay.querySelector('#debug-target').textContent = boundaryTarget;
this.overlay.querySelector('#debug-progress-bar').style.width =
`${scrollProgress * 100}%`;
// Update targets section
const targetsList = this.overlay.querySelector('#debug-targets-list');
let targetsHtml = '';
this.scradar.elements.forEach((el, idx) => {
const ctrl = el.scradar;
if (!ctrl) return;
const targetId = `target-${idx}`;
const title = el.dataset.scradarTitle || el.dataset.scradarConfig || el.className || el.tagName.toLowerCase();
const isIn = +el.dataset.scradarIn;
const isTargetCollapsed = this.collapsedTargets.includes(targetId);
targetsHtml += `
<div class="scradar-debug-target">
<div class="scradar-debug-target-header" onclick="window.scradarDebug.toggleTarget('${targetId}')">
<div class="scradar-debug-target-title">
#${idx + 1} ${title} ${isIn ? '🐵' : '🙈'}
</div>
<span class="scradar-debug-target-toggle" id="target-${targetId}-toggle">${isTargetCollapsed ? '▶' : '▼'}</span>
</div>
<div class="scradar-debug-target-content" id="target-${targetId}-content" ${isTargetCollapsed ? 'class="collapsed"' : ''}>
<div class="scradar-debug-progress">
${ctrl.visibility !== undefined ? `
<span>visibility:</span>
<span class="scradar-debug-value">${ctrl.visibility.toFixed(3)}</span>
` : ''}
${ctrl.fill !== undefined ? `
<span>fill:</span>
<span class="scradar-debug-value">${ctrl.fill.toFixed(3)}</span>
` : ''}
${ctrl.cover !== undefined ? `
<span>cover:</span>
<span class="scradar-debug-value">${ctrl.cover.toFixed(3)}</span>
` : ''}
${ctrl.enter !== undefined ? `
<span>enter:</span>
<span class="scradar-debug-value">${ctrl.enter.toFixed(3)}</span>
` : ''}
${ctrl.exit !== undefined ? `
<span>exit:</span>
<span class="scradar-debug-value">${ctrl.exit.toFixed(3)}</span>
` : ''}
${ctrl.peak !== undefined && ctrl.peak !== 0 ? `
<span>peak:</span>
<span class="scradar-debug-value">${ctrl.peak.toFixed(3)}</span>
` : ''}
${ctrl.currentVisibilityStep !== null ? `
<span>step:</span>
<span class="scradar-debug-value">${ctrl.currentVisibilityStep}</span>
` : ''}
</div>
${ctrl.visibility !== undefined ? `
<div class="scradar-debug-progress-bar">
<div class="scradar-debug-progress-fill" style="width: ${ctrl.visibility * 100}%"></div>
</div>
` : ''}
</div>
</div>
`;
});
targetsList.innerHTML = targetsHtml;
// Update performance metrics
const endTime = performance.now();
const updateTime = endTime - startTime;
this.performanceMetrics.updateCount++;
this.performanceMetrics.lastUpdateTime = updateTime;
// Calculate running average
this.performanceMetrics.avgUpdateTime =
(this.performanceMetrics.avgUpdateTime * (this.performanceMetrics.updateCount - 1) + updateTime)
/ this.performanceMetrics.updateCount;
// Track maximum update time
if (updateTime > this.performanceMetrics.maxUpdateTime) {
this.performanceMetrics.maxUpdateTime = updateTime;
}
}
destroy() {
if (this.overlay) {
this.overlay.remove();
this.overlay = null;
}
}
}