gitmaps 1.1.22 → 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/card-context-menu.tsx +18 -18
- package/app/lib/context.ts +1 -1
- package/app/lib/events.tsx +19 -1
- package/app/lib/file-preview.ts +1 -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 +11 -34
- 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 */}
|
|
@@ -50,32 +50,32 @@ function ContextMenu({ onAction, onActionLayer, onSelectFolder, isInActiveLayer,
|
|
|
50
50
|
|
|
51
51
|
return (
|
|
52
52
|
<>
|
|
53
|
-
<button className="ctx-item" onClick={() => onAction('copy-path')}>📋 Copy path</button>
|
|
54
|
-
<button className="ctx-item" onClick={() => onAction('select')}>☑️ Select</button>
|
|
53
|
+
<button type="button" className="ctx-item" onClick={() => onAction('copy-path')}>📋 Copy path</button>
|
|
54
|
+
<button type="button" className="ctx-item" onClick={() => onAction('select')}>☑️ Select</button>
|
|
55
55
|
{ancestors.length > 0 ? (
|
|
56
56
|
<div className="ctx-item ctx-dropdown">
|
|
57
57
|
<span>📁 Select from folder ▸</span>
|
|
58
58
|
<div className="ctx-dropdown-content">
|
|
59
59
|
{ancestors.map(dir => (
|
|
60
|
-
<button key={dir} className="ctx-item" onClick={() => onSelectFolder(dir)}>
|
|
60
|
+
<button type="button" key={dir} className="ctx-item" onClick={() => onSelectFolder(dir)}>
|
|
61
61
|
📂 {dir}
|
|
62
62
|
</button>
|
|
63
63
|
))}
|
|
64
64
|
</div>
|
|
65
65
|
</div>
|
|
66
66
|
) : (
|
|
67
|
-
<button className="ctx-item" onClick={() => onSelectFolder('')}>📁 Select all (root)</button>
|
|
67
|
+
<button type="button" className="ctx-item" onClick={() => onSelectFolder('')}>📁 Select all (root)</button>
|
|
68
68
|
)}
|
|
69
|
-
<button className="ctx-item" onClick={() => onAction('pin')}>{pinned ? '📌 Unpin card' : '📌 Pin card'}</button>
|
|
69
|
+
<button type="button" className="ctx-item" onClick={() => onAction('pin')}>{pinned ? '📌 Unpin card' : '📌 Pin card'}</button>
|
|
70
70
|
<div className="ctx-divider"></div>
|
|
71
|
-
<button className="ctx-item" onClick={() => onAction('expand')}>📖 Open in Editor</button>
|
|
72
|
-
<button className="ctx-item" onClick={() => onAction('edit')}>✏️ Edit file</button>
|
|
73
|
-
<button className="ctx-item" onClick={() => onAction('blame')}>👤 Git blame</button>
|
|
74
|
-
<button className="ctx-item" onClick={() => onAction('connect')}>🔗 Connect to...</button>
|
|
75
|
-
<button className="ctx-item" onClick={() => onAction('fit-content')}>📏 Fit content</button>
|
|
76
|
-
<button className="ctx-item" onClick={() => onAction('fit-screen')}>📺 Fit screen</button>
|
|
71
|
+
<button type="button" className="ctx-item" onClick={() => onAction('expand')}>📖 Open in Editor</button>
|
|
72
|
+
<button type="button" className="ctx-item" onClick={() => onAction('edit')}>✏️ Edit file</button>
|
|
73
|
+
<button type="button" className="ctx-item" onClick={() => onAction('blame')}>👤 Git blame</button>
|
|
74
|
+
<button type="button" className="ctx-item" onClick={() => onAction('connect')}>🔗 Connect to...</button>
|
|
75
|
+
<button type="button" className="ctx-item" onClick={() => onAction('fit-content')}>📏 Fit content</button>
|
|
76
|
+
<button type="button" className="ctx-item" onClick={() => onAction('fit-screen')}>📺 Fit screen</button>
|
|
77
77
|
<div className="ctx-divider"></div>
|
|
78
|
-
<button className="ctx-item" onClick={() => onAction('history')}>🕰️ File history</button>
|
|
78
|
+
<button type="button" className="ctx-item" onClick={() => onAction('history')}>🕰️ File history</button>
|
|
79
79
|
<div className="ctx-item ctx-dropdown">
|
|
80
80
|
<span>📦 Move to Layer ▸</span>
|
|
81
81
|
<div className="ctx-dropdown-content">
|
|
@@ -83,24 +83,24 @@ function ContextMenu({ onAction, onActionLayer, onSelectFolder, isInActiveLayer,
|
|
|
83
83
|
<div className="ctx-item" style="opacity: 0.5; pointer-events: none">No custom layers</div>
|
|
84
84
|
) : (
|
|
85
85
|
customLayers.map(l => (
|
|
86
|
-
<button key={l.id} className="ctx-item" onClick={() => onActionLayer(l.id)}>
|
|
86
|
+
<button type="button" key={l.id} className="ctx-item" onClick={() => onActionLayer(l.id)}>
|
|
87
87
|
+ {l.name}
|
|
88
88
|
</button>
|
|
89
89
|
))
|
|
90
90
|
)}
|
|
91
91
|
<div className="ctx-divider"></div>
|
|
92
|
-
<button className="ctx-item" onClick={() => onActionLayer('new')}>✨ Create New Layer</button>
|
|
92
|
+
<button type="button" className="ctx-item" onClick={() => onActionLayer('new')}>✨ Create New Layer</button>
|
|
93
93
|
</div>
|
|
94
94
|
</div>
|
|
95
95
|
{isInActiveLayer && (
|
|
96
|
-
<button className="ctx-item" onClick={() => onAction('remove-from-layer')} style="color: #60a5fa">
|
|
96
|
+
<button type="button" className="ctx-item" onClick={() => onAction('remove-from-layer')} style="color: #60a5fa">
|
|
97
97
|
↩ Move to Main
|
|
98
98
|
</button>
|
|
99
99
|
)}
|
|
100
100
|
<div className="ctx-divider"></div>
|
|
101
|
-
<button className="ctx-item" onClick={() => onAction('hide')} style="color: #f59e0b">🙈 Hide file</button>
|
|
102
|
-
<button className="ctx-item" onClick={() => onAction('rename')}>✏️ Rename / Move</button>
|
|
103
|
-
<button className="ctx-item" onClick={() => onAction('delete')} style="color: #ef4444">🗑️ Delete file</button>
|
|
101
|
+
<button type="button" className="ctx-item" onClick={() => onAction('hide')} style="color: #f59e0b">🙈 Hide file</button>
|
|
102
|
+
<button type="button" className="ctx-item" onClick={() => onAction('rename')}>✏️ Rename / Move</button>
|
|
103
|
+
<button type="button" className="ctx-item" onClick={() => onAction('delete')} style="color: #ef4444">🗑️ Delete file</button>
|
|
104
104
|
</>
|
|
105
105
|
);
|
|
106
106
|
}
|
package/app/lib/context.ts
CHANGED
|
@@ -94,7 +94,7 @@ export function createCanvasContext(actor: any): CanvasContext {
|
|
|
94
94
|
allFilesData: null,
|
|
95
95
|
commitFilesData: null,
|
|
96
96
|
deferredCards: new Map(),
|
|
97
|
-
controlMode: (localStorage.getItem('gitcanvas:controlMode') as any) || '
|
|
97
|
+
controlMode: (localStorage.getItem('gitcanvas:controlMode') as any) || 'simple',
|
|
98
98
|
};
|
|
99
99
|
|
|
100
100
|
currentCanvasContext = ctx;
|
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/file-preview.ts
CHANGED
|
@@ -35,7 +35,7 @@ let currentCardPath: string | null = null;
|
|
|
35
35
|
let isInitialized = false;
|
|
36
36
|
let _ctx: CanvasContext | null = null;
|
|
37
37
|
let isPreviewEnabled =
|
|
38
|
-
localStorage.getItem("gitmaps:previewEnabled")
|
|
38
|
+
localStorage.getItem("gitmaps:previewEnabled") === "true"; // disabled by default
|
|
39
39
|
let _isHoveringPopup = false;
|
|
40
40
|
|
|
41
41
|
// ─── Popup container ─────────────────────────────────────
|
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 ────────
|
|
@@ -98,14 +98,10 @@ function SettingsPanel({ settings }: { settings: GitCanvasSettings }) {
|
|
|
98
98
|
<ToggleGroup id="settingRenderMode" value={settings.renderMode}
|
|
99
99
|
options={[{ value: 'canvas', label: 'Canvas' }, { value: 'dom', label: 'DOM' }]} />
|
|
100
100
|
</SettingsRow>
|
|
101
|
-
<SettingsRow label="Font Size" desc="Code font size in
|
|
101
|
+
<SettingsRow label="Font Size" desc="Code font size in full cards and editor views">
|
|
102
102
|
<Slider id="settingFontSize" valueId="fontSizeValue"
|
|
103
103
|
min={10} max={18} step={1} value={settings.fontSize} suffix="px" />
|
|
104
104
|
</SettingsRow>
|
|
105
|
-
<SettingsRow label="Popup Font Size" desc="Font size for hover popup previews">
|
|
106
|
-
<Slider id="settingPopupFontSize" valueId="popupFontSizeValue"
|
|
107
|
-
min={10} max={24} step={1} value={settings.popupFontSize} suffix="px" />
|
|
108
|
-
</SettingsRow>
|
|
109
105
|
<SettingsRow label="Card Width" desc="Character columns per card (like editors)">
|
|
110
106
|
<Slider id="settingCardWidth" valueId="cardWidthValue"
|
|
111
107
|
min={40} max={120} step={5} value={cardCols} suffix=" cols" />
|
|
@@ -146,19 +142,15 @@ function SettingsPanel({ settings }: { settings: GitCanvasSettings }) {
|
|
|
146
142
|
|
|
147
143
|
{/* Preview Mode Section */}
|
|
148
144
|
<SettingsSection title="Preview Mode">
|
|
149
|
-
<SettingsRow label="
|
|
150
|
-
<Slider id="
|
|
151
|
-
min={
|
|
152
|
-
</SettingsRow>
|
|
153
|
-
<SettingsRow label="Near Zoom Title" desc="Filename size when zoomed in within preview mode">
|
|
154
|
-
<Slider id="settingPreviewNearTitlePx" valueId="previewNearTitlePxValue"
|
|
155
|
-
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" />
|
|
156
148
|
</SettingsRow>
|
|
157
|
-
<SettingsRow label="
|
|
149
|
+
<SettingsRow label="Zoomed-out lines" desc="Minimum content lines shown at the farthest preview zoom">
|
|
158
150
|
<Slider id="settingPreviewFarLines" valueId="previewFarLinesValue"
|
|
159
151
|
min={1} max={8} step={1} value={settings.previewFarLines} suffix="" />
|
|
160
152
|
</SettingsRow>
|
|
161
|
-
<SettingsRow label="
|
|
153
|
+
<SettingsRow label="Zoomed-in lines" desc="Target content lines shown at the closest preview zoom">
|
|
162
154
|
<Slider id="settingPreviewNearLines" valueId="previewNearLinesValue"
|
|
163
155
|
min={8} max={40} step={1} value={settings.previewNearLines} suffix="" />
|
|
164
156
|
</SettingsRow>
|
|
@@ -257,13 +249,6 @@ export function openSettingsModal(ctx?: any) {
|
|
|
257
249
|
applyFontSize(parseInt(fontSlider.value));
|
|
258
250
|
});
|
|
259
251
|
|
|
260
|
-
const popupFontSlider = _modal.querySelector('#settingPopupFontSize') as HTMLInputElement;
|
|
261
|
-
const popupFontValue = _modal.querySelector('#popupFontSizeValue')!;
|
|
262
|
-
popupFontSlider?.addEventListener('input', () => {
|
|
263
|
-
popupFontValue.textContent = `${popupFontSlider.value}px`;
|
|
264
|
-
updateSettings({ popupFontSize: parseInt(popupFontSlider.value) });
|
|
265
|
-
});
|
|
266
|
-
|
|
267
252
|
const cardWidthSlider = _modal.querySelector('#settingCardWidth') as HTMLInputElement;
|
|
268
253
|
const cardWidthValue = _modal.querySelector('#cardWidthValue')!;
|
|
269
254
|
cardWidthSlider?.addEventListener('input', () => {
|
|
@@ -281,19 +266,11 @@ export function openSettingsModal(ctx?: any) {
|
|
|
281
266
|
updateSettings({ maxVisibleLines: parseInt(maxLinesSlider.value) });
|
|
282
267
|
});
|
|
283
268
|
|
|
284
|
-
const
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
updateSettings({
|
|
289
|
-
window.dispatchEvent(new CustomEvent('gitcanvas:preview-settings-changed'));
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
const previewNearTitleSlider = _modal.querySelector('#settingPreviewNearTitlePx') as HTMLInputElement;
|
|
293
|
-
const previewNearTitleValue = _modal.querySelector('#previewNearTitlePxValue')!;
|
|
294
|
-
previewNearTitleSlider?.addEventListener('input', () => {
|
|
295
|
-
previewNearTitleValue.textContent = `${previewNearTitleSlider.value}px`;
|
|
296
|
-
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) });
|
|
297
274
|
window.dispatchEvent(new CustomEvent('gitcanvas:preview-settings-changed'));
|
|
298
275
|
});
|
|
299
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 };
|