gitmaps 1.1.6 → 1.1.8

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/globals.css CHANGED
@@ -1145,10 +1145,64 @@ body {
1145
1145
 
1146
1146
  /* ── Sticky Zoom Controls ── */
1147
1147
  body.repo-loading .sticky-zoom-pill,
1148
- body.landing-placeholder-visible .sticky-zoom-pill {
1148
+ body.repo-loading .detail-mode-switch,
1149
+ body.landing-placeholder-visible .sticky-zoom-pill,
1150
+ body.landing-placeholder-visible .detail-mode-switch {
1149
1151
  display: none;
1150
1152
  }
1151
1153
 
1154
+ .detail-mode-switch {
1155
+ position: fixed;
1156
+ top: 12px;
1157
+ right: 144px;
1158
+ z-index: 10001;
1159
+ }
1160
+
1161
+ .detail-mode-btn {
1162
+ display: inline-flex;
1163
+ align-items: center;
1164
+ gap: 8px;
1165
+ padding: 6px 10px;
1166
+ background: rgba(15, 23, 42, 0.88);
1167
+ border: 1px solid rgba(124, 58, 237, 0.24);
1168
+ border-radius: 10px;
1169
+ color: #d8ccff;
1170
+ font-family: 'JetBrains Mono', monospace;
1171
+ font-size: 10px;
1172
+ cursor: pointer;
1173
+ backdrop-filter: blur(8px);
1174
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
1175
+ transition:
1176
+ background 0.2s ease,
1177
+ border-color 0.2s ease,
1178
+ transform 0.12s ease;
1179
+ }
1180
+
1181
+ .detail-mode-btn:hover {
1182
+ background: rgba(15, 23, 42, 0.96);
1183
+ border-color: rgba(124, 58, 237, 0.45);
1184
+ }
1185
+
1186
+ .detail-mode-btn:active {
1187
+ transform: scale(0.98);
1188
+ }
1189
+
1190
+ .detail-mode-btn.active {
1191
+ border-color: rgba(96, 165, 250, 0.6);
1192
+ box-shadow: 0 10px 28px rgba(59, 130, 246, 0.16);
1193
+ }
1194
+
1195
+ .detail-mode-label {
1196
+ opacity: 0.72;
1197
+ text-transform: uppercase;
1198
+ letter-spacing: 0.06em;
1199
+ }
1200
+
1201
+ .detail-mode-state {
1202
+ color: #f8fafc;
1203
+ font-weight: 700;
1204
+ }
1205
+
1152
1206
  .sticky-zoom-pill {
1153
1207
  position: absolute;
1154
1208
  bottom: 16px;
package/app/layout.tsx CHANGED
@@ -875,6 +875,13 @@ export default function RootLayout({ children }: { children: any }) {
875
875
  </div>
876
876
 
877
877
  {/* Sticky Zoom Controls — floating pill, bottom-right */}
878
+ <div id="detailModeSwitch" className="detail-mode-switch" title="Toggle preview-focused detail mode">
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>
882
+ </button>
883
+ </div>
884
+
878
885
  <div id="stickyZoomControls" className="sticky-zoom-pill">
879
886
  <button id="stickyZoomOut" className="sz-btn" title="Zoom out">
880
887
  <svg
@@ -27,7 +27,7 @@ import { createLayer, getActiveLayer, addSectionToLayer } from './layers';
27
27
  import { updateCanvasTransform, updateZoomUI, updateMinimap, fitAllFiles, setupMinimapClick } from './canvas';
28
28
  import { zoomTowardScreen, panByDelta, screenToWorld, getCardManager } from './xydraw-bridge';
29
29
  import { hideSelectedFiles, showHiddenFilesModal as showHiddenModal } from './hidden-files';
30
- import { updatePillSelectionHighlights } from './viewport-culling';
30
+ import { getDetailMode, toggleDetailMode, updatePillSelectionHighlights } from './viewport-culling';
31
31
  import { clearSelectionHighlights, updateSelectionHighlights, updateArrangeToolbar, arrangeRow, arrangeColumn, arrangeGrid, toggleCardExpand, fitScreenSize, changeCardsFontSize } from './cards';
32
32
  import { loadRepository, rerenderCurrentView, selectCommit } from './repo';
33
33
  import { handoffRepoLoad, syncRepoSelection } from './repo-handoff';
@@ -451,6 +451,28 @@ export function setupEventListeners(ctx: CanvasContext) {
451
451
  });
452
452
  }
453
453
 
454
+ const detailModeToggle = document.getElementById('toggleDetailMode');
455
+ if (detailModeToggle) {
456
+ const stateEl = document.getElementById('detailModeState');
457
+ const updateDetailModeUi = () => {
458
+ const mode = getDetailMode();
459
+ detailModeToggle.classList.toggle('active', mode === 'preview');
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';
464
+ };
465
+ updateDetailModeUi();
466
+ detailModeToggle.addEventListener('click', () => {
467
+ const next = toggleDetailMode();
468
+ updateDetailModeUi();
469
+ rerenderCurrentView(ctx);
470
+ showToast(next === 'preview'
471
+ ? 'Preview mode forced on'
472
+ : 'Auto detail switching restored', 'info');
473
+ });
474
+ }
475
+
454
476
  // Control mode toggle (Simple vs Advanced)
455
477
  const modeToggle = document.getElementById('toggleControlMode');
456
478
  if (modeToggle) {
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from 'bun:test';
2
- import { estimatePreviewCharsPerLine, estimatePreviewLineCapacity, getLowZoomPreviewText, getLowZoomScale, wrapPreviewText } from './low-zoom-preview';
2
+ import { estimatePreviewCharsPerLine, estimatePreviewLineCapacity, 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', () => {
@@ -33,6 +33,13 @@ describe('low zoom preview helpers', () => {
33
33
 
34
34
  test('preview capacity estimates stay positive', () => {
35
35
  expect(estimatePreviewCharsPerLine(580, 0.25)).toBeGreaterThan(8);
36
+ expect(estimateTitleCharsPerLine(580, 0.25)).toBeGreaterThan(8);
36
37
  expect(estimatePreviewLineCapacity(700, 0.25)).toBeGreaterThanOrEqual(2);
37
38
  });
39
+
40
+ test('title typography is larger than body typography for readability', () => {
41
+ const scale = getLowZoomScale(0.18);
42
+ expect(scale.titleFont).toBeGreaterThan(scale.bodyFont);
43
+ expect(scale.titleLineHeight).toBeGreaterThan(scale.bodyLineHeight * 0.7);
44
+ });
38
45
  });
@@ -5,15 +5,16 @@ const PREVIEWABLE_EXTS = new Set([
5
5
  export function getLowZoomScale(zoom: number) {
6
6
  const clampedZoom = Math.max(0.08, Math.min(0.25, zoom));
7
7
  const progress = (0.25 - clampedZoom) / (0.25 - 0.08);
8
- const desiredScreenTitle = 10 + progress * 4;
9
- const desiredScreenBody = 8 + progress * 4;
8
+ const desiredScreenTitle = 14 + progress * 6;
9
+ const desiredScreenBody = 9 + progress * 4;
10
10
  return {
11
11
  titleFont: desiredScreenTitle / clampedZoom,
12
+ titleLineHeight: (desiredScreenTitle * 1.08) / clampedZoom,
12
13
  bodyFont: desiredScreenBody / clampedZoom,
13
14
  bodyLineHeight: (desiredScreenBody * 1.45) / clampedZoom,
14
- padding: (10 + progress * 4) / clampedZoom,
15
- gap: (6 + progress * 3) / clampedZoom,
16
- radius: 8 / clampedZoom,
15
+ padding: (12 + progress * 6) / clampedZoom,
16
+ gap: (7 + progress * 3) / clampedZoom,
17
+ radius: 10 / clampedZoom,
17
18
  };
18
19
  }
19
20
 
@@ -82,10 +83,17 @@ function ellipsizeWrappedLines(lines: string[], maxLines: number) {
82
83
 
83
84
  export function estimatePreviewLineCapacity(height: number, zoom: number): number {
84
85
  const scale = getLowZoomScale(zoom);
85
- const available = Math.max(scale.bodyLineHeight, height - scale.padding * 2 - scale.titleFont - scale.bodyFont - scale.gap * 3);
86
+ const available = Math.max(scale.bodyLineHeight, height - scale.padding * 2 - scale.titleLineHeight * 2 - scale.bodyFont - scale.gap * 4);
86
87
  return Math.max(2, Math.floor(available / scale.bodyLineHeight));
87
88
  }
88
89
 
90
+ export function estimateTitleCharsPerLine(width: number, zoom: number): number {
91
+ const scale = getLowZoomScale(zoom);
92
+ const available = Math.max(80, width - scale.padding * 2 - Math.max(14, width * 0.02));
93
+ const avgCharWidth = Math.max(7, scale.titleFont * 0.58);
94
+ return Math.max(8, Math.floor(available / avgCharWidth));
95
+ }
96
+
89
97
  export function estimatePreviewCharsPerLine(width: number, zoom: number): number {
90
98
  const scale = getLowZoomScale(zoom);
91
99
  const available = Math.max(60, width - scale.padding * 2 - Math.max(14, width * 0.02));
@@ -138,15 +146,19 @@ export function renderLowZoomPreviewCanvas(
138
146
  ctx.font = `700 ${scale.titleFont}px "JetBrains Mono", monospace`;
139
147
  ctx.fillStyle = '#f8fafc';
140
148
  const title = path.split('/').pop() || path;
141
- ctx.fillText(trimToWidth(ctx, title, maxTextWidth), leftInset, topInset);
149
+ const titleLines = wrapPreviewText(title, estimateTitleCharsPerLine(width, zoom), 2);
150
+ titleLines.forEach((line, index) => {
151
+ ctx.fillText(trimToWidth(ctx, line, maxTextWidth), leftInset, topInset + index * scale.titleLineHeight);
152
+ });
142
153
 
143
- const subtitleY = topInset + scale.titleFont + scale.gap;
144
- ctx.font = `${Math.max(scale.bodyFont * 0.78, 8 / Math.max(zoom, 0.08))}px "JetBrains Mono", monospace`;
154
+ const subtitleY = topInset + titleLines.length * scale.titleLineHeight + scale.gap * 0.8;
155
+ ctx.font = `${Math.max(scale.bodyFont * 0.8, 9 / Math.max(zoom, 0.08))}px "JetBrains Mono", monospace`;
145
156
  ctx.fillStyle = 'rgba(226,232,240,0.72)';
146
- const subtitle = path.includes('/') ? path.split('/').slice(0, -1).join('/') : 'root';
157
+ const pathParts = path.split('/');
158
+ const subtitle = pathParts.length > 1 ? pathParts.slice(Math.max(0, pathParts.length - 3), -1).join(' / ') : 'root';
147
159
  ctx.fillText(trimToWidth(ctx, subtitle, maxTextWidth), leftInset, subtitleY);
148
160
 
149
- const previewY = subtitleY + Math.max(scale.bodyFont * 0.78, 8 / Math.max(zoom, 0.08)) + scale.gap * 1.5;
161
+ const previewY = subtitleY + Math.max(scale.bodyFont * 0.8, 9 / Math.max(zoom, 0.08)) + scale.gap * 1.35;
150
162
  const rawPreview = getLowZoomPreviewText(file, scrollTop) || 'Preview unavailable';
151
163
  const wrapped = wrapPreviewText(
152
164
  rawPreview,
@@ -36,6 +36,7 @@ const VIEWPORT_MARGIN = 500;
36
36
 
37
37
  // LOD threshold: below this zoom level, use lightweight pill placeholders
38
38
  const LOD_ZOOM_THRESHOLD = 0.25;
39
+ const LOW_ZOOM_MODE_STORAGE_KEY = 'gitmaps:detailMode';
39
40
 
40
41
  // Maximum deferred cards to materialize per animation frame
41
42
  // Prevents frame drops when zooming out then back in on huge repos
@@ -52,6 +53,15 @@ export function markTransformActive() {
52
53
 
53
54
  // Track current LOD mode so we can detect transitions
54
55
  let _currentLodMode: 'full' | 'pill' = 'full';
56
+ let _detailMode: 'auto' | 'preview' = (() => {
57
+ try {
58
+ const stored = localStorage.getItem(LOW_ZOOM_MODE_STORAGE_KEY);
59
+ if (stored === 'auto' || stored === 'preview') return stored;
60
+ return 'preview';
61
+ } catch {
62
+ return 'preview';
63
+ }
64
+ })();
55
65
 
56
66
  // Track pill elements for cleanup
57
67
  const pillCards = new Map<string, HTMLElement>();
@@ -90,6 +100,27 @@ export function getPinnedCards(): Set<string> {
90
100
  return _pinnedCards;
91
101
  }
92
102
 
103
+ export function getDetailMode(): 'auto' | 'preview' {
104
+ return _detailMode;
105
+ }
106
+
107
+ export function setDetailMode(mode: 'auto' | 'preview') {
108
+ _detailMode = mode;
109
+ try {
110
+ localStorage.setItem(LOW_ZOOM_MODE_STORAGE_KEY, mode);
111
+ } catch { }
112
+ }
113
+
114
+ export function toggleDetailMode(): 'auto' | 'preview' {
115
+ const next = _detailMode === 'preview' ? 'auto' : 'preview';
116
+ setDetailMode(next);
117
+ return next;
118
+ }
119
+
120
+ export function isPreviewModeForced() {
121
+ return _detailMode === 'preview';
122
+ }
123
+
93
124
  // ── Status colors for low-zoom cards
94
125
  const PILL_COLORS: Record<string, string> = {
95
126
  'ts': '#3178c6',
@@ -293,7 +324,7 @@ export function performViewportCulling(ctx: CanvasContext) {
293
324
  // Phase 4c: also materialize deferred CardManager cards
294
325
  // Reuse zoom from worldRect (already snapped) — avoids redundant ctx.snap()
295
326
  const zoom = worldRect.zoom;
296
- const isLowZoom = zoom <= LOD_ZOOM_THRESHOLD;
327
+ const isLowZoom = _detailMode === 'preview' || zoom <= LOD_ZOOM_THRESHOLD;
297
328
 
298
329
  // Important: never materialize full cards while in low-zoom pill mode.
299
330
  // Otherwise CardManager keeps mounting heavyweight cards right when the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitmaps",
3
- "version": "1.1.6",
3
+ "version": "1.1.8",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "gitmaps": "cli.ts"
@@ -20,10 +20,12 @@
20
20
  "smoke:browser": "bun scripts/browser-smoke-local.ts",
21
21
  "smoke:browser-tools": "bash scripts/browser-smoke-local.sh",
22
22
  "smoke:browser-tools:load": "bash scripts/browser-repo-load-smoke.sh",
23
+ "smoke:browser-tools:low-zoom": "bash scripts/browser-low-zoom-perf-smoke.sh",
23
24
  "smoke:browser-tools:guard": "bash scripts/browser-smoke-guard.sh",
24
25
  "smoke:browser-tools:self-check": "bash scripts/browser-smoke-self-check.sh",
25
26
  "smoke:browser-tools:check": "bash scripts/browser-smoke-check.sh",
26
27
  "smoke:docker-image": "bash scripts/docker-image-smoke.sh",
28
+ "bench:low-zoom": "bun scripts/low-zoom-preview-bench.ts",
27
29
  "prepublishOnly": "echo 'Publishing gitmaps to npm'"
28
30
  },
29
31
  "keywords": [