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 pill cards
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: ${getPillColor(path, isChanged)};
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.9'};
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.9';
195
+ pill.style.opacity = '0.94';
153
196
  pill.style.transform = 'scale(1)';
154
197
  });
155
198
  }
156
199
 
157
- // File name label — show parent dir for common ambiguous filenames
158
- const parts = path.split('/');
159
- const filename = parts.pop() || path;
160
- const AMBIGUOUS = ['route.ts', 'route.tsx', 'page.tsx', 'page.ts', 'index.ts', 'index.tsx', 'index.js', 'layout.tsx', 'middleware.ts'];
161
- const name = AMBIGUOUS.includes(filename) && parts.length > 0
162
- ? `${parts[parts.length - 1]}/${filename}`
163
- : filename;
164
- const label = document.createElement('span');
165
- label.className = 'file-pill-label';
166
- label.textContent = name;
167
- label.style.cssText = `
168
- position: absolute;
169
- top: 50%;
170
- left: 50%;
171
- transform: translate(-50%, -50%) rotate(-90deg);
172
- white-space: nowrap;
173
- font-size: 48px;
174
- font-weight: 700;
175
- color: #fff;
176
- overflow: hidden;
177
- text-overflow: ellipsis;
178
- max-width: ${h - 40}px;
179
- line-height: 1;
180
- letter-spacing: 2px;
181
- font-family: 'JetBrains Mono', monospace;
182
- text-shadow: 0 2px 8px rgba(0,0,0,0.7);
183
- pointer-events: none;
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
- return pill;
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 pill = createPillCard(path, x, y, w, h, isChanged, true);
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.4",
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": [