gitmaps 1.0.0 → 1.1.1

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.
Files changed (145) hide show
  1. package/README.md +265 -122
  2. package/app/[...slug]/page.client.tsx +1 -0
  3. package/app/[...slug]/page.tsx +6 -0
  4. package/app/[owner]/[repo]/page.client.tsx +5 -0
  5. package/app/[slug]/page.client.tsx +5 -0
  6. package/app/analytics.db +0 -0
  7. package/app/api/analytics/route.ts +64 -0
  8. package/app/api/auth/positions/route.ts +95 -33
  9. package/app/api/build-info/route.ts +19 -0
  10. package/app/api/chat/route.ts +13 -2
  11. package/app/api/manifest.json/route.ts +20 -0
  12. package/app/api/og-image/route.ts +14 -0
  13. package/app/api/pwa-icon/route.ts +14 -0
  14. package/app/api/repo/clone-stream/route.ts +20 -12
  15. package/app/api/repo/file-content/route.ts +73 -20
  16. package/app/api/repo/imports/route.ts +21 -3
  17. package/app/api/repo/list/route.ts +30 -0
  18. package/app/api/repo/load/route.test.ts +62 -0
  19. package/app/api/repo/load/route.ts +41 -1
  20. package/app/api/repo/pdf-thumb/route.ts +127 -0
  21. package/app/api/repo/resolve-slug/route.ts +51 -0
  22. package/app/api/repo/tree/route.ts +188 -104
  23. package/app/api/repo/upload/route.ts +6 -9
  24. package/app/api/sw.js/route.ts +70 -0
  25. package/app/api/version/route.ts +26 -0
  26. package/app/galaxy-canvas/page.client.tsx +2 -0
  27. package/app/galaxy-canvas/page.tsx +5 -0
  28. package/app/globals.css +5844 -4694
  29. package/app/icon.png +0 -0
  30. package/app/layout.tsx +1284 -467
  31. package/app/lib/auto-arrange.test.ts +158 -0
  32. package/app/lib/auto-arrange.ts +147 -0
  33. package/app/lib/canvas-export.ts +358 -358
  34. package/app/lib/canvas-text.ts +4 -72
  35. package/app/lib/canvas.ts +625 -564
  36. package/app/lib/card-arrangement.ts +21 -7
  37. package/app/lib/card-context-menu.tsx +2 -2
  38. package/app/lib/card-groups.ts +9 -2
  39. package/app/lib/cards.tsx +1361 -914
  40. package/app/lib/chat.tsx +65 -9
  41. package/app/lib/code-editor.ts +86 -2
  42. package/app/lib/connections.tsx +34 -43
  43. package/app/lib/context.test.ts +32 -0
  44. package/app/lib/context.ts +19 -3
  45. package/app/lib/cursor-sharing.ts +34 -0
  46. package/app/lib/events.tsx +76 -73
  47. package/app/lib/export-canvas.ts +287 -0
  48. package/app/lib/file-card-plugin.ts +148 -134
  49. package/app/lib/file-modal.tsx +49 -0
  50. package/app/lib/file-preview.ts +486 -400
  51. package/app/lib/github-import.test.ts +424 -0
  52. package/app/lib/global-search.ts +48 -27
  53. package/app/lib/initial-route-hydration.test.ts +283 -0
  54. package/app/lib/initial-route-hydration.ts +202 -0
  55. package/app/lib/landing-reset.test.ts +99 -0
  56. package/app/lib/landing-reset.ts +106 -0
  57. package/app/lib/landing-shell.test.ts +75 -0
  58. package/app/lib/large-repo-optimization.ts +37 -0
  59. package/app/lib/layers.tsx +17 -18
  60. package/app/lib/layout-snapshots.ts +320 -0
  61. package/app/lib/loading.test.ts +69 -0
  62. package/app/lib/loading.tsx +160 -45
  63. package/app/lib/mount-cleanup.test.ts +52 -0
  64. package/app/lib/mount-cleanup.ts +34 -0
  65. package/app/lib/mount-init.test.ts +123 -0
  66. package/app/lib/mount-init.ts +107 -0
  67. package/app/lib/mount-lifecycle.test.ts +39 -0
  68. package/app/lib/mount-lifecycle.ts +12 -0
  69. package/app/lib/mount-route-wiring.test.ts +87 -0
  70. package/app/lib/mount-route-wiring.ts +84 -0
  71. package/app/lib/multi-repo.ts +14 -0
  72. package/app/lib/onboarding-tutorial.ts +278 -0
  73. package/app/lib/perf-overlay.ts +78 -0
  74. package/app/lib/positions.ts +191 -122
  75. package/app/lib/recent-commits.test.ts +869 -0
  76. package/app/lib/recent-commits.ts +227 -0
  77. package/app/lib/repo-handoff.test.ts +23 -0
  78. package/app/lib/repo-handoff.ts +16 -0
  79. package/app/lib/repo-progressive.ts +119 -0
  80. package/app/lib/repo-select.test.ts +61 -0
  81. package/app/lib/repo-select.ts +74 -0
  82. package/app/lib/repo.tsx +1383 -977
  83. package/app/lib/role.ts +228 -0
  84. package/app/lib/route-catchall.test.ts +27 -0
  85. package/app/lib/route-repo-entry.test.ts +95 -0
  86. package/app/lib/route-repo-entry.ts +36 -0
  87. package/app/lib/router-contract.test.ts +22 -0
  88. package/app/lib/router-contract.ts +19 -0
  89. package/app/lib/shared-layout.test.ts +86 -0
  90. package/app/lib/shared-layout.ts +82 -0
  91. package/app/lib/shortcuts-panel.ts +2 -0
  92. package/app/lib/status-bar.test.ts +118 -0
  93. package/app/lib/status-bar.ts +365 -128
  94. package/app/lib/sync-controls.test.ts +43 -0
  95. package/app/lib/sync-controls.tsx +303 -0
  96. package/app/lib/test-dom.ts +145 -0
  97. package/app/lib/test-fixtures/router-contract/[...slug]/page.tsx +3 -0
  98. package/app/lib/test-fixtures/router-contract/api/health/route.ts +3 -0
  99. package/app/lib/test-fixtures/router-contract/api/version/route.ts +3 -0
  100. package/app/lib/test-fixtures/router-contract/galaxy-canvas/page.tsx +3 -0
  101. package/app/lib/test-fixtures/router-contract/page.tsx +3 -0
  102. package/app/lib/transclusion-smoke.test.ts +163 -0
  103. package/app/lib/tutorial.ts +301 -0
  104. package/app/lib/version.ts +93 -0
  105. package/app/lib/viewport-culling.ts +740 -728
  106. package/app/lib/virtual-files.ts +456 -0
  107. package/app/lib/webgl-text.ts +189 -0
  108. package/app/lib/{galaxydraw-bridge.ts → xydraw-bridge.ts} +485 -477
  109. package/app/lib/{galaxydraw.test.ts → xydraw.test.ts} +228 -229
  110. package/app/og-image.png +0 -0
  111. package/app/page.client.tsx +70 -215
  112. package/app/page.tsx +27 -92
  113. package/app/state/machine.js +13 -0
  114. package/banner.png +0 -0
  115. package/package.json +17 -8
  116. package/server.ts +11 -1
  117. package/app/api/connections/route.ts +0 -72
  118. package/app/api/positions/route.ts +0 -80
  119. package/app/api/repo/browse/route.ts +0 -55
  120. package/app/lib/pr-review.ts +0 -374
  121. package/packages/galaxydraw/README.md +0 -296
  122. package/packages/galaxydraw/banner.png +0 -0
  123. package/packages/galaxydraw/demo/build-static.ts +0 -100
  124. package/packages/galaxydraw/demo/client.ts +0 -154
  125. package/packages/galaxydraw/demo/dist/client.js +0 -8
  126. package/packages/galaxydraw/demo/index.html +0 -256
  127. package/packages/galaxydraw/demo/server.ts +0 -96
  128. package/packages/galaxydraw/dist/index.js +0 -984
  129. package/packages/galaxydraw/dist/index.js.map +0 -16
  130. package/packages/galaxydraw/node_modules/.bin/tsc.bunx +0 -0
  131. package/packages/galaxydraw/node_modules/.bin/tsc.exe +0 -0
  132. package/packages/galaxydraw/node_modules/.bin/tsserver.bunx +0 -0
  133. package/packages/galaxydraw/node_modules/.bin/tsserver.exe +0 -0
  134. package/packages/galaxydraw/package.json +0 -49
  135. package/packages/galaxydraw/perf.test.ts +0 -284
  136. package/packages/galaxydraw/src/core/cards.ts +0 -435
  137. package/packages/galaxydraw/src/core/engine.ts +0 -339
  138. package/packages/galaxydraw/src/core/events.ts +0 -81
  139. package/packages/galaxydraw/src/core/layout.ts +0 -136
  140. package/packages/galaxydraw/src/core/minimap.ts +0 -216
  141. package/packages/galaxydraw/src/core/state.ts +0 -177
  142. package/packages/galaxydraw/src/core/viewport.ts +0 -106
  143. package/packages/galaxydraw/src/galaxydraw.css +0 -166
  144. package/packages/galaxydraw/src/index.ts +0 -40
  145. package/packages/galaxydraw/tsconfig.json +0 -30
package/app/layout.tsx CHANGED
@@ -5,489 +5,1306 @@
5
5
  * All interactivity is in page.client.tsx.
6
6
  */
7
7
 
