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 +0 -0
- package/app/globals.css +3 -19
- package/app/layout.tsx +20 -10
- package/app/lib/events.tsx +19 -1
- package/app/lib/layers.tsx +23 -2
- package/app/lib/low-zoom-preview.test.ts +3 -3
- package/app/lib/low-zoom-preview.ts +4 -5
- package/app/lib/repo.tsx +3 -67
- package/app/lib/settings-modal.tsx +8 -20
- package/app/lib/settings.ts +8 -6
- package/package.json +1 -1
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 .
|
|
1148
|
+
body.repo-loading .detail-mode-switch,
|
|
1149
1149
|
body.landing-placeholder-visible .sticky-zoom-pill,
|
|
1150
|
-
body.landing-placeholder-visible .
|
|
1150
|
+
body.landing-placeholder-visible .detail-mode-switch {
|
|
1151
1151
|
display: none;
|
|
1152
1152
|
}
|
|
1153
1153
|
|
|
1154
|
-
.
|
|
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="
|
|
879
|
-
<
|
|
880
|
-
<
|
|
881
|
-
|
|
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 */}
|
package/app/lib/events.tsx
CHANGED
|
@@ -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('
|
|
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();
|
package/app/lib/layers.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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).
|
|
40
|
-
expect(far.bodyFont * 0.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
|
|
13
|
-
const
|
|
14
|
-
const desiredScreenPadding =
|
|
15
|
-
const desiredScreenGap =
|
|
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
|
-
//
|
|
1002
|
-
//
|
|
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
|
-
|
|
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="
|
|
146
|
-
<Slider id="
|
|
147
|
-
min={
|
|
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
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
updateSettings({
|
|
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
|
|
package/app/lib/settings.ts
CHANGED
|
@@ -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
|
|
35
|
-
|
|
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
|
-
|
|
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 };
|