gitmaps 1.1.10 → 1.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/app/analytics.db CHANGED
Binary file
package/app/layout.tsx CHANGED
@@ -877,8 +877,8 @@ export default function RootLayout({ children }: { children: any }) {
877
877
  {/* Sticky Zoom Controls — floating pill, bottom-right */}
878
878
  <div id="detailModeSwitch" className="detail-mode-switch" title="Toggle preview-focused detail mode">
879
879
  <button id="toggleDetailMode" type="button" className="detail-mode-btn">
880
- <span className="detail-mode-label">Preview mode</span>
881
- <span id="detailModeState" className="detail-mode-state">Auto</span>
880
+ <span className="detail-mode-label">Renderer</span>
881
+ <span id="detailModeState" className="detail-mode-state">Preview</span>
882
882
  </button>
883
883
  </div>
884
884
 
@@ -458,9 +458,9 @@ export function setupEventListeners(ctx: CanvasContext) {
458
458
  const mode = getDetailMode();
459
459
  detailModeToggle.classList.toggle('active', mode === 'preview');
460
460
  detailModeToggle.setAttribute('title', mode === 'preview'
461
- ? 'Preview mode forced on click to restore auto detail switching'
462
- : 'Auto detail switching click to keep preview mode on at every zoom');
463
- if (stateEl) stateEl.textContent = mode === 'preview' ? 'Preview' : 'Auto';
461
+ ? 'Preview mode stays in the preview renderer at every zoom. Click to switch to classic cards.'
462
+ : 'Classic modeold full-card renderer. Click to switch back to preview mode.');
463
+ if (stateEl) stateEl.textContent = mode === 'preview' ? 'Preview' : 'Classic';
464
464
  };
465
465
  updateDetailModeUi();
466
466
  detailModeToggle.addEventListener('click', () => {
@@ -468,8 +468,8 @@ export function setupEventListeners(ctx: CanvasContext) {
468
468
  updateDetailModeUi();
469
469
  rerenderCurrentView(ctx);
470
470
  showToast(next === 'preview'
471
- ? 'Preview mode forced on'
472
- : 'Auto detail switching restored', 'info');
471
+ ? 'Preview mode enabled'
472
+ : 'Classic mode enabled', 'info');
473
473
  });
474
474
  }
475
475
 
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from 'bun:test';
2
- import { estimatePreviewCharsPerLine, estimatePreviewLineCapacity, estimateTitleCharsPerLine, getLowZoomPreviewText, getLowZoomScale, wrapPreviewText } from './low-zoom-preview';
2
+ import { estimatePreviewCharsPerLine, estimatePreviewLineCapacity, estimatePreviewMaxScroll, estimateTitleCharsPerLine, getLowZoomPreviewText, getLowZoomScale, wrapPreviewText } from './low-zoom-preview';
3
3
 
