gitmaps 1.1.23 → 1.1.24

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 CHANGED
Binary file
package/app/globals.css CHANGED
@@ -1145,24 +1145,17 @@ body {
1145
1145
 
1146
1146
  /* ── Sticky Zoom Controls ── */
1147
1147
  body.repo-loading .sticky-zoom-pill,
1148
- body.repo-loading .floating-top-controls,
1148
+ body.repo-loading .detail-mode-switch,
1149
1149
  body.landing-placeholder-visible .sticky-zoom-pill,
1150
- body.landing-placeholder-visible .floating-top-controls {
1150
+ body.landing-placeholder-visible .detail-mode-switch {
1151
1151
  display: none;
1152
1152
  }
1153
1153
 
1154
- .floating-top-controls {
1154
+ .detail-mode-switch {
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;
1166
1159
  }
1167
1160
 
1168
1161
  .detail-mode-btn {
@@ -1199,15 +1192,6 @@ body.landing-placeholder-visible .floating-top-controls {
1199
1192
  box-shadow: 0 10px 28px rgba(59, 130, 246, 0.16);
1200
1193
  }
1201
1194
 
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
-
1211
1195
  .detail-mode-label {
1212
1196
  opacity: 0.72;
1213
1197
  text-transform: uppercase;
package/app/layout.tsx CHANGED
@@ -875,16 +875,10 @@ export default function RootLayout({ children }: { children: any }) {
875
875
  </div>
876
876
 
877
877
  {/* Sticky Zoom Controls — floating pill, bottom-right */}
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>
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">Renderer</span>
881
+ <span id="detailModeState" className="detail-mode-state">Preview</span>
888
882
  </button>
889
883
  </div>
890
884
 
@@ -943,6 +937,22 @@ export default function RootLayout({ children }: { children: any }) {
943
937
  <path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7" />
944
938
  </svg>
945
939
  </button>
940
+ <div className="sz-divider" />
941
+ <button id="openSettingsBottom" className="sz-btn sz-fit" title="Open settings">
942
+ <svg
943
+ viewBox="0 0 24 24"
944
+ width="14"
945
+ height="14"
946
+ fill="none"
947
+ stroke="currentColor"
948
+ strokeWidth="2"
949
+ strokeLinecap="round"
950
+ strokeLinejoin="round"
951
+ >
952
+ <circle cx="12" cy="12" r="3" />
953
+ <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" />
954
+ </svg>
955
+ </button>
946
956
  </div>
947
957
 
948
958
  {/* Bottom Layers Bar */}
@@ -744,7 +744,7 @@ export function setupEventListeners(ctx: CanvasContext) {
744
744
  import('./settings-modal').then(({ openSettingsModal }) => openSettingsModal(ctx));
745
745
  };
746
746
  document.getElementById('openSettings')?.addEventListener('click', openSettings);
747
- document.getElementById('openSettingsFloating')?.addEventListener('click', openSettings);
747
+ document.getElementById('openSettingsBottom')?.addEventListener('click', openSettings);
748
748
 
749
749
  // Global search
750
750
  document.getElementById('openGlobalSearch')?.addEventListener('click', () => {
@@ -906,6 +906,24 @@ export function setupEventListeners(ctx: CanvasContext) {
906
906
  return;
907
907
  }
908
908
 
909
+ // Ctrl+= / Ctrl+- / Ctrl+0 = preview text size
910
+ if ((e.ctrlKey || e.metaKey) && !e.shiftKey && (e.key === '=' || e.key === '+' || e.key === '-' || e.key === '0')) {
911
+ e.preventDefault();
912
+ import('./settings').then(({ getSettings, updateSettings }) => {
913
+ const settings = getSettings();
914
+ let next = settings.previewFontPx;
915
+ if (e.key === '=' || e.key === '+') next = Math.min(16, settings.previewFontPx + 1);
916
+ else if (e.key === '-') next = Math.max(7, settings.previewFontPx - 1);
917
+ else next = 10;
918
+ if (next !== settings.previewFontPx) {
919
+ updateSettings({ previewFontPx: next });
920
+ window.dispatchEvent(new CustomEvent('gitcanvas:preview-settings-changed'));
921
+ showToast(`Preview text: ${next}px`, 'info');
922
+ }
923
+ });
924
+ return;
925
+ }
926
+
909
927
  // Ctrl+Shift+E = Export canvas as PNG
910
928
  if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key.toLowerCase() === 'e') {
911
929
  e.preventDefault();
@@ -52,6 +52,24 @@ export function saveLayers(ctx: CanvasContext) {
52
52
  localStorage.setItem(`gitcanvas:layers:${ctx.snap().context.repoPath}`, JSON.stringify(layerState.layers));
53
53
  }
54
54
 
55
+ function removeFileFromCurrentCanvas(ctx: CanvasContext, path: string) {
56
+ const card = ctx.fileCards.get(path);
57
+ if (card) {
58
+ card.remove();
59
+ ctx.fileCards.delete(path);
60
+ }
61
+ const pill = ctx.canvas?.querySelector(`.file-pill[data-path="${CSS.escape(path)}"]`) as HTMLElement | null;
62
+ if (pill) pill.remove();
63
+ if (ctx.deferredCards.has(path)) {
64
+ ctx.deferredCards.delete(path);
65
+ }
66
+ const selected = ctx.snap().context.selectedCards || [];
67
+ if (selected.includes(path)) {
68
+ ctx.actor.send({ type: 'SELECT_CARD', path, shift: true });
69
+ }
70
+ import('./canvas').then(({ forceMinimapRebuild }) => forceMinimapRebuild(ctx)).catch(() => {});
71
+ }
72
+
55
73
  export function createLayer(ctx: CanvasContext, name: string) {
56
74
  const newLayer: LayerData = {
57
75
  id: `layer_${Date.now()}`,
@@ -124,9 +142,8 @@ export function moveFileToLayer(ctx: CanvasContext, layerId: string, path: strin
124
142
 
125
143
  saveLayers(ctx);
126
144
  renderLayersUI(ctx);
127
- // Re-render current layer to hide the moved file
128
145
  if (layerState.activeLayerId === 'default') {
129
- applyLayer(ctx);
146
+ removeFileFromCurrentCanvas(ctx, path);
130
147
  }
131
148
  }
132
149
 
@@ -150,6 +167,10 @@ export function removeFileFromLayer(ctx: CanvasContext, layerId: string, path: s
150
167
  saveLayers(ctx);
151
168
  renderLayersUI(ctx);
152
169
  if (layer.id === layerState.activeLayerId) applyLayer(ctx);
170
+ else if (layerState.activeLayerId === 'default') {
171
+ // File becomes visible in Main again without a full rerender on the active custom layer path.
172
+ applyLayer(ctx);
173
+ }
153
174
  }
154
175
  }
155
176
 
@@ -33,11 +33,11 @@ describe('low zoom preview helpers', () => {
33
33
  expect(lines.some((line) => line.tone === 'added' && line.text === 'two')).toBe(true);
34
34
  });
35
35
 
36
- test('keeps zoomed-out on-screen text smaller than zoomed-in text', () => {
36
+ test('keeps preview text screen size stable across zoom levels', () => {
37
37
  const far = getLowZoomScale(0.1);
38
38
  const near = getLowZoomScale(1);
39
- expect(far.titleFont * 0.1).toBeLessThan(near.titleFont * 1);
40
- expect(far.bodyFont * 0.1).toBeLessThan(near.bodyFont * 1);
39
+ expect(Math.round(far.titleFont * 0.1)).toBe(Math.round(near.titleFont * 1));
40
+ expect(Math.round(far.bodyFont * 0.1)).toBe(Math.round(near.bodyFont * 1));
41
41
  });
42
42
 
43
43
  test('wraps preview text into bounded lines with ellipsis', () => {
@@ -6,13 +6,12 @@ const PREVIEWABLE_EXTS = new Set([
6
6
 
7
7
  export function getLowZoomScale(zoom: number) {
8
8
  const clampedZoom = Math.max(0.08, Math.min(1, zoom));
9
- const progress = (clampedZoom - 0.08) / (1 - 0.08);
10
9
  const settings = getSettings();
11
10
 
12
- const desiredScreenTitle = settings.previewFarTitlePx + progress * (settings.previewNearTitlePx - settings.previewFarTitlePx);
13
- const desiredScreenBody = 5.5 + progress * 6.5;
14
- const desiredScreenPadding = 6 + progress * 8;
15
- const desiredScreenGap = 4 + progress * 4;
11
+ const desiredScreenBody = settings.previewFontPx;
12
+ const desiredScreenTitle = settings.previewFontPx + 2;
13
+ const desiredScreenPadding = 10;
14
+ const desiredScreenGap = 6;
16
15
 
17
16
  return {
18
17
  titleFont: desiredScreenTitle / clampedZoom,
package/app/lib/repo.tsx CHANGED
@@ -991,80 +991,16 @@ export function renderAllFilesOnCanvas(ctx: CanvasContext, files: any[]) {
991
991
 
992
992
  renderConnections(ctx);
993
993
  buildConnectionMarkers(ctx);
994
- renderDirectoryLabels(ctx);
995
994
  forceMinimapRebuild(ctx);
996
995
  // Cull off-screen cards after browser layout (needs rAF for valid dimensions)
997
996
  requestAnimationFrame(() => performViewportCulling(ctx));
998
997
  });
999
998
  }
1000
999
 
1001
- // ─── Directory labels on canvas ──────────────────────────
1002
- // Groups visible file cards by parent directory and renders
1003
- // a world-space label above each directory cluster.
1000
+ // Directory labels disabled the auto-generated shared-prefix / directory containers
1001
+ // were adding noise and confusion without helping navigation.
1004
1002
  function renderDirectoryLabels(ctx: CanvasContext) {
1005
- // Remove existing labels
1006
- ctx.canvas?.querySelectorAll(".dir-label").forEach((el) => el.remove());
1007
-
1008
- // Group cards by parent directory
1009
- const groups = new Map<
1010
- string,
1011
- { minX: number; minY: number; maxX: number; count: number }
1012
- >();
1013
-
1014
- const processCard = (path: string, x: number, y: number, w: number) => {
1015
- const dir = path.includes("/")
1016
- ? path.substring(0, path.lastIndexOf("/"))
1017
- : ".";
1018
- const g = groups.get(dir);
1019
- if (g) {
1020
- g.minX = Math.min(g.minX, x);
1021
- g.minY = Math.min(g.minY, y);
1022
- g.maxX = Math.max(g.maxX, x + w);
1023
- g.count++;
1024
- } else {
1025
- groups.set(dir, { minX: x, minY: y, maxX: x + w, count: 1 });
1026
- }
1027
- };
1028
-
1029
- // Created cards (in DOM)
1030
- ctx.fileCards.forEach((card, path) => {
1031
- const x = parseFloat(card.style.left) || 0;
1032
- const y = parseFloat(card.style.top) || 0;
1033
- const w = card.offsetWidth || 580;
1034
- processCard(path, x, y, w);
1035
- });
1036
-
1037
- // Deferred cards (not yet in DOM)
1038
- ctx.deferredCards.forEach((info, path) => {
1039
- const w = info.size?.width || 580;
1040
- processCard(path, info.x, info.y, w);
1041
- });
1042
-
1043
- // Only show labels if we have multiple directories
1044
- if (groups.size <= 1) return;
1045
-
1046
- const frag = document.createDocumentFragment();
1047
- for (const [dir, g] of groups) {
1048
- const label = document.createElement("div");
1049
- label.className = "dir-label";
1050
- label.dataset.dir = dir;
1051
- const centerX = (g.minX + g.maxX) / 2;
1052
- label.style.left = `${centerX}px`;
1053
- label.style.top = `${g.minY - 36}px`;
1054
- label.style.transform = "translateX(-50%)";
1055
- label.innerHTML = `<span class="dir-label-icon">📁</span> ${dir}<span class="dir-label-count">${g.count}</span>`;
1056
-
1057
- // Click to collapse directory into a group card
1058
- label.addEventListener("click", (e) => {
1059
- e.stopPropagation();
1060
- import("./card-groups").then(({ toggleDirectoryCollapse }) => {
1061
- toggleDirectoryCollapse(ctx, dir);
1062
- });
1063
- });
1064
-
1065
- frag.appendChild(label);
1066
- }
1067
- ctx.canvas?.appendChild(frag);
1003
+ ctx.canvas?.querySelectorAll('.dir-label').forEach((el) => el.remove());
1068
1004
  }
1069
1005
 
1070
1006
  // ─── Highlight changed files without re-rendering ────────
@@ -142,13 +142,9 @@ function SettingsPanel({ settings }: { settings: GitCanvasSettings }) {
142
142
 
143
143
  {/* Preview Mode Section */}
144
144
  <SettingsSection title="Preview Mode">
145
- <SettingsRow label="Zoomed-out title" desc="Filename size at the farthest preview zoom">
146
- <Slider id="settingPreviewFarTitlePx" valueId="previewFarTitlePxValue"
147
- min={6} max={14} step={1} value={settings.previewFarTitlePx} suffix="px" />
148
- </SettingsRow>
149
- <SettingsRow label="Zoomed-in title" desc="Filename size at the closest preview zoom">
150
- <Slider id="settingPreviewNearTitlePx" valueId="previewNearTitlePxValue"
151
- min={10} max={24} step={1} value={settings.previewNearTitlePx} suffix="px" />
145
+ <SettingsRow label="Preview text size" desc="Fixed text size used by preview cards">
146
+ <Slider id="settingPreviewFontPx" valueId="previewFontPxValue"
147
+ min={7} max={16} step={1} value={settings.previewFontPx} suffix="px" />
152
148
  </SettingsRow>
153
149
  <SettingsRow label="Zoomed-out lines" desc="Minimum content lines shown at the farthest preview zoom">
154
150
  <Slider id="settingPreviewFarLines" valueId="previewFarLinesValue"
@@ -270,19 +266,11 @@ export function openSettingsModal(ctx?: any) {
270
266
  updateSettings({ maxVisibleLines: parseInt(maxLinesSlider.value) });
271
267
  });
272
268
 
273
- const previewFarTitleSlider = _modal.querySelector('#settingPreviewFarTitlePx') as HTMLInputElement;
274
- const previewFarTitleValue = _modal.querySelector('#previewFarTitlePxValue')!;
275
- previewFarTitleSlider?.addEventListener('input', () => {
276
- previewFarTitleValue.textContent = `${previewFarTitleSlider.value}px`;
277
- updateSettings({ previewFarTitlePx: parseInt(previewFarTitleSlider.value) });
278
- window.dispatchEvent(new CustomEvent('gitcanvas:preview-settings-changed'));
279
- });
280
-
281
- const previewNearTitleSlider = _modal.querySelector('#settingPreviewNearTitlePx') as HTMLInputElement;
282
- const previewNearTitleValue = _modal.querySelector('#previewNearTitlePxValue')!;
283
- previewNearTitleSlider?.addEventListener('input', () => {
284
- previewNearTitleValue.textContent = `${previewNearTitleSlider.value}px`;
285
- updateSettings({ previewNearTitlePx: parseInt(previewNearTitleSlider.value) });
269
+ const previewFontSlider = _modal.querySelector('#settingPreviewFontPx') as HTMLInputElement;
270
+ const previewFontValue = _modal.querySelector('#previewFontPxValue')!;
271
+ previewFontSlider?.addEventListener('input', () => {
272
+ previewFontValue.textContent = `${previewFontSlider.value}px`;
273
+ updateSettings({ previewFontPx: parseInt(previewFontSlider.value) });
286
274
  window.dispatchEvent(new CustomEvent('gitcanvas:preview-settings-changed'));
287
275
  });
288
276
 
@@ -31,10 +31,8 @@ export interface GitCanvasSettings {
31
31
  heatmapEnabled: boolean;
32
32
  /** Heatmap time range in days */
33
33
  heatmapDays: number;
34
- /** Preview mode far-zoom title size (screen px) */
35
- previewFarTitlePx: number;
36
- /** Preview mode near-zoom title size (screen px) */
37
- previewNearTitlePx: number;
34
+ /** Preview mode fixed text size (screen px) */
35
+ previewFontPx: number;
38
36
  /** Preview mode far-zoom minimum visible lines */
39
37
  previewFarLines: number;
40
38
  /** Preview mode near-zoom target visible lines */
@@ -54,8 +52,7 @@ const DEFAULTS: GitCanvasSettings = {
54
52
  popupFontSize: 14,
55
53
  heatmapEnabled: false,
56
54
  heatmapDays: 90,
57
- previewFarTitlePx: 8,
58
- previewNearTitlePx: 16,
55
+ previewFontPx: 10,
59
56
  previewFarLines: 3,
60
57
  previewNearLines: 20,
61
58
  };
@@ -69,6 +66,11 @@ export function getSettings(): GitCanvasSettings {
69
66
  const raw = localStorage.getItem(STORAGE_KEY);
70
67
  if (raw) {
71
68
  const parsed = JSON.parse(raw);
69
+ if (parsed.previewFontPx == null) {
70
+ const far = typeof parsed.previewFarTitlePx === 'number' ? parsed.previewFarTitlePx : DEFAULTS.previewFontPx;
71
+ const near = typeof parsed.previewNearTitlePx === 'number' ? parsed.previewNearTitlePx : DEFAULTS.previewFontPx;
72
+ parsed.previewFontPx = Math.round((far + near) / 2);
73
+ }
72
74
  _settings = { ...DEFAULTS, ...parsed };
73
75
  } else {
74
76
  _settings = { ...DEFAULTS };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitmaps",
3
- "version": "1.1.23",
3
+ "version": "1.1.24",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "gitmaps": "cli.ts"