gitmaps 1.1.6 → 1.1.7
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 +0 -0
- package/app/globals.css +55 -1
- package/app/layout.tsx +7 -0
- package/app/lib/events.tsx +23 -1
- package/app/lib/low-zoom-preview.test.ts +8 -1
- package/app/lib/low-zoom-preview.ts +23 -11
- package/app/lib/viewport-culling.ts +31 -1
- package/package.json +3 -1
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.
|
|
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
|
package/app/lib/events.tsx
CHANGED
|
@@ -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 =
|
|
9
|
-
const desiredScreenBody =
|
|
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: (
|
|
15
|
-
gap: (
|
|
16
|
-
radius:
|
|
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.
|
|
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
|
-
|
|
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.
|
|
144
|
-
ctx.font = `${Math.max(scale.bodyFont * 0.
|
|
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
|
|
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.
|
|
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,14 @@ 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
|
+
return stored === 'preview' ? 'preview' : 'auto';
|
|
60
|
+
} catch {
|
|
61
|
+
return 'auto';
|
|
62
|
+
}
|
|
63
|
+
})();
|
|
55
64
|
|
|
56
65
|
// Track pill elements for cleanup
|
|
57
66
|
const pillCards = new Map<string, HTMLElement>();
|
|
@@ -90,6 +99,27 @@ export function getPinnedCards(): Set<string> {
|
|
|
90
99
|
return _pinnedCards;
|
|
91
100
|
}
|
|
92
101
|
|
|
102
|
+
export function getDetailMode(): 'auto' | 'preview' {
|
|
103
|
+
return _detailMode;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function setDetailMode(mode: 'auto' | 'preview') {
|
|
107
|
+
_detailMode = mode;
|
|
108
|
+
try {
|
|
109
|
+
localStorage.setItem(LOW_ZOOM_MODE_STORAGE_KEY, mode);
|
|
110
|
+
} catch { }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function toggleDetailMode(): 'auto' | 'preview' {
|
|
114
|
+
const next = _detailMode === 'preview' ? 'auto' : 'preview';
|
|
115
|
+
setDetailMode(next);
|
|
116
|
+
return next;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function isPreviewModeForced() {
|
|
120
|
+
return _detailMode === 'preview';
|
|
121
|
+
}
|
|
122
|
+
|
|
93
123
|
// ── Status colors for low-zoom cards
|
|
94
124
|
const PILL_COLORS: Record<string, string> = {
|
|
95
125
|
'ts': '#3178c6',
|
|
@@ -293,7 +323,7 @@ export function performViewportCulling(ctx: CanvasContext) {
|
|
|
293
323
|
// Phase 4c: also materialize deferred CardManager cards
|
|
294
324
|
// Reuse zoom from worldRect (already snapped) — avoids redundant ctx.snap()
|
|
295
325
|
const zoom = worldRect.zoom;
|
|
296
|
-
const isLowZoom = zoom <= LOD_ZOOM_THRESHOLD;
|
|
326
|
+
const isLowZoom = _detailMode === 'preview' || zoom <= LOD_ZOOM_THRESHOLD;
|
|
297
327
|
|
|
298
328
|
// Important: never materialize full cards while in low-zoom pill mode.
|
|
299
329
|
// 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.
|
|
3
|
+
"version": "1.1.7",
|
|
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": [
|