4
4
  describe('low zoom preview helpers', () => {
5
5
  test('anchors preview text to approximate saved scroll position', () => {
@@ -42,6 +42,13 @@ describe('low zoom preview helpers', () => {
42
42
  expect(estimatePreviewLineCapacity(700, 0.1)).toBeLessThan(estimatePreviewLineCapacity(700, 1));
43
43
  });
44
44
 
45
+ test('preview scroll range grows with longer files', () => {
46
+ const shortFile = { content: Array.from({ length: 8 }, (_, i) => `line-${i}`).join('\n') };
47
+ const longFile = { content: Array.from({ length: 80 }, (_, i) => `line-${i}`).join('\n') };
48
+ expect(estimatePreviewMaxScroll(shortFile, 700, 1)).toBeGreaterThanOrEqual(0);
49
+ expect(estimatePreviewMaxScroll(longFile, 700, 1)).toBeGreaterThan(estimatePreviewMaxScroll(shortFile, 700, 1));
50
+ });
51
+
45
52
  test('title typography is larger than body typography for readability', () => {
46
53
  const scale = getLowZoomScale(0.18);
47
54
  expect(scale.titleFont).toBeGreaterThan(scale.bodyFont);
@@ -1,3 +1,5 @@
1
+ import { getSettings } from './settings';
2
+
1
3
  const PREVIEWABLE_EXTS = new Set([
2
4
  'ts', 'tsx', 'js', 'jsx', 'json', 'css', 'scss', 'html', 'md', 'py', 'rs', 'go', 'vue', 'svelte', 'toml', 'yaml', 'yml', 'sh', 'sql', 'txt'
3
5
  ]);
@@ -5,8 +7,9 @@ const PREVIEWABLE_EXTS = new Set([
5
7
  export function getLowZoomScale(zoom: number) {
6
8
  const clampedZoom = Math.max(0.08, Math.min(1, zoom));
7
9
  const progress = (clampedZoom - 0.08) / (1 - 0.08);
10
+ const settings = getSettings();
8
11
 
9
- const desiredScreenTitle = 8 + progress * 8;
12
+ const desiredScreenTitle = settings.previewFarTitlePx + progress * (settings.previewNearTitlePx - settings.previewFarTitlePx);
10
13
  const desiredScreenBody = 5.5 + progress * 6.5;
11
14
  const desiredScreenPadding = 6 + progress * 8;
12
15
  const desiredScreenGap = 4 + progress * 4;
@@ -87,12 +90,15 @@ function ellipsizeWrappedLines(lines: string[], maxLines: number) {
87
90
 
88
91
  export function estimatePreviewLineCapacity(height: number, zoom: number): number {
89
92
  const scale = getLowZoomScale(zoom);
93
+ const settings = getSettings();
90
94
  const titleLines = zoom >= 0.35 ? 2 : 1;
91
95
  const available = Math.max(
92
96
  scale.bodyLineHeight * 2,
93
97
  height - scale.padding * 2 - scale.titleLineHeight * titleLines - scale.bodyFont - scale.gap * 3,
94
98
  );
95
- return Math.max(zoom >= 0.6 ? 20 : zoom >= 0.35 ? 12 : 3, Math.floor(available / scale.bodyLineHeight));
99
+ const progress = (Math.max(0.08, Math.min(1, zoom)) - 0.08) / (1 - 0.08);
100
+ const targetLines = settings.previewFarLines + progress * (settings.previewNearLines - settings.previewFarLines);
101
+ return Math.max(Math.round(targetLines), Math.floor(available / scale.bodyLineHeight));
96
102
  }
97
103
 
98
104
  export function estimateTitleCharsPerLine(width: number, zoom: number): number {
@@ -109,6 +115,13 @@ export function estimatePreviewCharsPerLine(width: number, zoom: number): number
109
115
  return Math.max(10, Math.floor(available / avgCharWidth));
110
116
  }
111
117
 
118
+ export function estimatePreviewMaxScroll(file: any, height: number, zoom: number): number {
119
+ if (!file || !file.content) return 0;
120
+ const totalLines = String(file.content).split('\n').length;
121
+ const visibleLines = estimatePreviewLineCapacity(height, zoom);
122
+ return Math.max(0, (totalLines - visibleLines) * 20);
123
+ }
124
+
112
125
  export function renderLowZoomPreviewCanvas(
113
126
  canvas: HTMLCanvasElement,
114
127
  params: {
@@ -144,6 +144,26 @@ function SettingsPanel({ settings }: { settings: GitCanvasSettings }) {
144
144
  </SettingsRow>
145
145
  </SettingsSection>
146
146
 
147
+ {/* Preview Mode Section */}
148
+ <SettingsSection title="Preview Mode">
149
+ <SettingsRow label="Far Zoom Title" desc="Filename size when zoomed far out">
150
+ <Slider id="settingPreviewFarTitlePx" valueId="previewFarTitlePxValue"
151
+ min={6} max={14} step={1} value={settings.previewFarTitlePx} suffix="px" />
152
+ </SettingsRow>
153
+ <SettingsRow label="Near Zoom Title" desc="Filename size when zoomed in within preview mode">
154
+ <Slider id="settingPreviewNearTitlePx" valueId="previewNearTitlePxValue"
155
+ min={10} max={24} step={1} value={settings.previewNearTitlePx} suffix="px" />
156
+ </SettingsRow>
157
+ <SettingsRow label="Far Zoom Lines" desc="Minimum content lines shown when zoomed far out">
158
+ <Slider id="settingPreviewFarLines" valueId="previewFarLinesValue"
159
+ min={1} max={8} step={1} value={settings.previewFarLines} suffix="" />
160
+ </SettingsRow>
161
+ <SettingsRow label="Near Zoom Lines" desc="Target content lines shown when zoomed in">
162
+ <Slider id="settingPreviewNearLines" valueId="previewNearLinesValue"
163
+ min={8} max={40} step={1} value={settings.previewNearLines} suffix="" />
164
+ </SettingsRow>
165
+ </SettingsSection>
166
+
147
167
  {/* Advanced Section */}
148
168
  <SettingsSection title="Advanced">
149
169
  <SettingsRow label="Max Visible Lines" desc="Lines shown per card before virtual scroll">
@@ -261,6 +281,38 @@ export function openSettingsModal(ctx?: any) {
261
281
  updateSettings({ maxVisibleLines: parseInt(maxLinesSlider.value) });
262
282
  });
263
283
 
284
+ const previewFarTitleSlider = _modal.querySelector('#settingPreviewFarTitlePx') as HTMLInputElement;
285
+ const previewFarTitleValue = _modal.querySelector('#previewFarTitlePxValue')!;
286
+ previewFarTitleSlider?.addEventListener('input', () => {
287
+ previewFarTitleValue.textContent = `${previewFarTitleSlider.value}px`;
288
+ updateSettings({ previewFarTitlePx: parseInt(previewFarTitleSlider.value) });
289
+ window.dispatchEvent(new CustomEvent('gitcanvas:preview-settings-changed'));
290
+ });
291
+
292
+ const previewNearTitleSlider = _modal.querySelector('#settingPreviewNearTitlePx') as HTMLInputElement;
293
+ const previewNearTitleValue = _modal.querySelector('#previewNearTitlePxValue')!;
294
+ previewNearTitleSlider?.addEventListener('input', () => {
295
+ previewNearTitleValue.textContent = `${previewNearTitleSlider.value}px`;
296
+ updateSettings({ previewNearTitlePx: parseInt(previewNearTitleSlider.value) });
297
+ window.dispatchEvent(new CustomEvent('gitcanvas:preview-settings-changed'));
298
+ });
299
+
300
+ const previewFarLinesSlider = _modal.querySelector('#settingPreviewFarLines') as HTMLInputElement;
301
+ const previewFarLinesValue = _modal.querySelector('#previewFarLinesValue')!;
302
+ previewFarLinesSlider?.addEventListener('input', () => {
303
+ previewFarLinesValue.textContent = previewFarLinesSlider.value;
304
+ updateSettings({ previewFarLines: parseInt(previewFarLinesSlider.value) });
305
+ window.dispatchEvent(new CustomEvent('gitcanvas:preview-settings-changed'));
306
+ });
307
+
308
+ const previewNearLinesSlider = _modal.querySelector('#settingPreviewNearLines') as HTMLInputElement;
309
+ const previewNearLinesValue = _modal.querySelector('#previewNearLinesValue')!;
310
+ previewNearLinesSlider?.addEventListener('input', () => {
311
+ previewNearLinesValue.textContent = previewNearLinesSlider.value;
312
+ updateSettings({ previewNearLines: parseInt(previewNearLinesSlider.value) });
313
+ window.dispatchEvent(new CustomEvent('gitcanvas:preview-settings-changed'));
314
+ });
315
+
264
316
  // Wire switches
265
317
  const minimapSwitch = _modal.querySelector('#settingMinimap') as HTMLInputElement;
266
318
  minimapSwitch?.addEventListener('change', () => {
@@ -31,6 +31,14 @@ export interface GitCanvasSettings {
31
31
  heatmapEnabled: boolean;
32
32
  /** Heatmap time range in days */
33
33
  heatmapDays: number;
34
+ /** Preview mode far-zoom title size (screen px) */
35
+ previewFarTitlePx: number;
36
+ /** Preview mode near-zoom title size (screen px) */
37
+ previewNearTitlePx: number;
38
+ /** Preview mode far-zoom minimum visible lines */
39
+ previewFarLines: number;
40
+ /** Preview mode near-zoom target visible lines */
41
+ previewNearLines: number;
34
42
  }
35
43
 
36
44
  const DEFAULTS: GitCanvasSettings = {
@@ -46,6 +54,10 @@ const DEFAULTS: GitCanvasSettings = {
46
54
  popupFontSize: 14,
47
55
  heatmapEnabled: false,
48
56
  heatmapDays: 90,
57
+ previewFarTitlePx: 8,
58
+ previewNearTitlePx: 16,
59
+ previewFarLines: 3,
60
+ previewNearLines: 20,
49
61
  };
50
62
 
51
63
  let _settings: GitCanvasSettings | null = null;
@@ -23,7 +23,7 @@
23
23
  */
24
24
  import { measure } from 'measure-fn';
25
25
  import type { CanvasContext } from './context';
26
- import { getLowZoomScale, renderLowZoomPreviewCanvas } from './low-zoom-preview';
26
+ import { estimatePreviewMaxScroll, getLowZoomScale, renderLowZoomPreviewCanvas } from './low-zoom-preview';
27
27
  import { materializeViewport } from './xydraw-bridge';
28
28
 
29
29
  // ── Culling state ──────────────────────────────────────────
@@ -53,12 +53,12 @@ export function markTransformActive() {
53
53
 
54
54
  // Track current LOD mode so we can detect transitions
55
55
  let _currentLodMode: 'full' | 'pill' = 'full';
56
- let _detailMode: 'auto' | 'preview' = (() => {
56
+ let _detailMode: 'classic' | 'preview' = (() => {
57
57
  try {
58
58
  const stored = localStorage.getItem(LOW_ZOOM_MODE_STORAGE_KEY);
59
- return stored === 'preview' ? 'preview' : 'auto';
59
+ return stored === 'classic' ? 'classic' : 'preview';
60
60
  } catch {
61
- return 'auto';
61
+ return 'preview';
62
62
  }
63
63
  })();
64
64
 
@@ -99,19 +99,19 @@ export function getPinnedCards(): Set<string> {
99
99
  return _pinnedCards;
100
100
  }
101
101
 
102
- export function getDetailMode(): 'auto' | 'preview' {
102
+ export function getDetailMode(): 'classic' | 'preview' {
103
103
  return _detailMode;
104
104
  }
105
105
 
106
- export function setDetailMode(mode: 'auto' | 'preview') {
106
+ export function setDetailMode(mode: 'classic' | 'preview') {
107
107
  _detailMode = mode;
108
108
  try {
109
109
  localStorage.setItem(LOW_ZOOM_MODE_STORAGE_KEY, mode);
110
110
  } catch { }
111
111
  }
112
112
 
113
- export function toggleDetailMode(): 'auto' | 'preview' {
114
- const next = _detailMode === 'preview' ? 'auto' : 'preview';
113
+ export function toggleDetailMode(): 'classic' | 'preview' {
114
+ const next = _detailMode === 'preview' ? 'classic' : 'preview';
115
115
  setDetailMode(next);
116
116
  return next;
117
117
  }
@@ -323,7 +323,7 @@ export function performViewportCulling(ctx: CanvasContext) {
323
323
  // Phase 4c: also materialize deferred CardManager cards
324
324
  // Reuse zoom from worldRect (already snapped) — avoids redundant ctx.snap()
325
325
  const zoom = worldRect.zoom;
326
- const isLowZoom = _detailMode === 'preview' || zoom <= LOD_ZOOM_THRESHOLD;
326
+ const isLowZoom = _detailMode === 'preview';
327
327
 
328
328
  // Important: never materialize full cards while in low-zoom pill mode.
329
329
  // Otherwise CardManager keeps mounting heavyweight cards right when the
@@ -632,6 +632,42 @@ export function setupPillInteraction(ctx: CanvasContext) {
632
632
  let pillMoveInfos: { pill: HTMLElement; path: string; startLeft: number; startTop: number }[] = [];
633
633
  const DRAG_THRESHOLD = 5;
634
634
 
635
+ ctx.canvas.addEventListener('wheel', (e: WheelEvent) => {
636
+ const pill = (e.target as HTMLElement).closest('.file-pill') as HTMLElement;
637
+ if (!pill) return;
638
+ if (e.ctrlKey || e.metaKey) return;
639
+
640
+ e.preventDefault();
641
+ e.stopPropagation();
642
+
643
+ const path = pill.dataset.path || '';
644
+ if (!path) return;
645
+
646
+ const file = (pill as any)._fileData || ctx.allFilesData?.find(f => f.path === path) || ctx.commitFilesData?.find(f => f.path === path) || null;
647
+ const zoom = ctx.snap().context.zoom || 1;
648
+ const height = parseFloat(pill.style.height) || 700;
649
+ const current = getSavedScrollTop(ctx, path);
650
+ const maxScroll = estimatePreviewMaxScroll(file, height, zoom);
651
+ const next = Math.max(0, Math.min(maxScroll, current + e.deltaY));
652
+ if (next === current) return;
653
+
654
+ const key = `scroll:${path}`;
655
+ const existing = ctx.positions.get(key) || {};
656
+ ctx.positions.set(key, { ...existing, x: next, y: existing.y || 0 });
657
+ try {
658
+ const { debounceSaveScroll } = require('./cards');
659
+ debounceSaveScroll(ctx, path, next);
660
+ } catch { }
661
+ updatePillCardLayout(ctx, pill, zoom, pill.dataset.changed === 'true');
662
+ }, { passive: false });
663
+
664
+ window.addEventListener('gitcanvas:preview-settings-changed', () => {
665
+ const zoom = ctx.snap().context.zoom || 1;
666
+ for (const [, pill] of pillCards) {
667
+ updatePillCardLayout(ctx, pill, zoom, pill.dataset.changed === 'true');
668
+ }
669
+ });
670
+
635
671
  ctx.canvas.addEventListener('mousedown', (e: MouseEvent) => {
636
672
  if (e.button !== 0) return;
637
673
  const pill = (e.target as HTMLElement).closest('.file-pill') as HTMLElement;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitmaps",
3
- "version": "1.1.10",
3
+ "version": "1.1.12",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "gitmaps": "cli.ts"