gitmaps 1.1.4 → 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.
|
@@ -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": [
|