gitmaps 1.1.3 → 1.1.5
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/api/repo/pdf-meta/route.ts +68 -0
- package/app/lib/cards.tsx +115 -7
- package/app/lib/low-zoom-preview.test.ts +27 -0
- package/app/lib/low-zoom-preview.ts +31 -0
- package/app/lib/viewport-culling.ts +113 -37
- package/package.json +3 -2
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { validateRepoPath } from '../validate-path';
|
|
4
|
+
|
|
5
|
+
function parsePdfInfoPageCount(output: string): number | null {
|
|
6
|
+
const match = output.match(/^Pages:\s+(\d+)/mi);
|
|
7
|
+
if (!match) return null;
|
|
8
|
+
const value = parseInt(match[1], 10);
|
|
9
|
+
return Number.isFinite(value) && value > 0 ? value : null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function GET(req: Request) {
|
|
13
|
+
const url = new URL(req.url);
|
|
14
|
+
const repoPath = url.searchParams.get('path') || '';
|
|
15
|
+
const filePath = url.searchParams.get('file') || '';
|
|
16
|
+
|
|
17
|
+
if (!repoPath || !filePath) {
|
|
18
|
+
return Response.json({ error: 'path and file params required' }, { status: 400 });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const blocked = validateRepoPath(repoPath);
|
|
22
|
+
if (blocked) return blocked;
|
|
23
|
+
|
|
24
|
+
if (filePath.includes('..') || filePath.startsWith('/')) {
|
|
25
|
+
return Response.json({ error: 'Invalid file path' }, { status: 400 });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const fullPath = path.join(repoPath, filePath);
|
|
29
|
+
if (!existsSync(fullPath)) {
|
|
30
|
+
return Response.json({ error: 'File not found' }, { status: 404 });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const info = Bun.spawnSync(['pdfinfo', fullPath], {
|
|
35
|
+
stdout: 'pipe',
|
|
36
|
+
stderr: 'pipe',
|
|
37
|
+
timeout: 15000,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (info.exitCode === 0) {
|
|
41
|
+
const pageCount = parsePdfInfoPageCount(new TextDecoder().decode(info.stdout));
|
|
42
|
+
return Response.json({ pageCount });
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
// pdfinfo unavailable
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const identify = Bun.spawnSync(['magick', 'identify', fullPath], {
|
|
50
|
+
stdout: 'pipe',
|
|
51
|
+
stderr: 'pipe',
|
|
52
|
+
timeout: 20000,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (identify.exitCode === 0) {
|
|
56
|
+
const lines = new TextDecoder().decode(identify.stdout)
|
|
57
|
+
.split(/\r?\n/)
|
|
58
|
+
.map((line) => line.trim())
|
|
59
|
+
.filter(Boolean);
|
|
60
|
+
const pageCount = lines.length > 0 ? lines.length : null;
|
|
61
|
+
return Response.json({ pageCount });
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
// magick unavailable
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return Response.json({ pageCount: null });
|
|
68
|
+
}
|
package/app/lib/cards.tsx
CHANGED
|
@@ -965,6 +965,116 @@ export function _buildFileContentHTML(
|
|
|
965
965
|
return `<div class="file-content-preview"><pre><code>${code}</code></pre>${truncNote}</div>`;
|
|
966
966
|
}
|
|
967
967
|
|
|
968
|
+
function buildPdfThumbUrl(repoPath: string, filePath: string, page: number) {
|
|
969
|
+
return `/api/repo/pdf-thumb?path=${encodeURIComponent(repoPath)}&file=${encodeURIComponent(filePath)}&page=${page}`;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function buildPdfPreviewHTML(repoPath: string, file: any) {
|
|
973
|
+
const thumbUrl = buildPdfThumbUrl(repoPath, file.path, 0);
|
|
974
|
+
return `<div class="file-content-preview file-image-preview pdf-preview" data-file="${escapeHtml(file.path)}" data-page="0" data-page-count="" style="display:flex;align-items:center;justify-content:center;height:100%;background:var(--bg-card);overflow:hidden;position:relative;">
|
|
975
|
+
<img class="pdf-preview-image" src="${thumbUrl}"
|
|
976
|
+
alt="${escapeHtml(file.name)}"
|
|
977
|
+
style="max-width:100%;max-height:100%;object-fit:contain;"
|
|
978
|
+
loading="lazy"
|
|
979
|
+
onerror="this.parentElement.innerHTML='<pre><code><span class=\\'error-notice\\'>PDF preview unavailable</span></code></pre>'" />
|
|
980
|
+
<div class="pdf-preview-toolbar" style="position:absolute;left:8px;right:8px;bottom:8px;display:flex;align-items:center;justify-content:center;gap:8px;padding:6px 8px;background:rgba(10,10,15,0.72);backdrop-filter:blur(8px);border:1px solid rgba(255,255,255,0.08);border-radius:10px;z-index:2;">
|
|
981
|
+
<button class="pdf-page-prev" type="button" style="min-width:28px;height:28px;border-radius:7px;border:1px solid rgba(255,255,255,0.12);background:rgba(255,255,255,0.06);color:var(--text-primary);cursor:pointer;" disabled>‹</button>
|
|
982
|
+
<span class="pdf-page-indicator" style="font-size:11px;color:var(--text-primary);min-width:72px;text-align:center;">Page 1</span>
|
|
983
|
+
<button class="pdf-page-next" type="button" style="min-width:28px;height:28px;border-radius:7px;border:1px solid rgba(255,255,255,0.12);background:rgba(255,255,255,0.06);color:var(--text-primary);cursor:pointer;">›</button>
|
|
984
|
+
</div>
|
|
985
|
+
</div>`;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
async function setupPdfPreviewControls(ctx: CanvasContext, card: HTMLElement, file: any) {
|
|
989
|
+
const repoPath = ctx.snap().context.repoPath || "";
|
|
990
|
+
if (!repoPath) return;
|
|
991
|
+
|
|
992
|
+
const preview = card.querySelector('.pdf-preview') as HTMLElement | null;
|
|
993
|
+
const img = card.querySelector('.pdf-preview-image') as HTMLImageElement | null;
|
|
994
|
+
const prevBtn = card.querySelector('.pdf-page-prev') as HTMLButtonElement | null;
|
|
995
|
+
const nextBtn = card.querySelector('.pdf-page-next') as HTMLButtonElement | null;
|
|
996
|
+
const indicator = card.querySelector('.pdf-page-indicator') as HTMLElement | null;
|
|
997
|
+
if (!preview || !img || !prevBtn || !nextBtn || !indicator) return;
|
|
998
|
+
|
|
999
|
+
let page = 0;
|
|
1000
|
+
let pageCount: number | null = null;
|
|
1001
|
+
let lastGoodPage = 0;
|
|
1002
|
+
let loading = false;
|
|
1003
|
+
|
|
1004
|
+
const updateUi = () => {
|
|
1005
|
+
preview.dataset.page = String(page);
|
|
1006
|
+
preview.dataset.pageCount = pageCount == null ? '' : String(pageCount);
|
|
1007
|
+
indicator.textContent = pageCount && pageCount > 0 ? `Page ${page + 1} / ${pageCount}` : `Page ${page + 1}`;
|
|
1008
|
+
prevBtn.disabled = loading || page <= 0;
|
|
1009
|
+
nextBtn.disabled = loading || (pageCount != null && page >= pageCount - 1);
|
|
1010
|
+
prevBtn.style.opacity = prevBtn.disabled ? '0.45' : '1';
|
|
1011
|
+
nextBtn.style.opacity = nextBtn.disabled ? '0.45' : '1';
|
|
1012
|
+
prevBtn.style.cursor = prevBtn.disabled ? 'default' : 'pointer';
|
|
1013
|
+
nextBtn.style.cursor = nextBtn.disabled ? 'default' : 'pointer';
|
|
1014
|
+
};
|
|
1015
|
+
|
|
1016
|
+
const loadPage = (nextPage: number) => {
|
|
1017
|
+
if (loading || nextPage < 0) return;
|
|
1018
|
+
if (pageCount != null && nextPage >= pageCount) return;
|
|
1019
|
+
|
|
1020
|
+
loading = true;
|
|
1021
|
+
page = nextPage;
|
|
1022
|
+
updateUi();
|
|
1023
|
+
|
|
1024
|
+
const nextSrc = buildPdfThumbUrl(repoPath, file.path, nextPage);
|
|
1025
|
+
const rollbackPage = lastGoodPage;
|
|
1026
|
+
|
|
1027
|
+
img.onload = () => {
|
|
1028
|
+
lastGoodPage = nextPage;
|
|
1029
|
+
loading = false;
|
|
1030
|
+
img.onload = null;
|
|
1031
|
+
img.onerror = null;
|
|
1032
|
+
updateUi();
|
|
1033
|
+
};
|
|
1034
|
+
img.onerror = () => {
|
|
1035
|
+
loading = false;
|
|
1036
|
+
if (pageCount == null && nextPage > rollbackPage) {
|
|
1037
|
+
pageCount = nextPage;
|
|
1038
|
+
}
|
|
1039
|
+
page = rollbackPage;
|
|
1040
|
+
img.onload = () => {
|
|
1041
|
+
lastGoodPage = rollbackPage;
|
|
1042
|
+
img.onload = null;
|
|
1043
|
+
img.onerror = null;
|
|
1044
|
+
updateUi();
|
|
1045
|
+
};
|
|
1046
|
+
img.onerror = null;
|
|
1047
|
+
img.src = buildPdfThumbUrl(repoPath, file.path, rollbackPage);
|
|
1048
|
+
updateUi();
|
|
1049
|
+
};
|
|
1050
|
+
img.src = nextSrc;
|
|
1051
|
+
};
|
|
1052
|
+
|
|
1053
|
+
prevBtn.addEventListener('click', (e) => {
|
|
1054
|
+
e.stopPropagation();
|
|
1055
|
+
loadPage(page - 1);
|
|
1056
|
+
});
|
|
1057
|
+
nextBtn.addEventListener('click', (e) => {
|
|
1058
|
+
e.stopPropagation();
|
|
1059
|
+
loadPage(page + 1);
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
updateUi();
|
|
1063
|
+
|
|
1064
|
+
try {
|
|
1065
|
+
const res = await fetch(`/api/repo/pdf-meta?path=${encodeURIComponent(repoPath)}&file=${encodeURIComponent(file.path)}`);
|
|
1066
|
+
if (res.ok) {
|
|
1067
|
+
const data = await res.json();
|
|
1068
|
+
if (Number.isFinite(data?.pageCount) && data.pageCount > 0) {
|
|
1069
|
+
pageCount = data.pageCount;
|
|
1070
|
+
updateUi();
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
} catch {
|
|
1074
|
+
// Metadata is optional — controls still work with optimistic next/prev.
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
968
1078
|
// ─── Create all-file card (working tree) ────────────────
|
|
969
1079
|
export function createAllFileCard(
|
|
970
1080
|
ctx: CanvasContext,
|
|
@@ -1026,13 +1136,7 @@ export function createAllFileCard(
|
|
|
1026
1136
|
loading="lazy" />
|
|
1027
1137
|
</div>`;
|
|
1028
1138
|
} else if (isPdf) {
|
|
1029
|
-
contentHTML =
|
|
1030
|
-
<img src="/api/repo/pdf-thumb?path=${encodeURIComponent(ctx.snap().context.repoPath || "")}&file=${encodeURIComponent(file.path)}"
|
|
1031
|
-
alt="${escapeHtml(file.name)}"
|
|
1032
|
-
style="max-width:100%;max-height:100%;object-fit:contain;"
|
|
1033
|
-
loading="lazy"
|
|
1034
|
-
onerror="this.parentElement.innerHTML='<pre><code><span class=\\'error-notice\\'>PDF preview unavailable</span></code></pre>'" />
|
|
1035
|
-
</div>`;
|
|
1139
|
+
contentHTML = buildPdfPreviewHTML(ctx.snap().context.repoPath || "", file);
|
|
1036
1140
|
} else if (file.isBinary) {
|
|
1037
1141
|
contentHTML = `<div class="file-content-preview"><pre><code><span class="error-notice">Binary file</span></code></pre></div>`;
|
|
1038
1142
|
} else if (file.content) {
|
|
@@ -1208,6 +1312,10 @@ export function createAllFileCard(
|
|
|
1208
1312
|
});
|
|
1209
1313
|
}
|
|
1210
1314
|
|
|
1315
|
+
if (isPdf) {
|
|
1316
|
+
setupPdfPreviewControls(ctx, card, file);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1211
1319
|
if (canvasOptions) {
|
|
1212
1320
|
const previewEl = card.querySelector(".canvas-container") as HTMLElement;
|
|
1213
1321
|
if (previewEl) {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { getLowZoomPreviewText, getLowZoomScale } from './low-zoom-preview';
|
|
3
|
+
|
|
4
|
+
describe('low zoom preview helpers', () => {
|
|
5
|
+
test('anchors preview text to approximate saved scroll position', () => {
|
|
6
|
+
const file = {
|
|
7
|
+
path: 'src/example.ts',
|
|
8
|
+
ext: 'ts',
|
|
9
|
+
content: Array.from({ length: 12 }, (_, i) => `line-${i + 1}`).join('\n'),
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
expect(getLowZoomPreviewText(file, 0).startsWith('line-1')).toBe(true);
|
|
13
|
+
expect(getLowZoomPreviewText(file, 40).startsWith('line-3')).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('skips binary or unsupported files', () => {
|
|
17
|
+
expect(getLowZoomPreviewText({ path: 'image.png', ext: 'png', content: 'abc' }, 0)).toBe('');
|
|
18
|
+
expect(getLowZoomPreviewText({ path: 'bin.dat', ext: 'dat', isBinary: true, content: 'abc' }, 0)).toBe('');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('increases world-space font size as zoom goes down', () => {
|
|
22
|
+
const near = getLowZoomScale(0.25);
|
|
23
|
+
const far = getLowZoomScale(0.1);
|
|
24
|
+
expect(far.titleFont).toBeGreaterThan(near.titleFont);
|
|
25
|
+
expect(far.bodyFont).toBeGreaterThan(near.bodyFont);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const PREVIEWABLE_EXTS = new Set([
|
|
2
|
+
'ts', 'tsx', 'js', 'jsx', 'json', 'css', 'scss', 'html', 'md', 'py', 'rs', 'go', 'vue', 'svelte', 'toml', 'yaml', 'yml', 'sh', 'sql', 'txt'
|
|
3
|
+
]);
|
|
4
|
+
|
|
5
|
+
export function getLowZoomScale(zoom: number) {
|
|
6
|
+
const clampedZoom = Math.max(0.08, Math.min(0.25, zoom));
|
|
7
|
+
const progress = (0.25 - clampedZoom) / (0.25 - 0.08);
|
|
8
|
+
const desiredScreenTitle = 10 + progress * 4;
|
|
9
|
+
const desiredScreenBody = 8 + progress * 4;
|
|
10
|
+
return {
|
|
11
|
+
titleFont: desiredScreenTitle / clampedZoom,
|
|
12
|
+
bodyFont: desiredScreenBody / clampedZoom,
|
|
13
|
+
bodyLineHeight: (desiredScreenBody * 1.45) / clampedZoom,
|
|
14
|
+
padding: (10 + progress * 4) / clampedZoom,
|
|
15
|
+
gap: (6 + progress * 3) / clampedZoom,
|
|
16
|
+
radius: 8 / clampedZoom,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getLowZoomPreviewText(file: any, scrollTop: number): string {
|
|
21
|
+
if (!file || file.isBinary || !file.content) return '';
|
|
22
|
+
|
|
23
|
+
const ext = (file.ext || file.path?.split('.').pop() || '').toLowerCase();
|
|
24
|
+
if (!PREVIEWABLE_EXTS.has(ext)) return '';
|
|
25
|
+
|
|
26
|
+
const normalized = String(file.content).replace(/\t/g, ' ');
|
|
27
|
+
const lines = normalized.split('\n');
|
|
28
|
+
const approxLineHeight = 20;
|
|
29
|
+
const startLine = Math.max(0, Math.floor(scrollTop / approxLineHeight));
|
|
30
|
+
return lines.slice(startLine, startLine + 60).join('\n').trim();
|
|
31
|
+
}
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { measure } from 'measure-fn';
|
|
25
25
|
import type { CanvasContext } from './context';
|
|
26
|
+
import { getLowZoomPreviewText, getLowZoomScale } from './low-zoom-preview';
|
|
26
27
|
import { materializeViewport } from './xydraw-bridge';
|
|
27
28
|
|
|
28
29
|
// ── Culling state ──────────────────────────────────────────
|
|
@@ -89,7 +90,7 @@ export function getPinnedCards(): Set<string> {
|
|
|
89
90
|
return _pinnedCards;
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
// ── Status colors for
|
|
93
|
+
// ── Status colors for low-zoom cards
|
|
93
94
|
const PILL_COLORS: Record<string, string> = {
|
|
94
95
|
'ts': '#3178c6',
|
|
95
96
|
'tsx': '#3178c6',
|
|
@@ -118,25 +119,31 @@ function getPillColor(path: string, isChanged: boolean): string {
|
|
|
118
119
|
return PILL_COLORS[ext] || '#6b7280'; // Default gray
|
|
119
120
|
}
|
|
120
121
|
|
|
122
|
+
function getSavedScrollTop(ctx: CanvasContext, path: string): number {
|
|
123
|
+
const saved = ctx.positions.get(`scroll:${path}`);
|
|
124
|
+
return saved?.x || 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
121
127
|
/**
|
|
122
128
|
* Create a lightweight pill placeholder for a file.
|
|
123
129
|
* ~3 DOM nodes vs ~100+ for a full card = massive perf win at low zoom.
|
|
124
130
|
* Uses vertical text to fit file names in compact card footprint.
|
|
125
131
|
*/
|
|
126
|
-
function createPillCard(path: string, x: number, y: number, w: number, h: number, isChanged: boolean, animate = false): HTMLElement {
|
|
132
|
+
function createPillCard(ctx: CanvasContext, file: any, path: string, x: number, y: number, w: number, h: number, isChanged: boolean, zoom: number, animate = false): HTMLElement {
|
|
127
133
|
const pill = document.createElement('div');
|
|
128
134
|
pill.className = 'file-pill';
|
|
129
135
|
pill.dataset.path = path;
|
|
136
|
+
pill.dataset.previewMode = 'content';
|
|
130
137
|
pill.style.cssText = `
|
|
131
138
|
position: absolute;
|
|
132
139
|
left: ${x}px;
|
|
133
140
|
top: ${y}px;
|
|
134
141
|
width: ${w}px;
|
|
135
142
|
height: ${h}px;
|
|
136
|
-
background:
|
|
143
|
+
background: linear-gradient(180deg, rgba(15,23,42,0.96) 0%, rgba(2,6,23,0.96) 100%);
|
|
137
144
|
border-radius: 6px;
|
|
138
|
-
opacity: ${animate ? '0' : '0.
|
|
139
|
-
contain: layout style;
|
|
145
|
+
opacity: ${animate ? '0' : '0.94'};
|
|
146
|
+
contain: layout style paint;
|
|
140
147
|
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
|
141
148
|
border: 1px solid rgba(255,255,255,0.12);
|
|
142
149
|
overflow: hidden;
|
|
@@ -146,45 +153,109 @@ function createPillCard(path: string, x: number, y: number, w: number, h: number
|
|
|
146
153
|
transform: ${animate ? 'scale(0.92)' : 'scale(1)'};
|
|
147
154
|
`;
|
|
148
155
|
|
|
156
|
+
const accent = document.createElement('div');
|
|
157
|
+
accent.className = 'file-pill-accent';
|
|
158
|
+
accent.style.cssText = `
|
|
159
|
+
position:absolute;
|
|
160
|
+
top:0;
|
|
161
|
+
left:0;
|
|
162
|
+
width:${Math.max(10, w * 0.02)}px;
|
|
163
|
+
height:100%;
|
|
164
|
+
background:${getPillColor(path, isChanged)};
|
|
165
|
+
opacity:0.95;
|
|
166
|
+
pointer-events:none;
|
|
167
|
+
`;
|
|
168
|
+
pill.appendChild(accent);
|
|
169
|
+
|
|
170
|
+
const inner = document.createElement('div');
|
|
171
|
+
inner.className = 'file-pill-inner';
|
|
172
|
+
inner.style.cssText = 'position:absolute; inset:0; transform-origin:top left; pointer-events:none;';
|
|
173
|
+
pill.appendChild(inner);
|
|
174
|
+
|
|
175
|
+
const title = document.createElement('div');
|
|
176
|
+
title.className = 'file-pill-title';
|
|
177
|
+
title.textContent = path.split('/').pop() || path;
|
|
178
|
+
inner.appendChild(title);
|
|
179
|
+
|
|
180
|
+
const sub = document.createElement('div');
|
|
181
|
+
sub.className = 'file-pill-subtitle';
|
|
182
|
+
sub.textContent = path.includes('/') ? path.split('/').slice(0, -1).join('/') : 'root';
|
|
183
|
+
inner.appendChild(sub);
|
|
184
|
+
|
|
185
|
+
const preview = document.createElement('div');
|
|
186
|
+
preview.className = 'file-pill-preview';
|
|
187
|
+
preview.textContent = getLowZoomPreviewText(file, getSavedScrollTop(ctx, path)) || 'Preview unavailable';
|
|
188
|
+
inner.appendChild(preview);
|
|
189
|
+
|
|
190
|
+
updatePillCardLayout(pill, zoom);
|
|
191
|
+
|
|
149
192
|
// Animate pill entrance
|
|
150
193
|
if (animate) {
|
|
151
194
|
requestAnimationFrame(() => {
|
|
152
|
-
pill.style.opacity = '0.
|
|
195
|
+
pill.style.opacity = '0.94';
|
|
153
196
|
pill.style.transform = 'scale(1)';
|
|
154
197
|
});
|
|
155
198
|
}
|
|
156
199
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
200
|
+
return pill;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function updatePillCardLayout(pill: HTMLElement, zoom: number) {
|
|
204
|
+
const x = parseFloat(pill.style.left) || 0;
|
|
205
|
+
const y = parseFloat(pill.style.top) || 0;
|
|
206
|
+
const w = parseFloat(pill.style.width) || 580;
|
|
207
|
+
const h = parseFloat(pill.style.height) || 700;
|
|
208
|
+
const scale = getLowZoomScale(zoom);
|
|
209
|
+
pill.dataset.zoomBucket = zoom.toFixed(3);
|
|
210
|
+
pill.style.borderRadius = `${Math.max(6, scale.radius)}px`;
|
|
211
|
+
|
|
212
|
+
const inner = pill.querySelector('.file-pill-inner') as HTMLElement | null;
|
|
213
|
+
const title = pill.querySelector('.file-pill-title') as HTMLElement | null;
|
|
214
|
+
const sub = pill.querySelector('.file-pill-subtitle') as HTMLElement | null;
|
|
215
|
+
const preview = pill.querySelector('.file-pill-preview') as HTMLElement | null;
|
|
216
|
+
if (!inner || !title || !sub || !preview) return;
|
|
217
|
+
|
|
218
|
+
inner.style.padding = `${scale.padding}px ${scale.padding}px ${scale.padding}px ${scale.padding + Math.max(14, w * 0.02)}px`;
|
|
219
|
+
inner.style.display = 'flex';
|
|
220
|
+
inner.style.flexDirection = 'column';
|
|
221
|
+
inner.style.gap = `${scale.gap}px`;
|
|
222
|
+
|
|
223
|
+
title.style.cssText = `
|
|
224
|
+
color:#f8fafc;
|
|
225
|
+
font-family:'JetBrains Mono', monospace;
|
|
226
|
+
font-size:${scale.titleFont}px;
|
|
227
|
+
font-weight:700;
|
|
228
|
+
line-height:1.05;
|
|
229
|
+
white-space:nowrap;
|
|
230
|
+
overflow:hidden;
|
|
231
|
+
text-overflow:ellipsis;
|
|
232
|
+
text-shadow:0 2px 8px rgba(0,0,0,0.45);
|
|
184
233
|
`;
|
|
185
|
-
pill.appendChild(label);
|
|
186
234
|
|
|
187
|
-
|
|
235
|
+
sub.style.cssText = `
|
|
236
|
+
color:rgba(226,232,240,0.72);
|
|
237
|
+
font-family:'JetBrains Mono', monospace;
|
|
238
|
+
font-size:${Math.max(scale.bodyFont * 0.78, 8 / Math.max(zoom, 0.08))}px;
|
|
239
|
+
line-height:1.05;
|
|
240
|
+
white-space:nowrap;
|
|
241
|
+
overflow:hidden;
|
|
242
|
+
text-overflow:ellipsis;
|
|
243
|
+
`;
|
|
244
|
+
|
|
245
|
+
preview.style.cssText = `
|
|
246
|
+
color:rgba(226,232,240,0.92);
|
|
247
|
+
font-family:'JetBrains Mono', monospace;
|
|
248
|
+
font-size:${scale.bodyFont}px;
|
|
249
|
+
line-height:${scale.bodyLineHeight}px;
|
|
250
|
+
white-space:pre-wrap;
|
|
251
|
+
overflow-wrap:anywhere;
|
|
252
|
+
word-break:break-word;
|
|
253
|
+
overflow:hidden;
|
|
254
|
+
flex:1 1 auto;
|
|
255
|
+
text-shadow:0 1px 4px rgba(0,0,0,0.3);
|
|
256
|
+
mask-image:linear-gradient(to bottom, black 0%, black 84%, transparent 100%);
|
|
257
|
+
-webkit-mask-image:linear-gradient(to bottom, black 0%, black 84%, transparent 100%);
|
|
258
|
+
`;
|
|
188
259
|
}
|
|
189
260
|
|
|
190
261
|
/**
|
|
@@ -399,9 +470,11 @@ export function performViewportCulling(ctx: CanvasContext) {
|
|
|
399
470
|
);
|
|
400
471
|
|
|
401
472
|
if (inView && !pillCards.has(path)) {
|
|
402
|
-
const pill = createPillCard(path, x, y, cardW, cardH, !!isChanged, true);
|
|
473
|
+
const pill = createPillCard(ctx, file, path, x, y, cardW, cardH, !!isChanged, zoom, true);
|
|
403
474
|
ctx.canvas.appendChild(pill);
|
|
404
475
|
pillCards.set(path, pill);
|
|
476
|
+
} else if (inView && pillCards.has(path)) {
|
|
477
|
+
updatePillCardLayout(pillCards.get(path)!, zoom);
|
|
405
478
|
} else if (!inView && pillCards.has(path)) {
|
|
406
479
|
removePillForPath(path);
|
|
407
480
|
}
|
|
@@ -424,10 +497,13 @@ export function performViewportCulling(ctx: CanvasContext) {
|
|
|
424
497
|
);
|
|
425
498
|
|
|
426
499
|
if (inView) {
|
|
427
|
-
const
|
|
500
|
+
const file = ctx.allFilesData?.find(f => f.path === path) || ctx.commitFilesData?.find(f => f.path === path) || null;
|
|
501
|
+
const pill = createPillCard(ctx, file, path, x, y, w, h, isChanged, zoom, true);
|
|
428
502
|
ctx.canvas.appendChild(pill);
|
|
429
503
|
pillCards.set(path, pill);
|
|
430
504
|
}
|
|
505
|
+
} else {
|
|
506
|
+
updatePillCardLayout(pillCards.get(path)!, zoom);
|
|
431
507
|
}
|
|
432
508
|
}
|
|
433
509
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gitmaps",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"gitmaps": "cli.ts"
|
|
@@ -16,13 +16,14 @@
|
|
|
16
16
|
"scripts": {
|
|
17
17
|
"dev": "bun run server.ts",
|
|
18
18
|
"start": "bun run server.ts",
|
|
19
|
-
"test": "bun test app/api/repo/load/route.test.ts app/lib/route-catchall.test.ts app/lib/status-bar.test.ts app/lib/xydraw.test.ts app/lib/transclusion-smoke.test.ts app/lib/repo-select.test.ts packages/galaxydraw/perf.test.ts",
|
|
19
|
+
"test": "bun test app/api/repo/load/route.test.ts app/lib/route-catchall.test.ts app/lib/status-bar.test.ts app/lib/xydraw.test.ts app/lib/transclusion-smoke.test.ts app/lib/repo-select.test.ts app/lib/low-zoom-preview.test.ts packages/galaxydraw/perf.test.ts",
|
|
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
23
|
"smoke:browser-tools:guard": "bash scripts/browser-smoke-guard.sh",
|
|
24
24
|
"smoke:browser-tools:self-check": "bash scripts/browser-smoke-self-check.sh",
|
|
25
25
|
"smoke:browser-tools:check": "bash scripts/browser-smoke-check.sh",
|
|
26
|
+
"smoke:docker-image": "bash scripts/docker-image-smoke.sh",
|
|
26
27
|
"prepublishOnly": "echo 'Publishing gitmaps to npm'"
|
|
27
28
|
},
|
|
28
29
|
"keywords": [
|