Skip to content

Commit dc6296b

Browse files
ozgesolidkeyclaude
andcommitted
Add minimap hover preview and visibility toggle (v0.7.3)
Minimap preview: - Hover over the minimap to see a magnified preview of ~7 lines at that position, with the target line highlighted - 80ms debounce for smooth performance, no API calls for cached lines - Shows line numbers, truncated text, and current position Settings: - "Show minimap" toggle to completely hide/show the minimap - "Hover preview" toggle to enable/disable the magnifier - Both saved in localStorage and persist across sessions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ec924e6 commit dc6296b

File tree

4 files changed

+150
-1
lines changed

4 files changed

+150
-1
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "logan",
3-
"version": "0.7.2",
3+
"version": "0.7.3",
44
"description": "AI-powered log file viewer and analyzer — handles 14M+ lines with virtual scrolling, MCP agent integration, live serial/logcat/SSH connections, pattern correlation, diff view, and built-in terminal",
55
"keywords": [
66
"log-analyzer",

src/renderer/index.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1020,6 +1020,15 @@ <h3>Settings</h3>
10201020
</label>
10211021
<p class="hint">Automatically run analysis when a file is opened</p>
10221022
</div>
1023+
<div class="settings-group">
1024+
<label class="checkbox-label">
1025+
<input type="checkbox" id="minimap-visible" checked> Show minimap
1026+
</label>
1027+
<label class="checkbox-label" style="margin-left: 16px;">
1028+
<input type="checkbox" id="minimap-preview-enabled" checked> Hover preview
1029+
</label>
1030+
<p class="hint">Toggle the minimap sidebar and its hover magnifier</p>
1031+
</div>
10231032
<div class="settings-group">
10241033
<label>Sidebar Sections</label>
10251034
<div class="settings-checkboxes" id="sidebar-section-toggles">

src/renderer/renderer.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,8 @@ interface UserSettings {
390390
defaultFontSize: number; // 10-20, pixels
391391
defaultGapThreshold: number; // 1-60, seconds
392392
autoAnalyze: boolean;
393+
minimapVisible: boolean;
394+
minimapPreview: boolean;
393395
theme: 'dark' | 'paper';
394396
sidebarSections: Record<string, boolean>; // section-id → visible
395397
}
@@ -409,6 +411,8 @@ const DEFAULT_SETTINGS: UserSettings = {
409411
defaultFontSize: 13,
410412
defaultGapThreshold: 5,
411413
autoAnalyze: false,
414+
minimapVisible: true,
415+
minimapPreview: true,
412416
theme: 'dark',
413417
sidebarSections: { ...DEFAULT_SIDEBAR_SECTIONS },
414418
};
@@ -815,6 +819,8 @@ const elements = {
815819
defaultGapThresholdSlider: document.getElementById('default-gap-threshold') as HTMLInputElement,
816820
defaultGapThresholdValue: document.getElementById('default-gap-threshold-value') as HTMLSpanElement,
817821
autoAnalyzeCheckbox: document.getElementById('auto-analyze') as HTMLInputElement,
822+
minimapVisibleCheckbox: document.getElementById('minimap-visible') as HTMLInputElement,
823+
minimapPreviewCheckbox: document.getElementById('minimap-preview-enabled') as HTMLInputElement,
818824
themeSelect: document.getElementById('theme-select') as HTMLSelectElement,
819825
btnResetSettings: document.getElementById('btn-reset-settings') as HTMLButtonElement,
820826
btnCloseSettings: document.getElementById('btn-close-settings') as HTMLButtonElement,
@@ -1952,6 +1958,12 @@ function createLogViewer(): void {
19521958
minimapElement.appendChild(minimapContentElement);
19531959
minimapElement.appendChild(minimapViewportElement);
19541960

1961+
// Minimap hover preview box
1962+
const previewEl = document.createElement('div');
1963+
previewEl.className = 'minimap-preview hidden';
1964+
previewEl.id = 'minimap-preview';
1965+
minimapElement.appendChild(previewEl);
1966+
19551967
// Add to wrapper
19561968
logViewerWrapper.appendChild(logViewerElement);
19571969
logViewerWrapper.appendChild(minimapElement);
@@ -2016,6 +2028,8 @@ function createLogViewer(): void {
20162028
}, { passive: false });
20172029
minimapElement.addEventListener('click', handleMinimapClick);
20182030
minimapElement.addEventListener('mousedown', handleMinimapDrag);
2031+
minimapElement.addEventListener('mousemove', handleMinimapHover);
2032+
minimapElement.addEventListener('mouseleave', hideMinimapPreview);
20192033

20202034
// Use ResizeObserver for responsive updates
20212035
const resizeObserver = new ResizeObserver(() => {
@@ -3274,6 +3288,65 @@ function handleMinimapClick(event: MouseEvent): void {
32743288
logViewerElement.scrollTop = Math.max(0, targetScrollTop);
32753289
}
32763290

3291+
// ─── Minimap Hover Preview ──────────────────────────────────────────────
3292+
3293+
let minimapPreviewTimer: ReturnType<typeof setTimeout> | null = null;
3294+
let minimapPreviewLine = -1;
3295+
3296+
function isMinimapPreviewEnabled(): boolean {
3297+
return userSettings.minimapPreview;
3298+
}
3299+
3300+
function handleMinimapHover(event: MouseEvent): void {
3301+
if (!minimapElement || isDraggingMinimap || !isMinimapPreviewEnabled()) return;
3302+
3303+
const rect = minimapElement.getBoundingClientRect();
3304+
const hoverY = event.clientY - rect.top;
3305+
const minimapHeight = minimapElement.clientHeight;
3306+
const totalLines = getTotalLines();
3307+
if (totalLines === 0) return;
3308+
3309+
const targetLine = Math.floor((hoverY / minimapHeight) * totalLines);
3310+
if (targetLine === minimapPreviewLine) return; // Same line, skip
3311+
minimapPreviewLine = targetLine;
3312+
3313+
// Debounce to avoid hammering getLines
3314+
if (minimapPreviewTimer) clearTimeout(minimapPreviewTimer);
3315+
minimapPreviewTimer = setTimeout(async () => {
3316+
const preview = document.getElementById('minimap-preview');
3317+
if (!preview) return;
3318+
3319+
const PREVIEW_LINES = 7;
3320+
const startLine = Math.max(0, targetLine - Math.floor(PREVIEW_LINES / 2));
3321+
const result = await window.api.getLines(startLine, PREVIEW_LINES);
3322+
if (!result.success || !result.lines?.length) {
3323+
preview.classList.add('hidden');
3324+
return;
3325+
}
3326+
3327+
let html = `<div class="minimap-preview-header">Line ${targetLine + 1}</div>`;
3328+
for (const line of result.lines) {
3329+
const isCurrent = line.lineNumber === targetLine;
3330+
const text = line.text.length > 120 ? line.text.substring(0, 120) + '...' : line.text;
3331+
html += `<div class="minimap-preview-line${isCurrent ? ' current' : ''}"><span class="minimap-preview-num">${line.lineNumber + 1}</span>${escapeHtml(text)}</div>`;
3332+
}
3333+
preview.innerHTML = html;
3334+
3335+
// Position: vertically centered on hover Y, to the left of the minimap
3336+
const previewHeight = Math.max(120, (PREVIEW_LINES + 1) * 16 + 8);
3337+
const top = Math.max(0, Math.min(hoverY - previewHeight / 2, minimapHeight - previewHeight));
3338+
preview.style.top = `${top}px`;
3339+
preview.classList.remove('hidden');
3340+
}, 80);
3341+
}
3342+
3343+
function hideMinimapPreview(): void {
3344+
if (minimapPreviewTimer) { clearTimeout(minimapPreviewTimer); minimapPreviewTimer = null; }
3345+
minimapPreviewLine = -1;
3346+
const preview = document.getElementById('minimap-preview');
3347+
if (preview) preview.classList.add('hidden');
3348+
}
3349+
32773350
let isDraggingMinimap = false;
32783351

32793352
function handleMinimapDrag(_event: MouseEvent): void {
@@ -11929,6 +12002,9 @@ function setupActivityBar(): void {
1192912002
elements.defaultGapThresholdSlider.value = userSettings.defaultGapThreshold.toString();
1193012003
elements.defaultGapThresholdValue.textContent = `${userSettings.defaultGapThreshold}s`;
1193112004
elements.autoAnalyzeCheckbox.checked = userSettings.autoAnalyze;
12005+
elements.minimapVisibleCheckbox.checked = userSettings.minimapVisible;
12006+
elements.minimapPreviewCheckbox.checked = userSettings.minimapPreview;
12007+
if (minimapElement) minimapElement.style.display = userSettings.minimapVisible ? '' : 'none';
1193212008
elements.themeSelect.value = userSettings.theme;
1193312009
populateSidebarSectionToggles();
1193412010
// Load Datadog config
@@ -13155,6 +13231,19 @@ function init(): void {
1315513231
saveSettings();
1315613232
});
1315713233

13234+
elements.minimapVisibleCheckbox.addEventListener('change', () => {
13235+
userSettings.minimapVisible = elements.minimapVisibleCheckbox.checked;
13236+
saveSettings();
13237+
if (minimapElement) minimapElement.style.display = userSettings.minimapVisible ? '' : 'none';
13238+
if (!userSettings.minimapVisible) hideMinimapPreview();
13239+
});
13240+
13241+
elements.minimapPreviewCheckbox.addEventListener('change', () => {
13242+
userSettings.minimapPreview = elements.minimapPreviewCheckbox.checked;
13243+
saveSettings();
13244+
if (!userSettings.minimapPreview) hideMinimapPreview();
13245+
});
13246+
1315813247
elements.themeSelect.addEventListener('change', () => {
1315913248
userSettings.theme = elements.themeSelect.value as 'dark' | 'paper';
1316013249
saveSettings();
@@ -13172,6 +13261,9 @@ function init(): void {
1317213261
elements.defaultGapThresholdSlider.value = userSettings.defaultGapThreshold.toString();
1317313262
elements.defaultGapThresholdValue.textContent = `${userSettings.defaultGapThreshold}s`;
1317413263
elements.autoAnalyzeCheckbox.checked = userSettings.autoAnalyze;
13264+
elements.minimapVisibleCheckbox.checked = userSettings.minimapVisible;
13265+
elements.minimapPreviewCheckbox.checked = userSettings.minimapPreview;
13266+
if (minimapElement) minimapElement.style.display = userSettings.minimapVisible ? '' : 'none';
1317513267
elements.themeSelect.value = userSettings.theme;
1317613268
applyTheme(userSettings.theme);
1317713269
populateSidebarSectionToggles();

src/renderer/styles.css

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2893,6 +2893,54 @@ body.platform-darwin .titlebar {
28932893
border-color: rgba(255, 255, 255, 0.5);
28942894
}
28952895

2896+
/* Minimap hover preview */
2897+
.minimap-preview {
2898+
position: absolute;
2899+
right: 100%;
2900+
margin-right: 4px;
2901+
width: 450px;
2902+
max-width: 50vw;
2903+
background: var(--bg-secondary);
2904+
border: 1px solid var(--border-color);
2905+
border-radius: 4px;
2906+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
2907+
z-index: 100;
2908+
overflow: hidden;
2909+
pointer-events: none;
2910+
}
2911+
.minimap-preview.hidden { display: none; }
2912+
.minimap-preview-header {
2913+
padding: 4px 8px;
2914+
font-size: 10px;
2915+
font-weight: 600;
2916+
color: var(--text-secondary);
2917+
border-bottom: 1px solid var(--border-color);
2918+
background: var(--bg-tertiary);
2919+
}
2920+
.minimap-preview-line {
2921+
padding: 1px 8px;
2922+
font-size: 11px;
2923+
font-family: var(--font-mono, monospace);
2924+
white-space: nowrap;
2925+
overflow: hidden;
2926+
text-overflow: ellipsis;
2927+
color: var(--text-secondary);
2928+
line-height: 16px;
2929+
}
2930+
.minimap-preview-line.current {
2931+
background: rgba(255, 255, 0, 0.1);
2932+
color: var(--text-primary);
2933+
border-left: 2px solid var(--accent-color);
2934+
}
2935+
.minimap-preview-num {
2936+
display: inline-block;
2937+
width: 50px;
2938+
text-align: right;
2939+
margin-right: 8px;
2940+
color: var(--text-muted);
2941+
font-size: 10px;
2942+
}
2943+
28962944
/* Bookmark markers in minimap */
28972945
.minimap-bookmark {
28982946
position: absolute;

0 commit comments

Comments
 (0)