gitmaps 1.1.19 → 1.1.21
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 +19 -3
- package/app/layout.tsx +10 -4
- package/app/lib/events.tsx +4 -2
- package/app/lib/low-zoom-preview.test.ts +13 -1
- package/app/lib/low-zoom-preview.ts +106 -53
- package/package.json +1 -1
package/app/analytics.db
CHANGED
|
Binary file
|
package/app/globals.css
CHANGED
|
@@ -1145,17 +1145,24 @@ body {
|
|
|
1145
1145
|
|
|
1146
1146
|
/* ── Sticky Zoom Controls ── */
|
|
1147
1147
|
body.repo-loading .sticky-zoom-pill,
|
|
1148
|
-
body.repo-loading .
|
|
1148
|
+
body.repo-loading .floating-top-controls,
|
|
1149
1149
|
body.landing-placeholder-visible .sticky-zoom-pill,
|
|
1150
|
-
body.landing-placeholder-visible .
|
|
1150
|
+
body.landing-placeholder-visible .floating-top-controls {
|
|
1151
1151
|
display: none;
|
|
1152
1152
|
}
|
|
1153
1153
|
|
|
1154
|
-
.
|
|
1154
|
+
.floating-top-controls {
|
|
1155
1155
|
position: fixed;
|
|
1156
1156
|
top: 12px;
|
|
1157
1157
|
right: 144px;
|
|
1158
1158
|
z-index: 10001;
|
|
1159
|
+
display: flex;
|
|
1160
|
+
align-items: center;
|
|
1161
|
+
gap: 8px;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
.detail-mode-switch {
|
|
1165
|
+
position: relative;
|
|
1159
1166
|
}
|
|
1160
1167
|
|
|
1161
1168
|
.detail-mode-btn {
|
|
@@ -1192,6 +1199,15 @@ body.landing-placeholder-visible .detail-mode-switch {
|
|
|
1192
1199
|
box-shadow: 0 10px 28px rgba(59, 130, 246, 0.16);
|
|
1193
1200
|
}
|
|
1194
1201
|
|
|
1202
|
+
.detail-mode-btn--secondary {
|
|
1203
|
+
border-color: rgba(148, 163, 184, 0.28);
|
|
1204
|
+
color: #e2e8f0;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
.detail-mode-btn--secondary:hover {
|
|
1208
|
+
border-color: rgba(148, 163, 184, 0.48);
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1195
1211
|
.detail-mode-label {
|
|
1196
1212
|
opacity: 0.72;
|
|
1197
1213
|
text-transform: uppercase;
|
package/app/layout.tsx
CHANGED
|
@@ -875,10 +875,16 @@ export default function RootLayout({ children }: { children: any }) {
|
|
|
875
875
|
</div>
|
|
876
876
|
|
|
877
877
|
{/* Sticky Zoom Controls — floating pill, bottom-right */}
|
|
878
|
-
<div
|
|
879
|
-
<
|
|
880
|
-
<
|
|
881
|
-
|
|
878
|
+
<div className="floating-top-controls">
|
|
879
|
+
<div id="detailModeSwitch" className="detail-mode-switch" title="Toggle preview-focused detail mode">
|
|
880
|
+
<button id="toggleDetailMode" type="button" className="detail-mode-btn">
|
|
881
|
+
<span className="detail-mode-label">Renderer</span>
|
|
882
|
+
<span id="detailModeState" className="detail-mode-state">Preview</span>
|
|
883
|
+
</button>
|
|
884
|
+
</div>
|
|
885
|
+
<button id="openSettingsFloating" type="button" className="detail-mode-btn detail-mode-btn--secondary" title="Open settings">
|
|
886
|
+
<span className="detail-mode-label">Settings</span>
|
|
887
|
+
<span className="detail-mode-state">Tune</span>
|
|
882
888
|
</button>
|
|
883
889
|
</div>
|
|
884
890
|
|
package/app/lib/events.tsx
CHANGED
|
@@ -740,9 +740,11 @@ export function setupEventListeners(ctx: CanvasContext) {
|
|
|
740
740
|
});
|
|
741
741
|
|
|
742
742
|
// Settings modal
|
|
743
|
-
|
|
743
|
+
const openSettings = () => {
|
|
744
744
|
import('./settings-modal').then(({ openSettingsModal }) => openSettingsModal(ctx));
|
|
745
|
-
}
|
|
745
|
+
};
|
|
746
|
+
document.getElementById('openSettings')?.addEventListener('click', openSettings);
|
|
747
|
+
document.getElementById('openSettingsFloating')?.addEventListener('click', openSettings);
|
|
746
748
|
|
|
747
749
|
// Global search
|
|
748
750
|
document.getElementById('openGlobalSearch')?.addEventListener('click', () => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
import { collectPreviewDiffMarkers, estimatePreviewCharsPerLine, estimatePreviewLineCapacity, estimatePreviewMaxScroll, estimateTitleCharsPerLine, getLowZoomPreviewText, getLowZoomScale, getPreviewScrollMetrics, wrapPreviewText } from './low-zoom-preview';
|
|
2
|
+
import { collectPreviewDiffMarkers, estimatePreviewCharsPerLine, estimatePreviewLineCapacity, estimatePreviewMaxScroll, estimateTitleCharsPerLine, getLowZoomPreviewText, getLowZoomScale, getPreviewRenderableLines, getPreviewScrollMetrics, 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', () => {
|
|
@@ -21,6 +21,18 @@ describe('low zoom preview helpers', () => {
|
|
|
21
21
|
expect(getLowZoomPreviewText({ path: 'bin.dat', ext: 'dat', isBinary: true, content: 'abc' }, 0)).toBe('');
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
+
test('preview renderable lines preserve added and deleted diff context', () => {
|
|
25
|
+
const lines = getPreviewRenderableLines({
|
|
26
|
+
path: 'src/example.ts',
|
|
27
|
+
ext: 'ts',
|
|
28
|
+
content: ['one', 'two', 'three'].join('\n'),
|
|
29
|
+
addedLines: new Set([2]),
|
|
30
|
+
deletedBeforeLine: new Map([[2, ['old-two']]]),
|
|
31
|
+
}, 0);
|
|
32
|
+
expect(lines.some((line) => line.tone === 'deleted' && line.text.includes('old-two'))).toBe(true);
|
|
33
|
+
expect(lines.some((line) => line.tone === 'added' && line.text === 'two')).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
24
36
|
test('keeps zoomed-out on-screen text smaller than zoomed-in text', () => {
|
|
25
37
|
const far = getLowZoomScale(0.1);
|
|
26
38
|
const near = getLowZoomScale(1);
|
|
@@ -25,17 +25,81 @@ export function getLowZoomScale(zoom: number) {
|
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
type PreviewRenderableLine = {
|
|
29
|
+
text: string;
|
|
30
|
+
tone: 'normal' | 'added' | 'deleted';
|
|
31
|
+
sourceLine?: number;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function getPreviewDiffMaps(file: any) {
|
|
35
|
+
const addedLines = new Set<number>(file?.addedLines instanceof Set ? Array.from(file.addedLines) : []);
|
|
36
|
+
const deletedBeforeLine = new Map<number, string[]>(file?.deletedBeforeLine instanceof Map ? Array.from(file.deletedBeforeLine.entries()) : []);
|
|
37
|
+
|
|
38
|
+
if ((addedLines.size === 0 && deletedBeforeLine.size === 0) && Array.isArray(file?.hunks)) {
|
|
39
|
+
for (const hunk of file.hunks) {
|
|
40
|
+
let newLine = hunk?.newStart || 1;
|
|
41
|
+
let pendingDeleted: string[] = [];
|
|
42
|
+
for (const line of hunk?.lines || []) {
|
|
43
|
+
if (line?.type === 'add') {
|
|
44
|
+
addedLines.add(newLine);
|
|
45
|
+
if (pendingDeleted.length > 0) {
|
|
46
|
+
const existing = deletedBeforeLine.get(newLine) || [];
|
|
47
|
+
deletedBeforeLine.set(newLine, existing.concat(pendingDeleted));
|
|
48
|
+
pendingDeleted = [];
|
|
49
|
+
}
|
|
50
|
+
newLine += 1;
|
|
51
|
+
} else if (line?.type === 'del') {
|
|
52
|
+
pendingDeleted.push(String(line?.content || ''));
|
|
53
|
+
} else {
|
|
54
|
+
if (pendingDeleted.length > 0) {
|
|
55
|
+
const existing = deletedBeforeLine.get(newLine) || [];
|
|
56
|
+
deletedBeforeLine.set(newLine, existing.concat(pendingDeleted));
|
|
57
|
+
pendingDeleted = [];
|
|
58
|
+
}
|
|
59
|
+
newLine += 1;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (pendingDeleted.length > 0) {
|
|
63
|
+
const existing = deletedBeforeLine.get(newLine) || [];
|
|
64
|
+
deletedBeforeLine.set(newLine, existing.concat(pendingDeleted));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { addedLines, deletedBeforeLine };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function getPreviewRenderableLines(file: any, scrollTop: number): PreviewRenderableLine[] {
|
|
73
|
+
if (!file || file.isBinary || !file.content) return [];
|
|
30
74
|
|
|
31
75
|
const ext = (file.ext || file.path?.split('.').pop() || '').toLowerCase();
|
|
32
|
-
if (!PREVIEWABLE_EXTS.has(ext)) return
|
|
76
|
+
if (!PREVIEWABLE_EXTS.has(ext)) return [];
|
|
33
77
|
|
|
34
78
|
const normalized = String(file.content).replace(/\t/g, ' ');
|
|
35
79
|
const lines = normalized.split('\n');
|
|
36
80
|
const approxLineHeight = 20;
|
|
37
81
|
const startLine = Math.max(0, Math.floor(scrollTop / approxLineHeight));
|
|
38
|
-
|
|
82
|
+
const { addedLines, deletedBeforeLine } = getPreviewDiffMaps(file);
|
|
83
|
+
const out: PreviewRenderableLine[] = [];
|
|
84
|
+
|
|
85
|
+
for (let i = startLine; i < lines.length; i += 1) {
|
|
86
|
+
const lineNum = i + 1;
|
|
87
|
+
const deleted = deletedBeforeLine.get(lineNum) || [];
|
|
88
|
+
for (const deletedLine of deleted) {
|
|
89
|
+
out.push({ text: `- ${String(deletedLine).replace(/\t/g, ' ')}`, tone: 'deleted', sourceLine: lineNum });
|
|
90
|
+
}
|
|
91
|
+
out.push({
|
|
92
|
+
text: lines[i],
|
|
93
|
+
tone: file?.status === 'added' || addedLines.has(lineNum) ? 'added' : file?.status === 'deleted' ? 'deleted' : 'normal',
|
|
94
|
+
sourceLine: lineNum,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function getLowZoomPreviewText(file: any, scrollTop: number): string {
|
|
102
|
+
return getPreviewRenderableLines(file, scrollTop).map((line) => line.text).join('\n').trim();
|
|
39
103
|
}
|
|
40
104
|
|
|
41
105
|
export function wrapPreviewText(text: string, maxCharsPerLine: number, maxLines: number): string[] {
|
|
@@ -137,8 +201,7 @@ export function getPreviewScrollMetrics(file: any, height: number, zoom: number,
|
|
|
137
201
|
export function collectPreviewDiffMarkers(file: any, totalLines: number) {
|
|
138
202
|
const markers: Array<{ ratio: number; color: string; height?: number }> = [];
|
|
139
203
|
const safeTotal = Math.max(1, totalLines);
|
|
140
|
-
|
|
141
|
-
let deletedBefore = file?.deletedBeforeLine instanceof Map ? Array.from(file.deletedBeforeLine.keys()) : [];
|
|
204
|
+
const { addedLines, deletedBeforeLine } = getPreviewDiffMaps(file);
|
|
142
205
|
|
|
143
206
|
if (file?.status === 'added') {
|
|
144
207
|
markers.push({ ratio: 0, color: '#22c55e', height: 1 });
|
|
@@ -149,42 +212,10 @@ export function collectPreviewDiffMarkers(file: any, totalLines: number) {
|
|
|
149
212
|
return markers;
|
|
150
213
|
}
|
|
151
214
|
|
|
152
|
-
|
|
153
|
-
const derivedAdded: number[] = [];
|
|
154
|
-
const derivedDeleted: number[] = [];
|
|
155
|
-
for (const hunk of file.hunks) {
|
|
156
|
-
let newLine = hunk?.newStart || 1;
|
|
157
|
-
let sawPendingDelete = false;
|
|
158
|
-
for (const line of hunk?.lines || []) {
|
|
159
|
-
if (line?.type === 'add') {
|
|
160
|
-
derivedAdded.push(newLine);
|
|
161
|
-
if (sawPendingDelete) {
|
|
162
|
-
derivedDeleted.push(newLine);
|
|
163
|
-
sawPendingDelete = false;
|
|
164
|
-
}
|
|
165
|
-
newLine += 1;
|
|
166
|
-
} else if (line?.type === 'del') {
|
|
167
|
-
sawPendingDelete = true;
|
|
168
|
-
} else {
|
|
169
|
-
if (sawPendingDelete) {
|
|
170
|
-
derivedDeleted.push(newLine);
|
|
171
|
-
sawPendingDelete = false;
|
|
172
|
-
}
|
|
173
|
-
newLine += 1;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
if (sawPendingDelete) {
|
|
177
|
-
derivedDeleted.push(newLine);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
added = derivedAdded;
|
|
181
|
-
deletedBefore = derivedDeleted;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
for (const line of added) {
|
|
215
|
+
for (const line of addedLines) {
|
|
185
216
|
markers.push({ ratio: Math.max(0, Math.min(1, (line - 1) / safeTotal)), color: '#22c55e' });
|
|
186
217
|
}
|
|
187
|
-
for (const line of
|
|
218
|
+
for (const line of deletedBeforeLine.keys()) {
|
|
188
219
|
markers.push({ ratio: Math.max(0, Math.min(1, (line - 1) / safeTotal)), color: '#ef4444' });
|
|
189
220
|
}
|
|
190
221
|
return markers;
|
|
@@ -255,28 +286,50 @@ export function renderLowZoomPreviewCanvas(
|
|
|
255
286
|
ctx.fillText(trimToWidth(ctx, subtitle, maxTextWidth), leftInset, subtitleY);
|
|
256
287
|
|
|
257
288
|
const previewY = subtitleY + subtitleFont + scale.gap;
|
|
258
|
-
const
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
)
|
|
289
|
+
const previewLines = getPreviewRenderableLines(file, scrollTop);
|
|
290
|
+
const maxCharsPerLine = estimatePreviewCharsPerLine(width, zoom);
|
|
291
|
+
const maxVisibleLines = estimatePreviewLineCapacity(height, zoom);
|
|
292
|
+
const wrapped: Array<{ text: string; tone: 'normal' | 'added' | 'deleted' }> = [];
|
|
293
|
+
|
|
294
|
+
for (const line of previewLines) {
|
|
295
|
+
const parts = wrapPreviewText(line.text, maxCharsPerLine, Math.max(1, maxVisibleLines - wrapped.length));
|
|
296
|
+
for (const part of parts) {
|
|
297
|
+
wrapped.push({ text: part, tone: line.tone });
|
|
298
|
+
if (wrapped.length >= maxVisibleLines) break;
|
|
299
|
+
}
|
|
300
|
+
if (wrapped.length >= maxVisibleLines) break;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (wrapped.length === 0) {
|
|
304
|
+
wrapped.push({ text: 'Preview unavailable', tone: 'normal' });
|
|
305
|
+
}
|
|
264
306
|
|
|
265
307
|
ctx.font = `${scale.bodyFont}px "JetBrains Mono", monospace`;
|
|
266
|
-
ctx.fillStyle = 'rgba(226,232,240,0.92)';
|
|
267
308
|
|
|
268
309
|
const fadeStart = Math.max(previewY, height - scale.bodyLineHeight * 2.2);
|
|
269
310
|
const bodyHeight = Math.max(scale.bodyLineHeight * 2, height - previewY - scale.padding);
|
|
270
|
-
const
|
|
271
|
-
mask.addColorStop(0, 'rgba(226,232,240,0.92)');
|
|
272
|
-
mask.addColorStop(Math.max(0, (fadeStart - previewY) / Math.max(1, bodyHeight)), 'rgba(226,232,240,0.92)');
|
|
273
|
-
mask.addColorStop(1, 'rgba(226,232,240,0)');
|
|
274
|
-
ctx.fillStyle = mask;
|
|
311
|
+
const fadeRatio = Math.max(0, (fadeStart - previewY) / Math.max(1, bodyHeight));
|
|
275
312
|
|
|
276
313
|
wrapped.forEach((line, index) => {
|
|
277
314
|
const y = previewY + index * scale.bodyLineHeight;
|
|
278
315
|
if (y > height - scale.padding) return;
|
|
279
|
-
|
|
316
|
+
|
|
317
|
+
const t = Math.max(0, Math.min(1, (y - previewY) / Math.max(1, bodyHeight)));
|
|
318
|
+
const alpha = t <= fadeRatio ? 1 : Math.max(0, 1 - ((t - fadeRatio) / Math.max(0.0001, 1 - fadeRatio)));
|
|
319
|
+
|
|
320
|
+
if (line.tone === 'added') {
|
|
321
|
+
ctx.fillStyle = `rgba(34,197,94,${0.12 * alpha})`;
|
|
322
|
+
ctx.fillRect(leftInset - 4, y, maxTextWidth + 4, scale.bodyLineHeight - 1);
|
|
323
|
+
ctx.fillStyle = `rgba(134,239,172,${0.98 * alpha})`;
|
|
324
|
+
} else if (line.tone === 'deleted') {
|
|
325
|
+
ctx.fillStyle = `rgba(239,68,68,${0.1 * alpha})`;
|
|
326
|
+
ctx.fillRect(leftInset - 4, y, maxTextWidth + 4, scale.bodyLineHeight - 1);
|
|
327
|
+
ctx.fillStyle = `rgba(252,165,165,${0.96 * alpha})`;
|
|
328
|
+
} else {
|
|
329
|
+
ctx.fillStyle = `rgba(226,232,240,${0.92 * alpha})`;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
ctx.fillText(trimToWidth(ctx, line.text, maxTextWidth), leftInset, y);
|
|
280
333
|
});
|
|
281
334
|
|
|
282
335
|
}
|