8
+ import path from 'path';
9
+
10
+ function getBuildInfo() {
11
+ const repoRoot = path.resolve(import.meta.dir, '..');
12
+ const commitProc = Bun.spawnSync(['git', 'rev-parse', '--short', 'HEAD'], {
13
+ cwd: repoRoot,
14
+ stdout: 'pipe',
15
+ stderr: 'pipe',
16
+ });
17
+ const dateProc = Bun.spawnSync(['git', 'log', '-1', '--format=%cs'], {
18
+ cwd: repoRoot,
19
+ stdout: 'pipe',
20
+ stderr: 'pipe',
21
+ });
22
+
23
+ return {
24
+ commit: commitProc.exitCode === 0 ? commitProc.stdout.toString().trim() : 'unknown',
25
+ date: dateProc.exitCode === 0 ? dateProc.stdout.toString().trim() : '',
26
+ };
27
+ }
28
+
8
29
  export default function RootLayout({ children }: { children: any }) {
9
- return (
10
- <html lang="en">
11
- <head>
12
- <meta charSet="UTF-8" />
13
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
14
- <meta name="description" content="GitMaps - See your codebase in a new dimension. Spatial code explorer." />
15
- <title>GitMaps — Spatial Code Explorer</title>
16
- <link rel="icon" href="data:," />
17
- <link
18
- href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap"
19
- rel="stylesheet"
20
- />
21
- </head>
22
- <body>
23
- <div id="app">
24
- <nav className="sidebar">
25
- <div className="sidebar-header">
26
- <div className="logo">
27
- <svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" strokeWidth="2">
28
- <circle cx="12" cy="12" r="3" />
29
- <circle cx="12" cy="4" r="1.5" />
30
- <circle cx="12" cy="20" r="1.5" />
31
- <circle cx="4" cy="8" r="1.5" />
32
- <circle cx="20" cy="8" r="1.5" />
33
- <circle cx="4" cy="16" r="1.5" />
34
- <circle cx="20" cy="16" r="1.5" />
35
- <path d="M12 7v2M12 15v2M8.5 9.5l-2.5-1M15.5 9.5l2.5-1M8.5 14.5l-2.5 1M15.5 14.5l2.5 1" />
36
- </svg>
37
- <span>GitMaps</span>
38
- </div>
39
- </div>
30
+ const build = getBuildInfo();
40
31
 
41
- <div className="repo-selector">
42
- <select id="repoSelect" className="repo-dropdown">
43
- <option value="">Select a repository...</option>
44
- </select>
45
- <input type="text" id="repoPath" style={{ display: 'none' }} />
46
- <input type="file" id="folderPickerInput" style={{ display: 'none' }} />
47
- <div className="clone-status" id="cloneStatus" style={{ display: 'none' }}></div>
32
+ return (
33
+ <html lang="en">
34
+ <head>
35
+ <meta charSet="UTF-8" />
36
+ <meta
37
+ name="viewport"
38
+ content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
39
+ />
40
+ <meta
41
+ name="description"
42
+ content="Transcend the file tree. GitMaps renders knowledge on an infinite canvas — with layers, time-travel, and a minimap to never lose context."
43
+ />
44
+ <meta property="og:title" content="GitMaps — Spatial Code Explorer" />
45
+ <meta
46
+ property="og:description"
47
+ content="Every file in your repo. One infinite canvas. Layers, git time-travel, inline diffs."
48
+ />
49
+ <meta property="og:image" content="https://gitmaps.xyz/api/og-image" />
50
+ <meta property="og:url" content="https://gitmaps.xyz" />
51
+ <meta name="twitter:card" content="summary_large_image" />
52
+ <meta name="twitter:image" content="https://gitmaps.xyz/api/og-image" />
53
+ <title>GitMaps — Spatial Code Explorer</title>
54
+ <link rel="icon" type="image/png" href="/api/pwa-icon" />
55
+ <link rel="manifest" href="/api/manifest.json" />
56
+ <meta name="theme-color" content="#7c3aed" />
57
+ <script
58
+ dangerouslySetInnerHTML={{
59
+ __html: `window.__GITMAPS_BUILD__ = ${JSON.stringify(build)};`,
60
+ }}
61
+ />
62
+ <link
63
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap"
64
+ rel="stylesheet"
65
+ />
66
+ </head>
67
+ <body>
68
+ {/* Mobile gate — canvas needs a real screen */}
69
+ <div
70
+ id="mobileGate"
71
+ style={{
72
+ display: "none",
73
+ position: "fixed",
74
+ inset: 0,
75
+ zIndex: 99999,
76
+ background: "#0a0a1a",
77
+ color: "#e2e8f0",
78
+ flexDirection: "column",
79
+ alignItems: "center",
80
+ justifyContent: "center",
81
+ padding: "2rem",
82
+ textAlign: "center",
83
+ fontFamily: "Inter, sans-serif",
84
+ gap: "1rem",
85
+ }}
86
+ >
87
+ <div style={{ fontSize: "3rem" }}>🗺️</div>
88
+ <h2 style={{ margin: 0, fontSize: "1.5rem", fontWeight: 600 }}>
89
+ GitMaps needs a bigger screen
90
+ </h2>
91
+ <p
92
+ style={{
93
+ margin: 0,
94
+ color: "#94a3b8",
95
+ maxWidth: "320px",
96
+ lineHeight: 1.6,
97
+ }}
98
+ >
99
+ The infinite canvas works best on desktop or tablet. Come back on a
100
+ larger screen to explore your repos spatially.
101
+ </p>
102
+ <a
103
+ href="https://gitmaps.xyz"
104
+ style={{
105
+ marginTop: "0.5rem",
106
+ padding: "0.75rem 1.5rem",
107
+ background: "#7c3aed",
108
+ color: "white",
109
+ borderRadius: "8px",
110
+ textDecoration: "none",
111
+ fontWeight: 500,
112
+ }}
113
+ >
114
+ Learn more
115
+ </a>
116
+ <button
117
+ id="mobileGateDismiss"
118
+ style={{
119
+ background: "none",
120
+ border: "1px solid #334155",
121
+ color: "#94a3b8",
122
+ padding: "0.5rem 1rem",
123
+ borderRadius: "6px",
124
+ cursor: "pointer",
125
+ fontSize: "0.85rem",
126
+ }}
127
+ >
128
+ Continue anyway →
129
+ </button>
130
+ </div>
131
+ <script
132
+ dangerouslySetInnerHTML={{
133
+ __html: `
134
+ (function() {
135
+ window.__GITMAPS_BUILD_COMMIT__ = ${JSON.stringify(build.commit)};
136
+ window.__GITMAPS_BUILD_DATE__ = ${JSON.stringify(build.date)};
137
+ if (window.innerWidth < 768) {
138
+ var g = document.getElementById('mobileGate');
139
+ if (g) { g.style.display = 'flex'; }
140
+ var d = document.getElementById('mobileGateDismiss');
141
+ if (d) d.onclick = function() { g.style.display = 'none'; };
142
+ }
143
+ })();
144
+ `,
145
+ }}
146
+ />
147
+ <div id="app">
148
+ <nav className="sidebar">
149
+ <div className="sidebar-header">
150
+ <div className="logo">
151
+ <svg
152
+ viewBox="0 0 24 24"
153
+ width="22"
154
+ height="22"
155
+ fill="none"
156
+ stroke="currentColor"
157
+ strokeWidth="2"
158
+ >
159
+ <circle cx="12" cy="12" r="3" />
160
+ <circle cx="12" cy="4" r="1.5" />
161
+ <circle cx="12" cy="20" r="1.5" />
162
+ <circle cx="4" cy="8" r="1.5" />
163
+ <circle cx="20" cy="8" r="1.5" />
164
+ <circle cx="4" cy="16" r="1.5" />
165
+ <circle cx="20" cy="16" r="1.5" />
166
+ <path d="M12 7v2M12 15v2M8.5 9.5l-2.5-1M15.5 9.5l2.5-1M8.5 14.5l-2.5 1M15.5 14.5l2.5 1" />
167
+ </svg>
168
+ <span>GitMaps</span>
169
+ </div>
170
+ </div>
48
171
 
49
- <button id="githubImportBtn" className="github-import-btn" title="Import from GitHub">
50
- <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
51
- <path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
52
- </svg>
53
- Import from GitHub
54
- </button>
55
- </div>
172
+ <div className="repo-selector">
173
+ <select id="repoSelect" className="repo-dropdown">
174
+ <option value="">Select a repository...</option>
175
+ </select>
176
+ <input type="text" id="repoPath" style={{ display: "none" }} />
177
+ <input
178
+ type="file"
179
+ id="folderPickerInput"
180
+ style={{ display: "none" }}
181
+ />
182
+ <div
183
+ className="clone-status"
184
+ id="cloneStatus"
185
+ style={{ display: "none" }}
186
+ ></div>
56
187
 
57
- <div id="repoTabs" style={{ display: 'none', gap: '6px', padding: '0 12px 8px', flexWrap: 'wrap' }}></div>
188
+ <button
189
+ id="githubImportBtn"
190
+ className="github-import-btn"
191
+ title="Import from GitHub"
192
+ >
193
+ <svg
194
+ viewBox="0 0 24 24"
195
+ width="14"
196
+ height="14"
197
+ fill="currentColor"
198
+ >
199
+ <path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
200
+ </svg>
201
+ Import from GitHub
202
+ </button>
203
+ </div>
58
204
 
59
- <div className="commit-timeline" id="commitTimeline">
60
- <div className="section-header">
61
- <span className="section-title">History</span>
62
- <span className="badge" id="commitCount">0</span>
63
- </div>
64
- <div className="timeline-container" id="timelineContainer">
65
- <div className="empty-state">
66
- <svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" strokeWidth="1.5" opacity="0.3">
67
- <circle cx="12" cy="12" r="10" />
68
- <path d="M12 6v6l4 2" />
69
- </svg>
70
- <p>Load a repository</p>
71
- </div>
72
- </div>
73
- </div>
205
+ <div
206
+ id="repoTabs"
207
+ style={{
208
+ display: "none",
209
+ gap: "6px",
210
+ padding: "0 12px 8px",
211
+ flexWrap: "wrap",
212
+ }}
213
+ ></div>
74
214
 
75
- <div className="sidebar-bottom">
76
- <div className="canvas-controls">
77
- <div className="control-row">
78
- <button id="resetView" className="btn-ghost" title="Reset View">
79
- <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2">
80
- <path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
81
- <path d="M3 3v5h5" />
82
- </svg>
83
- </button>
84
- <button id="fitAll" className="btn-ghost" title="Fit All">
85
- <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2">
86
- <path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7" />
87
- </svg>
88
- </button>
89
- <div className="zoom-inline">
90
- <input type="range" id="zoomSlider" min="0.1" max="3" step="0.1" defaultValue="1" />
91
- <span id="zoomValue">100%</span>
92
- </div>
93
- </div>
94
- </div>
215
+ <div className="commit-timeline" id="commitTimeline">
216
+ <div className="section-header">
217
+ <span className="section-title">History</span>
218
+ <span className="badge" id="commitCount">
219
+ 0
220
+ </span>
221
+ <button
222
+ id="pullBtn"
223
+ className="btn-ghost btn-xs"
224
+ title="Pull latest commits from remote"
225
+ style={{ marginLeft: "auto" }}
226
+ >
227
+ <svg
228
+ viewBox="0 0 24 24"
229
+ width="12"
230
+ height="12"
231
+ fill="none"
232
+ stroke="currentColor"
233
+ strokeWidth="2.5"
234
+ >
235
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
236
+ <polyline points="7 10 12 15 17 10" />
237
+ <line x1="12" y1="15" x2="12" y2="3" />
238
+ </svg>
239
+ Pull
240
+ </button>
241
+ </div>
242
+ <div className="timeline-container" id="timelineContainer">
243
+ <div className="empty-state">
244
+ <svg
245
+ viewBox="0 0 24 24"
246
+ width="24"
247
+ height="24"
248
+ fill="none"
249
+ stroke="currentColor"
250
+ strokeWidth="1.5"
251
+ opacity="0.3"
252
+ >
253
+ <circle cx="12" cy="12" r="10" />
254
+ <path d="M12 6v6l4 2" />
255
+ </svg>
256
+ </div>
257
+ </div>
258
+ </div>
259
+
260
+
261
+
262
+ <div className="sidebar-bottom">
263
+ <div className="canvas-controls">
264
+ <div className="control-row">
265
+ <button
266
+ id="resetView"
267
+ className="btn-ghost"
268
+ title="Reset View"
269
+ >
270
+ <svg
271
+ viewBox="0 0 24 24"
272
+ width="14"
273
+ height="14"
274
+ fill="none"
275
+ stroke="currentColor"
276
+ strokeWidth="2"
277
+ >
278
+ <path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
279
+ <path d="M3 3v5h5" />
280
+ </svg>
281
+ </button>
282
+ <button id="fitAll" className="btn-ghost" title="Fit All">
283
+ <svg
284
+ viewBox="0 0 24 24"
285
+ width="14"
286
+ height="14"
287
+ fill="none"
288
+ stroke="currentColor"
289
+ strokeWidth="2"
290
+ >
291
+ <path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7" />
292
+ </svg>
293
+ </button>
294
+ <div className="zoom-inline">
295
+ <input
296
+ type="range"
297
+ id="zoomSlider"
298
+ min="0.1"
299
+ max="3"
300
+ step="0.1"
301
+ defaultValue="1"
302
+ />
303
+ <span id="zoomValue">100%</span>
304
+ </div>
305
+ </div>
306
+ </div>
95
307
 
96
- <div className="hotkey-toggle-wrapper">
97
- <button id="hotkeyToggle" className="btn-ghost hotkey-toggle" title="Keyboard shortcuts">
98
- <span>?</span>
99
- </button>
100
- <div className="hotkey-popup" id="hotkeyPopup">
101
- <div className="hotkey-popup-title">Keyboard Shortcuts</div>
102
- <div className="hotkey-grid">
103
- <div className="hk"><kbd>Scroll</kbd> Zoom</div>
104
- <div className="hk"><kbd>Space+Drag</kbd> Pan</div>
105
- <div className="hk"><kbd>Click</kbd> Select</div>
106
- <div className="hk"><kbd>Shift+Click</kbd> Multi-select</div>
107
- <div className="hk"><kbd>Drag canvas</kbd> Rect select</div>
108
- <div className="hk"><kbd>Drag card</kbd> Move</div>
109
- <div className="hk"><kbd>Del</kbd> Hide file</div>
110
- <div className="hk"><kbd>H</kbd> Arrange row</div>
111
- <div className="hk"><kbd>V</kbd> Arrange column</div>
112
- <div className="hk"><kbd>G</kbd> Arrange grid</div>
113
- <div className="hk"><kbd>W</kbd> Fit to screen</div>
114
- <div className="hk"><kbd>Ctrl+F</kbd> Search across files</div>
115
- <div className="hk"><kbd>Ctrl+O</kbd> Find file</div>
116
- <div className="hk"><kbd>Ctrl +/-</kbd> Text zoom</div>
117
- <div className="hk"><kbd>Dbl-click</kbd> Open editor</div>
118
- <div className="hk"><kbd>Alt+Click</kbd> Connect lines</div>
119
- <div className="hk"><kbd>Arrow keys</kbd> Prev/next commit</div>
120
- <div className="hk"><kbd>Ctrl+N</kbd> New file</div>
121
- <div className="hk"><kbd>Ctrl+S</kbd> Save (in editor)</div>
122
- </div>
123
- </div>
124
- </div>
125
- </div>
126
- </nav>
308
+ <div className="hotkey-toggle-wrapper">
309
+ <button
310
+ id="hotkeyToggle"
311
+ className="btn-ghost hotkey-toggle"
312
+ title="Keyboard shortcuts"
313
+ >
314
+ <span>?</span>
315
+ </button>
316
+ <div className="hotkey-popup" id="hotkeyPopup">
317
+ <div className="hotkey-popup-title">Keyboard Shortcuts</div>
318
+ <div className="hotkey-grid">
319
+ <div className="hk">
320
+ <kbd>Scroll</kbd> Zoom
321
+ </div>
322
+ <div className="hk">
323
+ <kbd>Space+Drag</kbd> Pan
324
+ </div>
325
+ <div className="hk">
326
+ <kbd>Click</kbd> Select
327
+ </div>
328
+ <div className="hk">
329
+ <kbd>Shift+Click</kbd> Multi-select
330
+ </div>
331
+ <div className="hk">
332
+ <kbd>Drag canvas</kbd> Rect select
333
+ </div>
334
+ <div className="hk">
335
+ <kbd>Drag card</kbd> Move
336
+ </div>
337
+ <div className="hk">
338
+ <kbd>Del</kbd> Hide file
339
+ </div>
340
+ <div className="hk">
341
+ <kbd>H</kbd> Arrange row
342
+ </div>
343
+ <div className="hk">
344
+ <kbd>V</kbd> Arrange column
345
+ </div>
346
+ <div className="hk">
347
+ <kbd>G</kbd> Arrange grid
348
+ </div>
349
+ <div className="hk">
350
+ <kbd>W</kbd> Fit to screen
351
+ </div>
352
+ <div className="hk">
353
+ <kbd>Ctrl+F</kbd> Search across files
354
+ </div>
355
+ <div className="hk">
356
+ <kbd>Ctrl+O</kbd> Find file
357
+ </div>
358
+ <div className="hk">
359
+ <kbd>Ctrl +/-</kbd> Text zoom
360
+ </div>
361
+ <div className="hk">
362
+ <kbd>Dbl-click</kbd> Open editor
363
+ </div>
364
+ <div className="hk">
365
+ <kbd>Alt+Click</kbd> Connect lines
366
+ </div>
367
+ <div className="hk">
368
+ <kbd>Arrow keys</kbd> Prev/next commit
369
+ </div>
370
+ <div className="hk">
371
+ <kbd>Ctrl+N</kbd> New file
372
+ </div>
373
+ <div className="hk">
374
+ <kbd>Ctrl+S</kbd> Save (in editor)
375
+ </div>
376
+ </div>
377
+ </div>
378
+ </div>
379
+ </div>
380
+ </nav>
127
381
 
128
- <main className="canvas-area">
129
- <div className="canvas-header">
130
- <div className="header-left">
131
- <div className="current-commit" id="currentCommitInfo">
132
- <span className="commit-hash-label">No commit selected</span>
133
- </div>
134
- </div>
135
- <div className="header-right">
136
- <div className="arrange-toolbar" id="arrangeToolbar" style={{ display: 'none' }}>
137
- <span className="arrange-label">Arrange:</span>
138
- <button id="arrangeRow" className="btn-ghost btn-xs" title="Arrange in row (H)">
139
- <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2">
140
- <rect x="2" y="7" width="5" height="10" rx="1" />
141
- <rect x="9.5" y="7" width="5" height="10" rx="1" />
142
- <rect x="17" y="7" width="5" height="10" rx="1" />
143
- </svg>
144
- </button>
145
- <button id="arrangeCol" className="btn-ghost btn-xs" title="Arrange in column (V)">
146
- <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2">
147
- <rect x="4" y="2" width="16" height="5" rx="1" />
148
- <rect x="4" y="9.5" width="16" height="5" rx="1" />
149
- <rect x="4" y="17" width="16" height="5" rx="1" />
150
- </svg>
151
- </button>
152
- <button id="arrangeGrid" className="btn-ghost btn-xs" title="Arrange in grid (G)">
153
- <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2">
154
- <rect x="3" y="3" width="7" height="7" rx="1" />
155
- <rect x="14" y="3" width="7" height="7" rx="1" />
156
- <rect x="3" y="14" width="7" height="7" rx="1" />
157
- <rect x="14" y="14" width="7" height="7" rx="1" />
158
- </svg>
159
- </button>
160
- <div style={{ width: 1, height: 16, background: 'var(--border)', margin: '0 4px' }}></div>
161
- <button id="arrangeFit" className="btn-ghost btn-xs" title="Reset Size (W)">
162
- <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2">
163
- <path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7" />
164
- </svg>
165
- </button>
166
- <button id="arrangeAI" className="btn-ghost btn-xs" title="Explain with AI...">
167
- <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
168
- <path d="M11 2h2v4h-2zm0 16h2v4h-2zm11-7v2h-4v-2zm-16 0v2H2v-2zm12.3-5.3l1.4 1.4-2.8 2.8-1.4-1.4zm-9.8 9.8l1.4 1.4-2.8 2.8-1.4-1.4z" />
169
- </svg>
170
- </button>
171
- </div>
172
- <button id="toggleChangedFiles" className="btn-ghost btn-sm" title="Show changed files">
173
- <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2">
174
- <path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" />
175
- <polyline points="14 2 14 8 20 8" />
176
- </svg>
177
- <span id="fileCount">0</span>
178
- </button>
179
- <button id="showHidden" className="btn-ghost btn-sm" title="Show hidden files" style={{ display: 'none' }}>
180
- <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2">
181
- <path d="M17.94 17.94A10.07 10.07 0 0112 20c-7 0-11-8-11-8a18.45 18.45 0 015.06-5.94" />
182
- <path d="M9.9 4.24A9.12 9.12 0 0112 4c7 0 11 8 11 8a18.5 18.5 0 01-2.16 3.19" />
183
- <line x1="1" y1="1" x2="23" y2="23" />
184
- </svg>
185
- <span id="hiddenCount">0</span>
186
- </button>
187
- <button id="toggleConnections" className="btn-ghost btn-sm" title="Toggle connection lines">
188
- <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2">
189
- <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
190
- <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
191
- </svg>
192
- </button>
193
- <button id="dep-graph-btn" className="btn-ghost btn-sm" title="Toggle dependency graph (Ctrl+G)">
194
- <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2">
195
- <circle cx="5" cy="5" r="2.5" /><circle cx="19" cy="5" r="2.5" />
196
- <circle cx="12" cy="19" r="2.5" /><line x1="7" y1="6" x2="17" y2="6" />
197
- <line x1="6" y1="7" x2="11" y2="17" /><line x1="18" y1="7" x2="13" y2="17" />
198
- </svg>
199
- </button>
200
- <button id="toggleCanvasText" className="btn-ghost btn-sm" title="Toggle text rendering mode (DOM vs WebGL/Canvas)">
201
- <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
202
- <polyline points="4 7 4 4 20 4 20 7" />
203
- <line x1="9" y1="20" x2="15" y2="20" />
204
- <line x1="12" y1="4" x2="12" y2="20" />
205
- </svg>
206
- </button>
207
- <button id="autoDetectImports" className="btn-ghost btn-sm" title="Auto-detect import connections">
208
- <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2">
209
- <path d="M9 3H5a2 2 0 00-2 2v4m6-6h10a2 2 0 012 2v4M9 3v18m0 0h10a2 2 0 002-2v-4M9 21H5a2 2 0 01-2-2v-4" />
210
- <path d="M14 9l2 2-2 2" />
211
- </svg>
212
- </button>
213
- <button id="shareLayout" className="btn-ghost btn-sm" title="Share Layout (Copy URL)">
214
- <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2">
215
- <circle cx="18" cy="5" r="3" />
216
- <circle cx="6" cy="12" r="3" />
217
- <circle cx="18" cy="19" r="3" />
218
- <line x1="8.59" y1="13.51" x2="15.42" y2="17.49" />
219
- <line x1="15.41" y1="6.51" x2="8.59" y2="10.49" />
220
- </svg>
221
- </button>
222
- <button id="helpOnboarding" className="btn-ghost btn-sm" title="Replay Tutorial (?)">
223
- <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2">
224
- <circle cx="12" cy="12" r="10" />
225
- <path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
226
- <line x1="12" y1="17" x2="12.01" y2="17" />
227
- </svg>
228
- </button>
229
- <button id="toggleControlMode" className="btn-ghost btn-sm" title="Toggle control mode: Simple (drag=pan) / Advanced (space+drag=pan)">
230
- <svg id="controlModeIcon" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2">
231
- {/* Default: Advanced (crosshair) — gets swapped by JS */}
232
- <circle cx="12" cy="12" r="10" />
233
- <line x1="12" y1="2" x2="12" y2="6" />
234
- <line x1="12" y1="18" x2="12" y2="22" />
235
- <line x1="2" y1="12" x2="6" y2="12" />
236
- <line x1="18" y1="12" x2="22" y2="12" />
237
- </svg>
238
- </button>
239
- <button id="openGlobalSearch" className="btn-ghost btn-sm" title="Search Files (Ctrl+F)">
240
- <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
241
- <circle cx="11" cy="11" r="8" /><line x1="21" y1="21" x2="16.65" y2="16.65" />
242
- </svg>
243
- </button>
244
- <button id="openBranchCompare" className="btn-ghost btn-sm" title="Compare Branches">
245
- <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
246
- <circle cx="18" cy="18" r="3" /><circle cx="6" cy="6" r="3" />
247
- <path d="M13 6h3a2 2 0 0 1 2 2v7" />
248
- <path d="M6 9v12" />
249
- </svg>
250
- </button>
251
- <button id="openSettings" className="btn-ghost btn-sm" title="Settings">
252
- <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
253
- <circle cx="12" cy="12" r="3" />
254
- <path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z" />
255
- </svg>
256
- </button>
257
- <button id="toggleCanvasChat" className="btn-ghost btn-sm ai-chat-btn" title="AI Chat">
258
- <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2">
259
- <path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z" />
260
- </svg>
261
- AI
262
- </button>
263
- </div>
264
- </div>
382
+ <main className="canvas-area">
383
+ <div className="canvas-header">
384
+ <div className="header-left">
385
+ <div className="current-commit" id="currentCommitInfo">
386
+ <span className="commit-hash-label">No commit selected</span>
387
+ </div>
388
+ </div>
389
+ <div className="header-right">
390
+ <div
391
+ className="arrange-toolbar"
392
+ id="arrangeToolbar"
393
+ style={{ display: "none" }}
394
+ >
395
+ <span className="arrange-label">Arrange:</span>
396
+ <button
397
+ id="arrangeRow"
398
+ className="btn-ghost btn-xs"
399
+ title="Arrange in row (H)"
400
+ >
401
+ <svg
402
+ viewBox="0 0 24 24"
403
+ width="14"
404
+ height="14"
405
+ fill="none"
406
+ stroke="currentColor"
407
+ strokeWidth="2"
408
+ >
409
+ <rect x="2" y="7" width="5" height="10" rx="1" />
410
+ <rect x="9.5" y="7" width="5" height="10" rx="1" />
411
+ <rect x="17" y="7" width="5" height="10" rx="1" />
412
+ </svg>
413
+ </button>
414
+ <button
415
+ id="arrangeCol"
416
+ className="btn-ghost btn-xs"
417
+ title="Arrange in column (V)"
418
+ >
419
+ <svg
420
+ viewBox="0 0 24 24"
421
+ width="14"
422
+ height="14"
423
+ fill="none"
424
+ stroke="currentColor"
425
+ strokeWidth="2"
426
+ >
427
+ <rect x="4" y="2" width="16" height="5" rx="1" />
428
+ <rect x="4" y="9.5" width="16" height="5" rx="1" />
429
+ <rect x="4" y="17" width="16" height="5" rx="1" />
430
+ </svg>
431
+ </button>
432
+ <button
433
+ id="arrangeGrid"
434
+ className="btn-ghost btn-xs"
435
+ title="Arrange in grid (G)"
436
+ >
437
+ <svg
438
+ viewBox="0 0 24 24"
439
+ width="14"
440
+ height="14"
441
+ fill="none"
442
+ stroke="currentColor"
443
+ strokeWidth="2"
444
+ >
445
+ <rect x="3" y="3" width="7" height="7" rx="1" />
446
+ <rect x="14" y="3" width="7" height="7" rx="1" />
447
+ <rect x="3" y="14" width="7" height="7" rx="1" />
448
+ <rect x="14" y="14" width="7" height="7" rx="1" />
449
+ </svg>
450
+ </button>
451
+ <div
452
+ style={{
453
+ width: 1,
454
+ height: 16,
455
+ background: "var(--border)",
456
+ margin: "0 4px",
457
+ }}
458
+ ></div>
459
+ <button
460
+ id="arrangeFit"
461
+ className="btn-ghost btn-xs"
462
+ title="Reset Size (W)"
463
+ >
464
+ <svg
465
+ viewBox="0 0 24 24"
466
+ width="14"
467
+ height="14"
468
+ fill="none"
469
+ stroke="currentColor"
470
+ strokeWidth="2"
471
+ >
472
+ <path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7" />
473
+ </svg>
474
+ </button>
475
+ <button
476
+ id="arrangeAI"
477
+ className="btn-ghost btn-xs"
478
+ title="Explain with AI..."
479
+ >
480
+ <svg
481
+ viewBox="0 0 24 24"
482
+ width="14"
483
+ height="14"
484
+ fill="currentColor"
485
+ >
486
+ <path d="M11 2h2v4h-2zm0 16h2v4h-2zm11-7v2h-4v-2zm-16 0v2H2v-2zm12.3-5.3l1.4 1.4-2.8 2.8-1.4-1.4zm-9.8 9.8l1.4 1.4-2.8 2.8-1.4-1.4z" />
487
+ </svg>
488
+ </button>
489
+ </div>
490
+ <button
491
+ id="toggleChangedFiles"
492
+ className="btn-ghost btn-sm"
493
+ title="Show changed files"
494
+ >
495
+ <svg
496
+ viewBox="0 0 24 24"
497
+ width="14"
498
+ height="14"
499
+ fill="none"
500
+ stroke="currentColor"
501
+ strokeWidth="2"
502
+ >
503
+ <path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" />
504
+ <polyline points="14 2 14 8 20 8" />
505
+ </svg>
506
+ <span id="fileCount">0</span>
507
+ </button>
508
+ <button
509
+ id="showHidden"
510
+ className="btn-ghost btn-sm"
511
+ title="Show hidden files"
512
+ style={{ display: "none" }}
513
+ >
514
+ <svg
515
+ viewBox="0 0 24 24"
516
+ width="14"
517
+ height="14"
518
+ fill="none"
519
+ stroke="currentColor"
520
+ strokeWidth="2"
521
+ >
522
+ <path d="M17.94 17.94A10.07 10.07 0 0112 20c-7 0-11-8-11-8a18.45 18.45 0 015.06-5.94" />
523
+ <path d="M9.9 4.24A9.12 9.12 0 0112 4c7 0 11 8 11 8a18.5 18.5 0 01-2.16 3.19" />
524
+ <line x1="1" y1="1" x2="23" y2="23" />
525
+ </svg>
526
+ <span id="hiddenCount">0</span>
527
+ </button>
528
+ <button
529
+ id="toggleConnections"
530
+ className="btn-ghost btn-sm"
531
+ title="Toggle connection lines"
532
+ >
533
+ <svg
534
+ viewBox="0 0 24 24"
535
+ width="14"
536
+ height="14"
537
+ fill="none"
538
+ stroke="currentColor"
539
+ strokeWidth="2"
540
+ >
541
+ <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
542
+ <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
543
+ </svg>
544
+ </button>
545
+ <button
546
+ id="dep-graph-btn"
547
+ className="btn-ghost btn-sm"
548
+ title="Toggle dependency graph (Ctrl+G)"
549
+ >
550
+ <svg
551
+ viewBox="0 0 24 24"
552
+ width="14"
553
+ height="14"
554
+ fill="none"
555
+ stroke="currentColor"
556
+ strokeWidth="2"
557
+ >
558
+ <circle cx="5" cy="5" r="2.5" />
559
+ <circle cx="19" cy="5" r="2.5" />
560
+ <circle cx="12" cy="19" r="2.5" />
561
+ <line x1="7" y1="6" x2="17" y2="6" />
562
+ <line x1="6" y1="7" x2="11" y2="17" />
563
+ <line x1="18" y1="7" x2="13" y2="17" />
564
+ </svg>
565
+ </button>
566
+ <button
567
+ id="toggleCanvasText"
568
+ className="btn-ghost btn-sm"
569
+ title="Toggle text rendering mode (DOM vs WebGL/Canvas)"
570
+ >
571
+ <svg
572
+ viewBox="0 0 24 24"
573
+ width="16"
574
+ height="16"
575
+ fill="none"
576
+ stroke="currentColor"
577
+ strokeWidth="2"
578
+ strokeLinecap="round"
579
+ strokeLinejoin="round"
580
+ >
581
+ <polyline points="4 7 4 4 20 4 20 7" />
582
+ <line x1="9" y1="20" x2="15" y2="20" />
583
+ <line x1="12" y1="4" x2="12" y2="20" />
584
+ </svg>
585
+ </button>
586
+ <button
587
+ id="autoDetectImports"
588
+ className="btn-ghost btn-sm"
589
+ title="Auto-detect import connections"
590
+ >
591
+ <svg
592
+ viewBox="0 0 24 24"
593
+ width="14"
594
+ height="14"
595
+ fill="none"
596
+ stroke="currentColor"
597
+ strokeWidth="2"
598
+ >
599
+ <path d="M9 3H5a2 2 0 00-2 2v4m6-6h10a2 2 0 012 2v4M9 3v18m0 0h10a2 2 0 002-2v-4M9 21H5a2 2 0 01-2-2v-4" />
600
+ <path d="M14 9l2 2-2 2" />
601
+ </svg>
602
+ </button>
603
+ <button
604
+ id="shareLayout"
605
+ className="btn-ghost btn-sm"
606
+ title="Share Layout (Copy URL)"
607
+ >
608
+ <svg
609
+ viewBox="0 0 24 24"
610
+ width="14"
611
+ height="14"
612
+ fill="none"
613
+ stroke="currentColor"
614
+ strokeWidth="2"
615
+ >
616
+ <circle cx="18" cy="5" r="3" />
617
+ <circle cx="6" cy="12" r="3" />
618
+ <circle cx="18" cy="19" r="3" />
619
+ <line x1="8.59" y1="13.51" x2="15.42" y2="17.49" />
620
+ <line x1="15.41" y1="6.51" x2="8.59" y2="10.49" />
621
+ </svg>
622
+ </button>
623
+ <button
624
+ id="toggleControlMode"
625
+ className="btn-ghost btn-sm"
626
+ title="Toggle control mode: Simple (drag=pan) / Advanced (space+drag=pan)"
627
+ >
628
+ <svg
629
+ id="controlModeIcon"
630
+ viewBox="0 0 24 24"
631
+ width="14"
632
+ height="14"
633
+ fill="none"
634
+ stroke="currentColor"
635
+ strokeWidth="2"
636
+ >
637
+ {/* Default: Advanced (crosshair) — gets swapped by JS */}
638
+ <circle cx="12" cy="12" r="10" />
639
+ <line x1="12" y1="2" x2="12" y2="6" />
640
+ <line x1="12" y1="18" x2="12" y2="22" />
641
+ <line x1="2" y1="12" x2="6" y2="12" />
642
+ <line x1="18" y1="12" x2="22" y2="12" />
643
+ </svg>
644
+ </button>
645
+ <button
646
+ id="openSnapshots"
647
+ className="btn-ghost btn-sm"
648
+ title="Layout Snapshots"
649
+ >
650
+ <svg
651
+ viewBox="0 0 24 24"
652
+ width="14"
653
+ height="14"
654
+ fill="none"
655
+ stroke="currentColor"
656
+ strokeWidth="2"
657
+ strokeLinecap="round"
658
+ strokeLinejoin="round"
659
+ >
660
+ <path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z" />
661
+ <circle cx="12" cy="13" r="4" />
662
+ <path d="M15 8h.01" />
663
+ </svg>
664
+ </button>
665
+ <button
666
+ id="openGlobalSearch"
667
+ className="btn-ghost btn-sm"
668
+ title="Search Files (Ctrl+F)"
669
+ >
670
+ <svg
671
+ viewBox="0 0 24 24"
672
+ width="14"
673
+ height="14"
674
+ fill="none"
675
+ stroke="currentColor"
676
+ strokeWidth="2"
677
+ strokeLinecap="round"
678
+ strokeLinejoin="round"
679
+ >
680
+ <circle cx="11" cy="11" r="8" />
681
+ <line x1="21" y1="21" x2="16.65" y2="16.65" />
682
+ </svg>
683
+ </button>
684
+ <button
685
+ id="openBranchCompare"
686
+ className="btn-ghost btn-sm"
687
+ title="Compare Branches"
688
+ >
689
+ <svg
690
+ viewBox="0 0 24 24"
691
+ width="14"
692
+ height="14"
693
+ fill="none"
694
+ stroke="currentColor"
695
+ strokeWidth="2"
696
+ strokeLinecap="round"
697
+ strokeLinejoin="round"
698
+ >
699
+ <circle cx="18" cy="18" r="3" />
700
+ <circle cx="6" cy="6" r="3" />
701
+ <path d="M13 6h3a2 2 0 0 1 2 2v7" />
702
+ <path d="M6 9v12" />
703
+ </svg>
704
+ </button>
705
+ <button
706
+ id="openSettings"
707
+ className="btn-ghost btn-sm"
708
+ title="Settings"
709
+ >
710
+ <svg
711
+ viewBox="0 0 24 24"
712
+ width="14"
713
+ height="14"
714
+ fill="none"
715
+ stroke="currentColor"
716
+ strokeWidth="2"
717
+ strokeLinecap="round"
718
+ strokeLinejoin="round"
719
+ >
720
+ <circle cx="12" cy="12" r="3" />
721
+ <path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z" />
722
+ </svg>
723
+ </button>
724
+ <button
725
+ id="toggleCanvasChat"
726
+ className="btn-ghost btn-sm ai-chat-btn"
727
+ title="AI Chat"
728
+ >
729
+ <svg
730
+ viewBox="0 0 24 24"
731
+ width="14"
732
+ height="14"
733
+ fill="none"
734
+ stroke="currentColor"
735
+ strokeWidth="2"
736
+ >
737
+ <path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z" />
738
+ </svg>
739
+ AI
740
+ </button>
741
+ </div>
742
+ </div>
265
743
 
266
- {children}
744
+ {children}
267
745
 
268
- {/* Changed Files Panel */}
269
- <div className="changed-files-panel" id="changedFilesPanel" style={{ display: 'none' }}>
270
- <div className="panel-header">
271
- <span className="panel-title">Changed Files</span>
272
- <button id="closeChangedFiles" className="btn-ghost btn-xs" title="Close">
273
- <svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" strokeWidth="2.5">
274
- <line x1="18" y1="6" x2="6" y2="18" />
275
- <line x1="6" y1="6" x2="18" y2="18" />
276
- </svg>
277
- </button>
278
- </div>
279
- <div className="changed-files-list" id="changedFilesList"></div>
280
- </div>
746
+ {/* Changed Files Panel */}
747
+ <div
748
+ className="changed-files-panel"
749
+ id="changedFilesPanel"
750
+ style={{ display: "none" }}
751
+ >
752
+ <div className="panel-header">
753
+ <span className="panel-title">Changed Files</span>
754
+ <button
755
+ id="closeChangedFiles"
756
+ className="btn-ghost btn-xs"
757
+ title="Close"
758
+ >
759
+ <svg
760
+ viewBox="0 0 24 24"
761
+ width="12"
762
+ height="12"
763
+ fill="none"
764
+ stroke="currentColor"
765
+ strok fill="none"
766
+ stroke="currentColor"
767
+ strokeWidth="2.5"
768
+ >
769
+ <line x1="18" y1="6" x2="6" y2="18" />
770
+ <line x1="6" y1="6" x2="18" y2="18" />
771
+ </svg>
772
+ </button>
773
+ </div>
774
+ <div className="changed-files-list" id="changedFilesList"></div>
775
+ </div>
281
776
 
777
+ {/* Connections Panel */}
778
+ <div
779
+ className="connections-panel"
780
+ id="connectionsPanel"
781
+ style={{ display: "none" }}
782
+ >
783
+ <div
784
+ className="panel-header"
785
+ style={{
786
+ display: "flex",
787
+ justifyContent: "space-between",
788
+ alignItems: "center",
789
+ padding: "12px 16px",
790
+ borderBottom: "1px solid var(--border)",
791
+ }}
792
+ >
793
+ <span
794
+ className="panel-title"
795
+ style={{
796
+ fontSize: "0.85rem",
797
+ fontWeight: 600,
798
+ color: "var(--text-primary)",
799
+ }}
800
+ >
801
+ Connections{" "}
802
+ <span
803
+ id="connCount"
804
+ style={{
805
+ marginLeft: "6px",
806
+ background: "rgba(255,255,255,0.1)",
807
+ padding: "2px 8px",
808
+ borderRadius: "12px",
809
+ fontSize: "0.75rem",
810
+ }}
811
+ >
812
+ 0
813
+ </span>
814
+ </span>
815
+ <button
816
+ id="closeConnectionsPanel"
817
+ className="btn-ghost btn-xs"
818
+ title="Close"
819
+ >
820
+ <svg
821
+ viewBox="0 0 24 24"
822
+ width="12"
823
+ height="12"
824
+ fill="none"
825
+ stroke="currentColor"
826
+ strokeWidth="2.5"
827
+ >
828
+ <line x1="18" y1="6" x2="6" y2="18" />
829
+ <line x1="6" y1="6" x2="18" y2="18" />
830
+ </svg>
831
+ </button>
832
+ </div>
833
+ <div
834
+ style={{
835
+ padding: "8px 16px",
836
+ fontSize: "0.75rem",
837
+ color: "var(--text-muted)",
838
+ borderBottom: "1px solid var(--border)",
839
+ background: "rgba(0,0,0,0.2)",
840
+ }}
841
+ >
842
+ 💡 Tip: <strong>Alt+Click</strong> any line number then select a
843
+ target to connect them.
844
+ </div>
845
+ <div
846
+ className="connections-list"
847
+ id="connectionsList"
848
+ style={{ flex: 1, overflowY: "auto" }}
849
+ ></div>
850
+ </div>
282
851
 
852
+ <div className="minimap-container">
853
+ <div className="minimap" id="minimap">
854
+ <div className="minimap-viewport" id="minimapViewport"></div>
855
+ </div>
856
+ <button
857
+ id="expandMinimap"
858
+ className="btn-ghost btn-xs minimap-expand"
859
+ title="Expand minimap"
860
+ >
861
+ <svg
862
+ viewBox="0 0 24 24"
863
+ width="11"
864
+ height="11"
865
+ fill="none"
866
+ stroke="currentColor"
867
+ strokeWidth="2"
868
+ >
869
+ <polyline points="15 3 21 3 21 9" />
870
+ <polyline points="9 21 3 21 3 15" />
871
+ <line x1="21" y1="3" x2="14" y2="10" />
872
+ <line x1="3" y1="21" x2="10" y2="14" />
873
+ </svg>
874
+ </button>
875
+ </div>
283
876
 
284
- <div className="minimap-container">
285
- <div className="minimap" id="minimap">
286
- <div className="minimap-viewport" id="minimapViewport"></div>
287
- </div>
288
- <button id="expandMinimap" className="btn-ghost btn-xs minimap-expand" title="Expand minimap">
289
- <svg viewBox="0 0 24 24" width="11" height="11" fill="none" stroke="currentColor" strokeWidth="2">
290
- <polyline points="15 3 21 3 21 9" />
291
- <polyline points="9 21 3 21 3 15" />
292
- <line x1="21" y1="3" x2="14" y2="10" />
293
- <line x1="3" y1="21" x2="10" y2="14" />
294
- </svg>
295
- </button>
296
- </div>
877
+ {/* Sticky Zoom Controls — floating pill, bottom-right */}
878
+ <div id="stickyZoomControls" className="sticky-zoom-pill">
879
+ <button id="stickyZoomOut" className="sz-btn" title="Zoom out">
880
+ <svg
881
+ viewBox="0 0 24 24"
882
+ width="14"
883
+ height="14"
884
+ fill="none"
885
+ stroke="currentColor"
886
+ strokeWidth="2.5"
887
+ >
888
+ <line x1="5" y1="12" x2="19" y2="12" />
889
+ </svg>
890
+ </button>
891
+ <input
892
+ type="range"
893
+ id="stickyZoomSlider"
894
+ className="sz-slider"
895
+ min="0.1"
896
+ max="3"
897
+ step="0.05"
898
+ defaultValue="1"
899
+ />
900
+ <button id="stickyZoomIn" className="sz-btn" title="Zoom in">
901
+ <svg
902
+ viewBox="0 0 24 24"
903
+ width="14"
904
+ height="14"
905
+ fill="none"
906
+ stroke="currentColor"
907
+ strokeWidth="2.5"
908
+ >
909
+ <line x1="12" y1="5" x2="12" y2="19" />
910
+ <line x1="5" y1="12" x2="19" y2="12" />
911
+ </svg>
912
+ </button>
913
+ <span id="stickyZoomValue" className="sz-value">
914
+ 100%
915
+ </span>
916
+ <div className="sz-divider" />
917
+ <button
918
+ id="stickyFitAll"
919
+ className="sz-btn sz-fit"
920
+ title="Fit all cards"
921
+ >
922
+ <svg
923
+ viewBox="0 0 24 24"
924
+ width="14"
925
+ height="14"
926
+ fill="none"
927
+ stroke="currentColor"
928
+ strokeWidth="2"
929
+ >
930
+ <path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7" />
931
+ </svg>
932
+ </button>
933
+ </div>
297
934
 
298
- {/* Sticky Zoom Controls — floating pill, bottom-right */}
299
- <div id="stickyZoomControls" className="sticky-zoom-pill">
300
- <button id="stickyZoomOut" className="sz-btn" title="Zoom out">
301
- <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2.5">
302
- <line x1="5" y1="12" x2="19" y2="12" />
303
- </svg>
304
- </button>
305
- <input type="range" id="stickyZoomSlider" className="sz-slider" min="0.1" max="3" step="0.05" defaultValue="1" />
306
- <button id="stickyZoomIn" className="sz-btn" title="Zoom in">
307
- <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2.5">
308
- <line x1="12" y1="5" x2="12" y2="19" />
309
- <line x1="5" y1="12" x2="19" y2="12" />
310
- </svg>
311
- </button>
312
- <span id="stickyZoomValue" className="sz-value">100%</span>
313
- <div className="sz-divider" />
314
- <button id="stickyFitAll" className="sz-btn sz-fit" title="Fit all cards">
315
- <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2">
316
- <path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7" />
317
- </svg>
318
- </button>
319
- </div>
935
+ {/* Bottom Layers Bar */}
936
+ <div id="layersBarContainer"></div>
937
+ </main>
938
+ </div>
320
939
 
321
- {/* Bottom Layers Bar */}
322
- <div id="layersBarContainer"></div>
323
- </main>
940
+ {/* File Preview Modal */}
941
+ <div className="file-preview-modal" id="filePreviewModal">
942
+ <div className="modal-backdrop"></div>
943
+ <div className="modal-content">
944
+ <div className="modal-header">
945
+ <div className="modal-header-left">
946
+ <span className="file-path" id="previewFilePath"></span>
947
+ <span
948
+ className="modal-file-status"
949
+ id="previewFileStatus"
950
+ ></span>
951
+ </div>
952
+ <div className="modal-header-right">
953
+ <div
954
+ className="modal-diff-nav"
955
+ id="modalDiffNav"
956
+ style={{
957
+ display: "none",
958
+ marginRight: "16px",
959
+ gap: "4px",
960
+ alignItems: "center",
961
+ }}
962
+ >
963
+ <button
964
+ className="btn-ghost btn-xs"
965
+ id="diffNavPrev"
966
+ title="Previous changed file (k)"
967
+ >
968
+ <svg
969
+ viewBox="0 0 24 24"
970
+ width="14"
971
+ height="14"
972
+ fill="none"
973
+ stroke="currentColor"
974
+ strokeWidth="2.5"
975
+ >
976
+ <polyline points="15 18 9 12 15 6"></polyline>
977
+ </svg>
978
+ </button>
979
+ <button
980
+ className="btn-ghost btn-xs"
981
+ id="diffNavNext"
982
+ title="Next changed file (j)"
983
+ >
984
+ <svg
985
+ viewBox="0 0 24 24"
986
+ width="14"
987
+ height="14"
988
+ fill="none"
989
+ stroke="currentColor"
990
+ strokeWidth="2.5"
991
+ >
992
+ <polyline points="9 18 15 12 9 6"></polyline>
993
+ </svg>
994
+ </button>
324
995
  </div>
325
-
326
- {/* File Preview Modal */}
327
- <div className="file-preview-modal" id="filePreviewModal">
328
- <div className="modal-backdrop"></div>
329
- <div className="modal-content">
330
- <div className="modal-header">
331
- <div className="modal-header-left">
332
- <span className="file-path" id="previewFilePath"></span>
333
- <span className="modal-line-count" id="previewLineCount"></span>
334
- <span className="modal-file-status" id="previewFileStatus"></span>
335
- </div>
336
- <div className="modal-header-right">
337
- <div className="modal-diff-nav" id="modalDiffNav" style={{ display: 'none', marginRight: '16px', gap: '4px', alignItems: 'center' }}>
338
- <button className="btn-ghost btn-xs" id="diffNavPrev" title="Previous changed file (k)">
339
- <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2.5"><polyline points="15 18 9 12 15 6"></polyline></svg>
340
- </button>
341
- <button className="btn-ghost btn-xs" id="diffNavNext" title="Next changed file (j)">
342
- <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2.5"><polyline points="9 18 15 12 9 6"></polyline></svg>
343
- </button>
344
- </div>
345
- <div className="modal-view-tabs" id="modalViewTabs">
346
- <button className="modal-tab active" data-view="edit">
347
- <svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" strokeWidth="2">
348
- <path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7" />
349
- <path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z" />
350
- </svg>
351
- Edit
352
- </button>
353
- <button className="modal-tab" data-view="diff">
354
- <svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" strokeWidth="2">
355
- <path d="M12 3v18M3 12h18" />
356
- </svg>
357
- Diff
358
- </button>
359
- </div>
360
- <span className="modal-save-status" id="modalSaveStatus" style={{ display: 'none' }}></span>
361
- <button className="btn-ghost btn-xs modal-outline-toggle" id="outlineToggle" title="Toggle symbol outline (Ctrl+Shift+O)">
362
- <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2">
363
- <line x1="8" y1="6" x2="21" y2="6" /><line x1="8" y1="12" x2="21" y2="12" /><line x1="8" y1="18" x2="21" y2="18" />
364
- <line x1="3" y1="6" x2="3.01" y2="6" /><line x1="3" y1="12" x2="3.01" y2="12" /><line x1="3" y1="18" x2="3.01" y2="18" />
365
- </svg>
366
- </button>
367
- <button className="modal-close" id="closePreview">&times;</button>
368
- </div>
369
- </div>
370
- <div className="modal-body-wrapper">
371
- <pre className="modal-body" id="modalBodyPre"><code id="previewContent"></code></pre>
372
- <div className="modal-outline-panel" id="modalOutlinePanel" style={{ display: 'none' }}></div>
373
- </div>
374
- <div className="modal-blame-container" id="modalBlameContainer" style={{ display: 'none' }}></div>
375
- <div className="modal-chat-container" id="modalChatContainer" style={{ display: 'none' }}></div>
376
- <div className="modal-edit-container" id="modalEditContainer" style={{ display: 'none' }}>
377
- <textarea id="modalEditTextarea" className="modal-edit-textarea" spellCheck={false} autoComplete="off"></textarea>
378
- <div className="modal-edit-toolbar" id="modalEditToolbar">
379
- <span className="edit-line-info" id="editLineInfo">Line 1, Col 1</span>
380
- <div className="edit-toolbar-right">
381
- <div className="edit-commit-section" id="editCommitSection" style={{ display: 'none' }}>
382
- <input
383
- type="text"
384
- id="editCommitMsg"
385
- className="edit-commit-input"
386
- placeholder="Commit message..."
387
- spellCheck={false}
388
- autoComplete="off"
389
- />
390
- <button className="btn-ghost btn-sm edit-commit-btn" id="editCommitBtn" title="Commit this file">
391
- <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2">
392
- <circle cx="12" cy="12" r="4" />
393
- <line x1="1.05" y1="12" x2="7" y2="12" />
394
- <line x1="17.01" y1="12" x2="22.96" y2="12" />
395
- </svg>
396
- Commit
397
- </button>
398
- <button className="btn-ghost btn-xs edit-commit-cancel" id="editCommitCancel" title="Cancel">
399
- <svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" strokeWidth="2.5">
400
- <line x1="18" y1="6" x2="6" y2="18" />
401
- <line x1="6" y1="6" x2="18" y2="18" />
402
- </svg>
403
- </button>
404
- </div>
405
- <button className="btn-ghost btn-sm edit-save-btn" id="editSaveBtn" title="Save (Ctrl+S)">
406
- <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2">
407
- <path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z" />
408
- <polyline points="17 21 17 13 7 13 7 21" />
409
- <polyline points="7 3 7 8 15 8" />
410
- </svg>
411
- Save
412
- </button>
413
- </div>
414
- </div>
415
- </div>
416
- </div>
996
+ <div className="modal-view-tabs" id="modalViewTabs">
997
+ <button className="modal-tab active" data-view="edit">
998
+ <svg
999
+ viewBox="0 0 24 24"
1000
+ width="12"
1001
+ height="12"
1002
+ fill="none"
1003
+ stroke="currentColor"
1004
+ strokeWidth="2"
1005
+ >
1006
+ <path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z" />
1007
+ </svg>
1008
+ Edit
1009
+ </button>
1010
+ <button className="modal-tab" data-view="diff">
1011
+ <svg
1012
+ viewBox="0 0 24 24"
1013
+ width="12"
1014
+ height="12"
1015
+ fill="none"
1016
+ stroke="currentColor"
1017
+ strokeWidth="2"
1018
+ >
1019
+ <path d="M12 3v18M3 12h18" />
1020
+ </svg>
1021
+ Diff
1022
+ </button>
417
1023
  </div>
418
-
419
- {/* GitHub Import Modal */}
420
- <div className="github-modal" id="githubModal">
421
- <div className="github-modal-backdrop"></div>
422
- <div className="github-modal-content">
423
- <div className="github-modal-header">
424
- <div className="github-modal-title">
425
- <svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor" style={{ opacity: 0.7 }}>
426
- <path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
427
- </svg>
428
- <span>Import from GitHub</span>
429
- </div>
430
- <button className="github-modal-close" id="githubModalClose">&times;</button>
431
- </div>
432
- <div className="github-search-row">
433
- <input
434
- type="text"
435
- id="githubUserInput"
436
- className="github-user-input"
437
- placeholder="Username, org, or paste a GitHub URL..."
438
- spellCheck={false}
439
- autoComplete="off"
440
- />
441
- <select id="githubSortSelect" className="github-sort-select">
442
- <option value="updated">Recently Updated</option>
443
- <option value="stars">Most Stars</option>
444
- <option value="name">Name A→Z</option>
445
- </select>
446
- <button id="githubSearchBtn" className="github-search-btn">
447
- <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2.5">
448
- <circle cx="11" cy="11" r="8" />
449
- <line x1="21" y1="21" x2="16.65" y2="16.65" />
450
- </svg>
451
- Search
452
- </button>
453
- </div>
454
- <div className="github-url-clone-row" id="githubUrlCloneRow" style={{ display: 'none' }}>
455
- <div className="github-url-detected">
456
- <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2">
457
- <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
458
- <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
459
- </svg>
460
- <span id="githubDetectedUrl">URL detected</span>
461
- </div>
462
- <button id="githubUrlCloneBtn" className="github-clone-btn github-url-clone-btn">Clone &amp; Open</button>
463
- </div>
464
- <div className="github-filter-row" id="githubFilterRow" style={{ display: 'none' }}>
465
- <input
466
- type="text"
467
- id="githubRepoFilter"
468
- className="github-repo-filter"
469
- placeholder="Filter repos by name..."
470
- spellCheck={false}
471
- autoComplete="off"
472
- />
473
- </div>
474
- <div className="github-profile" id="githubProfile" style={{ display: 'none' }}></div>
475
- <div className="github-repos-grid" id="githubReposGrid">
476
- <div className="github-empty-state">
477
- <svg viewBox="0 0 24 24" width="40" height="40" fill="none" stroke="currentColor" strokeWidth="1" opacity="0.2">
478
- <path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
479
- </svg>
480
- <p>Enter a GitHub username to browse their repositories</p>
481
- </div>
482
- </div>
483
- <div className="github-pagination" id="githubPagination" style={{ display: 'none' }}>
484
- <button id="githubPrevPage" className="github-page-btn" disabled>← Previous</button>
485
- <span id="githubPageInfo" className="github-page-info">Page 1</span>
486
- <button id="githubNextPage" className="github-page-btn">Next →</button>
487
- </div>
488
- </div>
1024
+ <span
1025
+ id="modalSaveStatus"
1026
+ style={{ display: "none" }}
1027
+ ></span>
1028
+ <button
1029
+ className="btn-ghost btn-xs modal-outline-toggle"
1030
+ id="outlineToggle"
1031
+ title="Toggle symbol outline (Ctrl+Shift+O)"
1032
+ >
1033
+ <svg
1034
+ viewBox="0 0 24 24"
1035
+ width="14"
1036
+ height="14"
1037
+ fill="none"
1038
+ stroke="currentColor"
1039
+ strokeWidth="2"
1040
+ >
1041
+ <line x1="8" y1="6" x2="21" y2="6" />
1042
+ <line x1="8" y1="12" x2="21" y2="12" />
1043
+ <line x1="8" y1="18" x2="21" y2="18" />
1044
+ <line x1="3" y1="6" x2="3.01" y2="6" />
1045
+ <line x1="3" y1="12" x2="3.01" y2="12" />
1046
+ <line x1="3" y1="18" x2="3.01" y2="18" />
1047
+ </svg>
1048
+ </button>
1049
+ <button className="modal-close" id="closePreview">
1050
+ &times;
1051
+ </button>
1052
+ </div>
1053
+ </div>
1054
+ <div className="modal-body-wrapper">
1055
+ <pre className="modal-body" id="modalBodyPre">
1056
+ <code id="previewContent"></code>
1057
+ </pre>
1058
+ <div
1059
+ className="modal-outline-panel"
1060
+ id="modalOutlinePanel"
1061
+ style={{ display: "none" }}
1062
+ ></div>
1063
+ </div>
1064
+ <div
1065
+ className="modal-blame-container"
1066
+ id="modalBlameContainer"
1067
+ style={{ display: "none" }}
1068
+ ></div>
1069
+ <div
1070
+ className="modal-chat-container"
1071
+ id="modalChatContainer"
1072
+ style={{ display: "none" }}
1073
+ ></div>
1074
+ <div
1075
+ className="modal-edit-container"
1076
+ id="modalEditContainer"
1077
+ style={{ display: "none" }}
1078
+ >
1079
+ <textarea
1080
+ id="modalEditTextarea"
1081
+ className="modal-edit-textarea"
1082
+ spellCheck={false}
1083
+ autoComplete="off"
1084
+ ></textarea>
1085
+ <div className="modal-edit-toolbar" id="modalEditToolbar">
1086
+ <span className="edit-line-info" id="editLineInfo">
1087
+ Line 1, Col 1
1088
+ </span>
1089
+ <div className="edit-toolbar-right">
1090
+ <div
1091
+ className="edit-commit-section"
1092
+ id="editCommitSection"
1093
+ style={{ display: "none" }}
1094
+ >
1095
+ <input
1096
+ type="text"
1097
+ id="editCommitMsg"
1098
+ className="edit-commit-input"
1099
+ placeholder="Commit message..."
1100
+ spellCheck={false}
1101
+ autoComplete="off"
1102
+ />
1103
+ <button
1104
+ className="btn-ghost btn-sm edit-commit-btn"
1105
+ id="editCommitBtn"
1106
+ title="Commit this file"
1107
+ >
1108
+ <svg
1109
+ viewBox="0 0 24 24"
1110
+ width="14"
1111
+ height="14"
1112
+ fill="none"
1113
+ stroke="currentColor"
1114
+ strokeWidth="2"
1115
+ >
1116
+ <circle cx="12" cy="12" r="4" />
1117
+ <path d="M5 12l5 5L20 7" />
1118
+ </svg>
1119
+ Commit
1120
+ </button>
1121
+ <button
1122
+ className="btn-ghost btn-xs edit-commit-cancel"
1123
+ id="editCommitCancel"
1124
+ title="Cancel"
1125
+ >
1126
+ <svg
1127
+ viewBox="0 0 24 24"
1128
+ width="12"
1129
+ height="12"
1130
+ fill="none"
1131
+ stroke="currentColor"
1132
+ strokeWidth="2.5"
1133
+ >
1134
+ <line x1="18" y1="6" x2="6" y2="18" />
1135
+ <line x1="6" y1="6" x2="18" y2="18" />
1136
+ </svg>
1137
+ </button>
1138
+ </div>
1139
+ <button
1140
+ className="btn-ghost btn-sm edit-save-btn"
1141
+ id="editSaveBtn"
1142
+ title="Save (Ctrl+S)"
1143
+ >
1144
+ <svg
1145
+ viewBox="0 0 24 24"
1146
+ width="14"
1147
+ height="14"
1148
+ fill="none"
1149
+ stroke="currentColor"
1150
+ strokeWidth="2"
1151
+ >
1152
+ <path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z" />
1153
+ <polyline points="17 21 17 13 7 13 7 21" />
1154
+ <polyline points="7 3 7 8 15 8" />
1155
+ </svg>
1156
+ Save
1157
+ </button>
489
1158
  </div>
490
- </body >
491
- </html >
492
- );
1159
+ </div>
1160
+ </div>
1161
+ </div>
1162
+ </div>
1163
+
1164
+ {/* GitHub Import Modal */}
1165
+ <div className="github-modal" id="githubModal">
1166
+ <div className="github-modal-backdrop"></div>
1167
+ <div className="github-modal-content">
1168
+ <div className="github-modal-header">
1169
+ <div className="github-modal-title">
1170
+ <svg
1171
+ viewBox="0 0 24 24"
1172
+ width="20"
1173
+ height="20"
1174
+ fill="currentColor"
1175
+ style={{ opacity: 0.7 }}
1176
+ >
1177
+ <path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
1178
+ </svg>
1179
+ <span>Import from GitHub</span>
1180
+ </div>
1181
+ <button className="github-modal-close" id="githubModalClose">
1182
+ &times;
1183
+ </button>
1184
+ </div>
1185
+ <div className="github-search-row">
1186
+ <input
1187
+ type="text"
1188
+ id="githubUserInput"
1189
+ className="github-user-input"
1190
+ placeholder="Username, org, or paste a GitHub URL..."
1191
+ spellCheck={false}
1192
+ autoComplete="off"
1193
+ />
1194
+ <select id="githubSortSelect" className="github-sort-select">
1195
+ <option value="updated">Recently Updated</option>
1196
+ <option value="stars">Most Stars</option>
1197
+ <option value="name">Name A→Z</option>
1198
+ </select>
1199
+ <button id="githubSearchBtn" className="github-search-btn">
1200
+ <svg
1201
+ viewBox="0 0 24 24"
1202
+ width="14"
1203
+ height="14"
1204
+ fill="none"
1205
+ stroke="currentColor"
1206
+ strokeWidth="2.5"
1207
+ >
1208
+ <circle cx="11" cy="11" r="8" />
1209
+ <line x1="21" y1="21" x2="16.65" y2="16.65" />
1210
+ </svg>
1211
+ Search
1212
+ </button>
1213
+ </div>
1214
+ <div
1215
+ className="github-url-clone-row"
1216
+ id="githubUrlCloneRow"
1217
+ style={{ display: "none" }}
1218
+ >
1219
+ <div className="github-url-detected">
1220
+ <svg
1221
+ viewBox="0 0 24 24"
1222
+ width="14"
1223
+ height="14"
1224
+ fill="none"
1225
+ stroke="currentColor"
1226
+ strokeWidth="2"
1227
+ >
1228
+ <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
1229
+ <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
1230
+ </svg>
1231
+ <span id="githubDetectedUrl">URL detected</span>
1232
+ </div>
1233
+ <button
1234
+ id="githubUrlCloneBtn"
1235
+ className="github-clone-btn github-url-clone-btn"
1236
+ >
1237
+ Clone &amp; Open
1238
+ </button>
1239
+ </div>
1240
+ <div
1241
+ className="github-filter-row"
1242
+ id="githubFilterRow"
1243
+ style={{ display: "none" }}
1244
+ >
1245
+ <input
1246
+ type="text"
1247
+ id="githubRepoFilter"
1248
+ className="github-repo-filter"
1249
+ placeholder="Filter repos by name..."
1250
+ spellCheck={false}
1251
+ autoComplete="off"
1252
+ />
1253
+ </div>
1254
+ <div
1255
+ className="github-profile"
1256
+ id="githubProfile"
1257
+ style={{ display: "none" }}
1258
+ ></div>
1259
+ <div className="github-repos-grid" id="githubReposGrid">
1260
+ <div className="github-empty-state">
1261
+ <svg
1262
+ viewBox="0 0 24 24"
1263
+ width="40"
1264
+ height="40"
1265
+ fill="none"
1266
+ stroke="currentColor"
1267
+ strokeWidth="1"
1268
+ opacity="0.2"
1269
+ >
1270
+ <path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
1271
+ </svg>
1272
+ <p>Enter a GitHub username to browse their repositories</p>
1273
+ </div>
1274
+ </div>
1275
+ <div
1276
+ className="github-pagination"
1277
+ id="githubPagination"
1278
+ style={{ display: "none" }}
1279
+ >
1280
+ <button id="githubPrevPage" className="github-page-btn" disabled>
1281
+ ← Previous
1282
+ </button>
1283
+ <span id="githubPageInfo" className="github-page-info">
1284
+ Page 1
1285
+ </span>
1286
+ <button id="githubNextPage" className="github-page-btn">
1287
+ Next →
1288
+ </button>
1289
+ </div>
1290
+ </div>
1291
+ </div>
1292
+ <script
1293
+ dangerouslySetInnerHTML={{
1294
+ __html: `
1295
+ if ('serviceWorker' in navigator) {
1296
+ navigator.serviceWorker.register('/api/sw.js', { scope: '/' })
1297
+ .catch(function(e) { console.warn('[SW] Registration failed:', e); });
1298
+ }
1299
+ fetch('/api/analytics', {
1300
+ method: 'POST',
1301
+ headers: { 'Content-Type': 'application/json' },
1302
+ body: JSON.stringify({ path: location.pathname })
1303
+ }).catch(function(){});
1304
+ `,
1305
+ }}
1306
+ />
1307
+ </body>
1308
+ </html>
1309
+ );
493
1310
  }