Runeboard scroll track for terminal output
/* init-scroll.js - Runeboard-integrated terminal scroll track * Day 00255 * Requires: init-state.js (boardMode), init-runeboard.js (DOM) * Provides: Scroll track between wand and rune slots in #dual-grid-row */
(function() { var swatch = window.swatch || function (n) { return getComputedStyle(document.documentElement).getPropertyValue('--swatch-' + n).trim(); }; // Mode color mapping: boardMode -> thumb color var MODE_COLORS = { '1': { color: swatch('ui-ff8c00'), glow: 'rgba(167,167,167,0.6)' }, // Fire '0': { color: swatch('terminal-cursor'), glow: 'rgba(255,255,255,0.4)' }, // Air '-1': { color: swatch('ui-6495ed'), glow: 'rgba(139,139,139,0.6)' } // Earth };
var track = document.getElementById('scroll-track'); if (!track) return;
// Style the track container track.style.cssText = 'width:6px;align-self:stretch;background:rgba(255,255,255,0.05);border-radius: var(--radius-sm, 3px);position:relative;cursor:pointer;margin:0 4px;flex-shrink:0;';
// Create thumb var thumb = document.createElement('div'); thumb.id = 'scroll-thumb'; thumb.style.cssText = 'position:absolute;left:0;width:100%;border-radius: var(--radius-sm, 3px);min-height:8px;transition:top 0.05s linear,height 0.05s linear,background 0.3s,box-shadow 0.3s;cursor:grab;'; track.appendChild(thumb);
// New-content indicator (pulses at bottom of track) var indicator = document.createElement('div'); indicator.id = 'scroll-new-indicator'; indicator.style.cssText = 'position:absolute;bottom:0;left:0;width:100%;height:6px;border-radius:0 0 3px 3px;opacity:0;transition:opacity 0.3s;'; track.appendChild(indicator);
var isDragging = false; var dragStartY = 0; var dragStartViewportY = 0; var hasNewContent = false; var isAtBottom = true; var pulseInterval = null;
function getModeColor() { var mode = typeof boardMode !== 'undefined' ? String(boardMode) : '0'; return MODE_COLORS[mode] || MODE_COLORS['0']; }
function getScrollMetrics() { if (typeof term === 'undefined') return null; var buf = term.buffer.active; return { totalLines: buf.length, viewportRows: term.rows, viewportY: buf.viewportY, baseY: buf.baseY }; }
function updateThumb() { var m = getScrollMetrics(); if (!m || m.baseY <= 0) { // All content fits in viewport - thumb fills track thumb.style.top = '0'; thumb.style.height = '100%'; hasNewContent = false; stopPulse(); indicator.style.opacity = '0'; return; }
var trackH = track.clientHeight; if (trackH <= 0) return;
// Thumb height proportional to visible vs total content var thumbRatio = m.viewportRows / m.totalLines; var thumbH = Math.max(8, thumbRatio * trackH);
// Thumb position: viewportY ranges 0 (top) to baseY (bottom) var scrollRatio = m.viewportY / m.baseY; var thumbTop = scrollRatio * (trackH - thumbH);
thumb.style.top = Math.round(thumbTop) + 'px'; thumb.style.height = Math.round(thumbH) + 'px';
// Track if we're at the bottom isAtBottom = (m.viewportY >= m.baseY);
// New-content indicator if (hasNewContent && !isAtBottom) { indicator.style.opacity = '1'; startPulse(); } else { hasNewContent = false; indicator.style.opacity = '0'; stopPulse(); } }
function applyColors() { var colors = getModeColor(); thumb.style.background = colors.color; thumb.style.boxShadow = 'none'; indicator.style.background = colors.color; }
function startPulse() { if (pulseInterval) return; var on = true; pulseInterval = setInterval(function() { indicator.style.opacity = on ? '0.3' : '1'; on = !on; }, 800); }
function stopPulse() { if (pulseInterval) { clearInterval(pulseInterval); pulseInterval = null; } }
// --- Drag handling --- thumb.addEventListener('pointerdown', function(e) { isDragging = true; dragStartY = e.clientY; var m = getScrollMetrics(); dragStartViewportY = m ? m.viewportY : 0; thumb.style.cursor = 'grabbing'; thumb.style.transition = 'background 0.3s, box-shadow 0.3s'; thumb.setPointerCapture(e.pointerId); e.preventDefault(); e.stopPropagation(); });
document.addEventListener('pointermove', function(e) { if (!isDragging) return; var m = getScrollMetrics(); if (!m || m.baseY <= 0) return;
var trackH = track.clientHeight; var thumbRatio = m.viewportRows / m.totalLines; var thumbH = Math.max(8, thumbRatio * trackH); var scrollableTrack = trackH - thumbH; if (scrollableTrack <= 0) return;
var deltaY = e.clientY - dragStartY; var lineDelta = (deltaY / scrollableTrack) * m.baseY; var newLine = Math.round(dragStartViewportY + lineDelta); newLine = Math.max(0, Math.min(newLine, m.baseY)); term.scrollToLine(newLine); });
document.addEventListener('pointerup', function() { if (isDragging) { isDragging = false; thumb.style.cursor = 'grab'; thumb.style.transition = 'top 0.05s linear, height 0.05s linear, background 0.3s, box-shadow 0.3s'; } });
// --- Click track: jump to position --- track.addEventListener('click', function(e) { if (e.target === thumb || isDragging) return; var m = getScrollMetrics(); if (!m || m.baseY <= 0) return;
var rect = track.getBoundingClientRect(); var clickRatio = (e.clientY - rect.top) / rect.height; var targetLine = Math.round(clickRatio * m.baseY); targetLine = Math.max(0, Math.min(targetLine, m.baseY)); term.scrollToLine(targetLine); });
// --- Double-click: scroll to bottom --- track.addEventListener('dblclick', function() { if (typeof term !== 'undefined') { term.scrollToBottom(); hasNewContent = false; indicator.style.opacity = '0'; stopPulse(); } });
// --- Hover glow --- thumb.addEventListener('mouseenter', function() { var colors = getModeColor(); thumb.style.boxShadow = '0 0 6px ' + colors.glow; });
thumb.addEventListener('mouseleave', function() { if (!isDragging) { thumb.style.boxShadow = 'none'; } });
// --- Hook xterm events --- function hookTerminal() { if (typeof term === 'undefined') { setTimeout(hookTerminal, 200); return; }
term.onScroll(function() { updateThumb(); });
term.onLineFeed(function() { if (!isAtBottom) { hasNewContent = true; } updateThumb(); });
// Initial state applyColors(); updateThumb(); }
// Watch for boardMode changes (mode toggle recolors thumb) var lastMode = typeof boardMode !== 'undefined' ? boardMode : 0; setInterval(function() { var currentMode = typeof boardMode !== 'undefined' ? boardMode : 0; if (currentMode !== lastMode) { lastMode = currentMode; applyColors(); } }, 300);
hookTerminal(); })();