git-history-ui 1.0.0

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 (64) hide show
  1. package/.eslintrc.js +21 -0
  2. package/README.md +304 -0
  3. package/demo.js +45 -0
  4. package/dist/__tests__/gitService.test.d.ts +2 -0
  5. package/dist/__tests__/gitService.test.d.ts.map +1 -0
  6. package/dist/__tests__/gitService.test.js +32 -0
  7. package/dist/__tests__/gitService.test.js.map +1 -0
  8. package/dist/backend/dev-server.d.ts +2 -0
  9. package/dist/backend/dev-server.d.ts.map +1 -0
  10. package/dist/backend/dev-server.js +16 -0
  11. package/dist/backend/dev-server.js.map +1 -0
  12. package/dist/backend/gitService.d.ts +45 -0
  13. package/dist/backend/gitService.d.ts.map +1 -0
  14. package/dist/backend/gitService.js +239 -0
  15. package/dist/backend/gitService.js.map +1 -0
  16. package/dist/backend/server.d.ts +9 -0
  17. package/dist/backend/server.d.ts.map +1 -0
  18. package/dist/backend/server.js +118 -0
  19. package/dist/backend/server.js.map +1 -0
  20. package/dist/cli.d.ts +3 -0
  21. package/dist/cli.d.ts.map +1 -0
  22. package/dist/cli.js +49 -0
  23. package/dist/cli.js.map +1 -0
  24. package/frontend/.editorconfig +17 -0
  25. package/frontend/.vscode/extensions.json +4 -0
  26. package/frontend/.vscode/launch.json +20 -0
  27. package/frontend/.vscode/tasks.json +42 -0
  28. package/frontend/README.md +59 -0
  29. package/frontend/angular.json +99 -0
  30. package/frontend/package-lock.json +10566 -0
  31. package/frontend/package.json +55 -0
  32. package/frontend/proxy.conf.json +7 -0
  33. package/frontend/public/favicon.ico +0 -0
  34. package/frontend/src/app/app.component.ts +598 -0
  35. package/frontend/src/app/app.config.ts +12 -0
  36. package/frontend/src/app/app.css +0 -0
  37. package/frontend/src/app/app.html +342 -0
  38. package/frontend/src/app/app.routes.ts +3 -0
  39. package/frontend/src/app/app.spec.ts +23 -0
  40. package/frontend/src/app/app.ts +12 -0
  41. package/frontend/src/app/components/color-palette-selector/color-palette-selector.component.ts +137 -0
  42. package/frontend/src/app/components/commit-detail/commit-detail.component.ts +327 -0
  43. package/frontend/src/app/components/commit-graph/commit-graph.component.ts +294 -0
  44. package/frontend/src/app/components/commit-list/commit-list.component.ts +199 -0
  45. package/frontend/src/app/components/diff-viewer/diff-viewer.component.ts +311 -0
  46. package/frontend/src/app/models/color-palette.models.ts +229 -0
  47. package/frontend/src/app/models/git.models.ts +39 -0
  48. package/frontend/src/app/services/git.service.ts +43 -0
  49. package/frontend/src/index.html +13 -0
  50. package/frontend/src/main.ts +6 -0
  51. package/frontend/src/styles.css +397 -0
  52. package/frontend/tsconfig.app.json +15 -0
  53. package/frontend/tsconfig.json +34 -0
  54. package/frontend/tsconfig.spec.json +14 -0
  55. package/jest.config.js +13 -0
  56. package/package.json +70 -0
  57. package/public/app.js +403 -0
  58. package/public/index.html +172 -0
  59. package/src/__tests__/gitService.test.ts +35 -0
  60. package/src/backend/dev-server.ts +14 -0
  61. package/src/backend/gitService.ts +277 -0
  62. package/src/backend/server.ts +132 -0
  63. package/src/cli.ts +56 -0
  64. package/tsconfig.json +25 -0
@@ -0,0 +1,15 @@
1
+ /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
2
+ /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
3
+ {
4
+ "extends": "./tsconfig.json",
5
+ "compilerOptions": {
6
+ "outDir": "./out-tsc/app",
7
+ "types": []
8
+ },
9
+ "include": [
10
+ "src/**/*.ts"
11
+ ],
12
+ "exclude": [
13
+ "src/**/*.spec.ts"
14
+ ]
15
+ }
@@ -0,0 +1,34 @@
1
+ /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
2
+ /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
3
+ {
4
+ "compileOnSave": false,
5
+ "compilerOptions": {
6
+ "strict": true,
7
+ "noImplicitOverride": true,
8
+ "noPropertyAccessFromIndexSignature": true,
9
+ "noImplicitReturns": true,
10
+ "noFallthroughCasesInSwitch": true,
11
+ "skipLibCheck": true,
12
+ "isolatedModules": true,
13
+ "experimentalDecorators": true,
14
+ "importHelpers": true,
15
+ "target": "ES2022",
16
+ "module": "preserve"
17
+ },
18
+ "angularCompilerOptions": {
19
+ "enableI18nLegacyMessageIdFormat": false,
20
+ "strictInjectionParameters": true,
21
+ "strictInputAccessModifiers": true,
22
+ "typeCheckHostBindings": true,
23
+ "strictTemplates": true
24
+ },
25
+ "files": [],
26
+ "references": [
27
+ {
28
+ "path": "./tsconfig.app.json"
29
+ },
30
+ {
31
+ "path": "./tsconfig.spec.json"
32
+ }
33
+ ]
34
+ }
@@ -0,0 +1,14 @@
1
+ /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
2
+ /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
3
+ {
4
+ "extends": "./tsconfig.json",
5
+ "compilerOptions": {
6
+ "outDir": "./out-tsc/spec",
7
+ "types": [
8
+ "jasmine"
9
+ ]
10
+ },
11
+ "include": [
12
+ "src/**/*.ts"
13
+ ]
14
+ }
package/jest.config.js ADDED
@@ -0,0 +1,13 @@
1
+ module.exports = {
2
+ preset: 'ts-jest',
3
+ testEnvironment: 'node',
4
+ roots: ['<rootDir>/src'],
5
+ testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
6
+ transform: {
7
+ '^.+\\.ts$': 'ts-jest',
8
+ },
9
+ collectCoverageFrom: [
10
+ 'src/**/*.ts',
11
+ '!src/**/*.d.ts',
12
+ ],
13
+ };
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "git-history-ui",
3
+ "version": "1.0.0",
4
+ "description": "Beautiful git history visualization in your browser",
5
+ "main": "dist/cli.js",
6
+ "bin": {
7
+ "git-history-ui": "dist/cli.js"
8
+ },
9
+ "scripts": {
10
+ "build": "npm run build:backend && npm run build:frontend",
11
+ "build:backend": "tsc -p tsconfig.json",
12
+ "build:frontend": "cd frontend && ng build --configuration production",
13
+ "dev": "concurrently \"npm run dev:backend\" \"npm run dev:frontend\"",
14
+ "dev:backend": "ts-node-dev --respawn --transpile-only src/backend/dev-server.ts",
15
+ "dev:frontend": "cd frontend && ng serve --port 4200",
16
+ "start": "node dist/backend/server.js",
17
+ "test": "jest",
18
+ "lint": "eslint src/**/*.ts",
19
+ "prepare": "npm run build"
20
+ },
21
+ "keywords": [
22
+ "git",
23
+ "history",
24
+ "visualization",
25
+ "cli",
26
+ "ui",
27
+ "commit",
28
+ "graph",
29
+ "diff",
30
+ "angular"
31
+ ],
32
+ "author": "beingmartinbmc",
33
+ "license": "MIT",
34
+ "dependencies": {
35
+ "chalk": "^5.3.0",
36
+ "commander": "^11.1.0",
37
+ "cors": "^2.8.5",
38
+ "express": "^4.18.2",
39
+ "open": "^9.1.0",
40
+ "simple-git": "^3.20.0",
41
+ "socket.io": "^4.7.4"
42
+ },
43
+ "devDependencies": {
44
+ "@types/commander": "^2.12.2",
45
+ "@types/cors": "^2.8.17",
46
+ "@types/express": "^4.17.21",
47
+ "@types/jest": "^29.5.0",
48
+ "@types/node": "^20.10.0",
49
+ "@types/open": "^6.2.1",
50
+ "@typescript-eslint/eslint-plugin": "^6.13.0",
51
+ "@typescript-eslint/parser": "^6.13.0",
52
+ "concurrently": "^8.2.2",
53
+ "eslint": "^8.55.0",
54
+ "jest": "^29.7.0",
55
+ "ts-jest": "^29.1.0",
56
+ "ts-node-dev": "^2.0.0",
57
+ "typescript": "^5.3.0"
58
+ },
59
+ "engines": {
60
+ "node": ">=18.0.0"
61
+ },
62
+ "repository": {
63
+ "type": "git",
64
+ "url": "https://github.com/beingmartinbmc/git-history-ui.git"
65
+ },
66
+ "bugs": {
67
+ "url": "https://github.com/beingmartinbmc/git-history-ui/issues"
68
+ },
69
+ "homepage": "https://github.com/beingmartinbmc/git-history-ui#readme"
70
+ }
package/public/app.js ADDED
@@ -0,0 +1,403 @@
1
+ class GitHistoryUI {
2
+ constructor() {
3
+ this.commits = [];
4
+ this.currentView = 'list';
5
+ this.darkMode = false;
6
+ this.socket = io();
7
+
8
+ this.initializeEventListeners();
9
+ this.loadCommits();
10
+ this.setupSocket();
11
+ this.initializeDarkMode();
12
+ }
13
+
14
+ initializeEventListeners() {
15
+ // View toggles
16
+ document.getElementById('graphView').addEventListener('click', () => this.switchView('graph'));
17
+ document.getElementById('listView').addEventListener('click', () => this.switchView('list'));
18
+
19
+ // Dark mode toggle
20
+ document.getElementById('darkMode').addEventListener('click', () => this.toggleDarkMode());
21
+
22
+ // Search and filters
23
+ document.getElementById('search').addEventListener('input', (e) => this.handleSearch(e.target.value));
24
+ document.getElementById('authorFilter').addEventListener('change', (e) => this.handleAuthorFilter(e.target.value));
25
+ document.getElementById('sinceFilter').addEventListener('change', (e) => this.handleSinceFilter(e.target.value));
26
+ document.getElementById('fileFilter').addEventListener('input', (e) => this.handleFileFilter(e.target.value));
27
+
28
+ // Modal
29
+ document.getElementById('closeModal').addEventListener('click', () => this.closeModal());
30
+
31
+ // Close modal on outside click
32
+ document.getElementById('commitModal').addEventListener('click', (e) => {
33
+ if (e.target.id === 'commitModal') {
34
+ this.closeModal();
35
+ }
36
+ });
37
+ }
38
+
39
+ setupSocket() {
40
+ this.socket.on('connect', () => {
41
+ console.log('Connected to server');
42
+ });
43
+
44
+ this.socket.on('disconnect', () => {
45
+ console.log('Disconnected from server');
46
+ });
47
+ }
48
+
49
+ async loadCommits() {
50
+ try {
51
+ const response = await fetch('/api/commits');
52
+ this.commits = await response.json();
53
+ this.renderCommits();
54
+ this.populateFilters();
55
+ this.hideLoading();
56
+ } catch (error) {
57
+ console.error('Error loading commits:', error);
58
+ this.showError('Failed to load commits');
59
+ }
60
+ }
61
+
62
+ async loadCommitsWithFilters(filters = {}) {
63
+ try {
64
+ const params = new URLSearchParams(filters);
65
+ const response = await fetch(`/api/commits?${params}`);
66
+ this.commits = await response.json();
67
+ this.renderCommits();
68
+ } catch (error) {
69
+ console.error('Error loading commits with filters:', error);
70
+ }
71
+ }
72
+
73
+ renderCommits() {
74
+ if (this.currentView === 'graph') {
75
+ this.renderGraphView();
76
+ } else {
77
+ this.renderListView();
78
+ }
79
+ }
80
+
81
+ renderListView() {
82
+ const container = document.getElementById('commitsList');
83
+ container.innerHTML = '';
84
+
85
+ this.commits.forEach(commit => {
86
+ const commitElement = this.createCommitElement(commit);
87
+ container.appendChild(commitElement);
88
+ });
89
+ }
90
+
91
+ createCommitElement(commit) {
92
+ const div = document.createElement('div');
93
+ div.className = 'bg-white dark:bg-gray-800 rounded-lg shadow p-4 hover:shadow-md transition-all duration-200 cursor-pointer';
94
+ div.innerHTML = `
95
+ <div class="flex items-start justify-between">
96
+ <div class="flex-1">
97
+ <div class="flex items-center space-x-2 mb-2">
98
+ <span class="text-sm font-mono text-gray-500 dark:text-gray-400 transition-colors duration-200">${commit.hash.substring(0, 8)}</span>
99
+ <span class="text-sm text-gray-600 dark:text-gray-400 transition-colors duration-200">${commit.author}</span>
100
+ <span class="text-sm text-gray-500 dark:text-gray-500 transition-colors duration-200">${this.formatDate(commit.date)}</span>
101
+ </div>
102
+ <h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2 transition-colors duration-200">${commit.message}</h3>
103
+ <div class="flex items-center space-x-4 text-sm text-gray-600 dark:text-gray-400 transition-colors duration-200">
104
+ <span>${commit.files.length} files changed</span>
105
+ ${commit.branches.length > 0 ? `<span>Branch: ${commit.branches[0]}</span>` : ''}
106
+ ${commit.tags.length > 0 ? `<span>Tag: ${commit.tags[0]}</span>` : ''}
107
+ </div>
108
+ </div>
109
+ <div class="flex space-x-2">
110
+ <button class="px-3 py-1 text-sm bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors duration-200" onclick="app.showCommitDetails('${commit.hash}')">
111
+ View
112
+ </button>
113
+ </div>
114
+ </div>
115
+ `;
116
+ return div;
117
+ }
118
+
119
+ renderGraphView() {
120
+ const svg = d3.select('#commitGraph');
121
+ svg.selectAll('*').remove();
122
+
123
+ const width = svg.node().getBoundingClientRect().width;
124
+ const height = 600;
125
+ const margin = { top: 20, right: 20, bottom: 20, left: 20 };
126
+
127
+ // Get colors based on dark mode
128
+ const colors = this.getGraphColors();
129
+
130
+ const g = svg.append('g')
131
+ .attr('transform', `translate(${margin.left},${margin.top})`);
132
+
133
+ // Create commit nodes
134
+ const nodes = this.commits.map((commit, i) => ({
135
+ id: commit.hash,
136
+ x: (i % 10) * 80 + 40,
137
+ y: Math.floor(i / 10) * 100 + 50,
138
+ commit: commit
139
+ }));
140
+
141
+ // Create links between commits
142
+ const links = [];
143
+ for (let i = 1; i < nodes.length; i++) {
144
+ links.push({
145
+ source: nodes[i - 1],
146
+ target: nodes[i]
147
+ });
148
+ }
149
+
150
+ // Draw links
151
+ g.selectAll('.link')
152
+ .data(links)
153
+ .enter().append('line')
154
+ .attr('class', 'link')
155
+ .attr('x1', d => d.source.x)
156
+ .attr('y1', d => d.source.y)
157
+ .attr('x2', d => d.target.x)
158
+ .attr('y2', d => d.target.y)
159
+ .attr('stroke', colors.link)
160
+ .attr('stroke-width', 2);
161
+
162
+ // Draw nodes
163
+ const node = g.selectAll('.commit-node')
164
+ .data(nodes)
165
+ .enter().append('g')
166
+ .attr('class', 'commit-node')
167
+ .attr('transform', d => `translate(${d.x},${d.y})`);
168
+
169
+ node.append('circle')
170
+ .attr('r', 8)
171
+ .attr('fill', colors.nodeFill)
172
+ .attr('stroke', colors.nodeStroke)
173
+ .attr('stroke-width', 2);
174
+
175
+ node.append('text')
176
+ .attr('text-anchor', 'middle')
177
+ .attr('dy', 25)
178
+ .attr('fill', colors.text)
179
+ .attr('class', 'text-xs')
180
+ .text(d => d.commit.hash.substring(0, 6));
181
+
182
+ // Add click handlers
183
+ node.on('click', (event, d) => {
184
+ this.showCommitDetails(d.commit.hash);
185
+ });
186
+ }
187
+
188
+ async showCommitDetails(hash) {
189
+ try {
190
+ const [commit, diff] = await Promise.all([
191
+ fetch(`/api/commit/${hash}`).then(r => r.json()),
192
+ fetch(`/api/diff/${hash}`).then(r => r.json())
193
+ ]);
194
+
195
+ const modal = document.getElementById('commitModal');
196
+ const details = document.getElementById('commitDetails');
197
+
198
+ details.innerHTML = `
199
+ <div class="space-y-4">
200
+ <div class="border-b border-gray-200 dark:border-gray-700 pb-4">
201
+ <div class="flex items-center space-x-2 mb-2">
202
+ <span class="text-sm font-mono text-gray-500 dark:text-gray-400 transition-colors duration-200">${commit.hash}</span>
203
+ <span class="text-sm text-gray-600 dark:text-gray-400 transition-colors duration-200">by ${commit.author}</span>
204
+ <span class="text-sm text-gray-500 dark:text-gray-500 transition-colors duration-200">${this.formatDate(commit.date)}</span>
205
+ </div>
206
+ <h3 class="text-xl font-semibold text-gray-900 dark:text-white transition-colors duration-200">${commit.message}</h3>
207
+ </div>
208
+
209
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
210
+ <div>
211
+ <h4 class="text-lg font-medium text-gray-900 dark:text-white mb-3 transition-colors duration-200">Files Changed</h4>
212
+ <div class="space-y-2">
213
+ ${commit.files.map(file => `
214
+ <div class="flex items-center justify-between p-2 bg-gray-50 dark:bg-gray-700 rounded transition-colors duration-200">
215
+ <span class="text-sm text-gray-700 dark:text-gray-300 transition-colors duration-200">${file}</span>
216
+ <button class="text-blue-500 hover:text-blue-600 text-sm transition-colors duration-200" onclick="app.showFileDiff('${file}')">
217
+ View Diff
218
+ </button>
219
+ </div>
220
+ `).join('')}
221
+ </div>
222
+ </div>
223
+
224
+ <div>
225
+ <h4 class="text-lg font-medium text-gray-900 dark:text-white mb-3 transition-colors duration-200">Diff Summary</h4>
226
+ <div class="space-y-2">
227
+ ${diff.map(file => `
228
+ <div class="p-2 bg-gray-50 dark:bg-gray-700 rounded transition-colors duration-200">
229
+ <div class="text-sm font-medium text-gray-700 dark:text-gray-300 transition-colors duration-200">${file.file}</div>
230
+ <div class="text-xs text-gray-500 dark:text-gray-500 transition-colors duration-200">
231
+ +${file.additions} -${file.deletions}
232
+ </div>
233
+ </div>
234
+ `).join('')}
235
+ </div>
236
+ </div>
237
+ </div>
238
+ </div>
239
+ `;
240
+
241
+ modal.classList.remove('hidden');
242
+ } catch (error) {
243
+ console.error('Error loading commit details:', error);
244
+ this.showError('Failed to load commit details');
245
+ }
246
+ }
247
+
248
+ closeModal() {
249
+ document.getElementById('commitModal').classList.add('hidden');
250
+ }
251
+
252
+ switchView(view) {
253
+ this.currentView = view;
254
+
255
+ // Update button styles with transition classes
256
+ document.getElementById('graphView').className = view === 'graph'
257
+ ? 'px-3 py-1 text-sm bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors duration-200'
258
+ : 'px-3 py-1 text-sm bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors duration-200';
259
+
260
+ document.getElementById('listView').className = view === 'list'
261
+ ? 'px-3 py-1 text-sm bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors duration-200'
262
+ : 'px-3 py-1 text-sm bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors duration-200';
263
+
264
+ // Show/hide views
265
+ document.getElementById('graphView').classList.toggle('hidden', view !== 'graph');
266
+ document.getElementById('listView').classList.toggle('hidden', view !== 'list');
267
+
268
+ this.renderCommits();
269
+ }
270
+
271
+ toggleDarkMode() {
272
+ this.darkMode = !this.darkMode;
273
+ document.documentElement.classList.toggle('dark', this.darkMode);
274
+ document.body.classList.toggle('dark', this.darkMode);
275
+
276
+ // Save preference to localStorage
277
+ localStorage.setItem('darkMode', this.darkMode.toString());
278
+
279
+ const button = document.getElementById('darkMode');
280
+ button.textContent = this.darkMode ? '☀️' : '🌙';
281
+
282
+ // Force re-render to ensure all elements get updated dark mode styles
283
+ this.renderCommits();
284
+
285
+ // If currently in graph view, re-render the graph with new colors
286
+ if (this.currentView === 'graph') {
287
+ this.renderGraphView();
288
+ }
289
+ }
290
+
291
+ handleSearch(query) {
292
+ const filtered = this.commits.filter(commit =>
293
+ commit.message.toLowerCase().includes(query.toLowerCase()) ||
294
+ commit.author.toLowerCase().includes(query.toLowerCase()) ||
295
+ commit.hash.toLowerCase().includes(query.toLowerCase())
296
+ );
297
+ this.renderFilteredCommits(filtered);
298
+ }
299
+
300
+ handleAuthorFilter(author) {
301
+ if (!author) {
302
+ this.renderCommits();
303
+ return;
304
+ }
305
+ const filtered = this.commits.filter(commit => commit.author === author);
306
+ this.renderFilteredCommits(filtered);
307
+ }
308
+
309
+ handleSinceFilter(since) {
310
+ if (!since) {
311
+ this.renderCommits();
312
+ return;
313
+ }
314
+ const filtered = this.commits.filter(commit => new Date(commit.date) >= new Date(since));
315
+ this.renderFilteredCommits(filtered);
316
+ }
317
+
318
+ handleFileFilter(file) {
319
+ if (!file) {
320
+ this.renderCommits();
321
+ return;
322
+ }
323
+ const filtered = this.commits.filter(commit =>
324
+ commit.files.some(f => f.toLowerCase().includes(file.toLowerCase()))
325
+ );
326
+ this.renderFilteredCommits(filtered);
327
+ }
328
+
329
+ renderFilteredCommits(filtered) {
330
+ if (this.currentView === 'list') {
331
+ const container = document.getElementById('commitsList');
332
+ container.innerHTML = '';
333
+ filtered.forEach(commit => {
334
+ const commitElement = this.createCommitElement(commit);
335
+ container.appendChild(commitElement);
336
+ });
337
+ }
338
+ }
339
+
340
+ populateFilters() {
341
+ const authors = [...new Set(this.commits.map(c => c.author))];
342
+ const authorSelect = document.getElementById('authorFilter');
343
+ authorSelect.innerHTML = '<option value="">All authors</option>';
344
+ authors.forEach(author => {
345
+ const option = document.createElement('option');
346
+ option.value = author;
347
+ option.textContent = author;
348
+ authorSelect.appendChild(option);
349
+ });
350
+ }
351
+
352
+ formatDate(dateString) {
353
+ const date = new Date(dateString);
354
+ return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
355
+ }
356
+
357
+ showError(message) {
358
+ // Simple error display - could be enhanced with a toast notification
359
+ alert(message);
360
+ }
361
+
362
+ hideLoading() {
363
+ document.getElementById('loading').style.display = 'none';
364
+ }
365
+
366
+ initializeDarkMode() {
367
+ // Check if user has a saved preference
368
+ const savedDarkMode = localStorage.getItem('darkMode');
369
+ if (savedDarkMode !== null) {
370
+ this.darkMode = savedDarkMode === 'true';
371
+ if (this.darkMode) {
372
+ document.documentElement.classList.add('dark');
373
+ document.body.classList.add('dark');
374
+ const button = document.getElementById('darkMode');
375
+ button.textContent = '☀️';
376
+ }
377
+ }
378
+ }
379
+
380
+ getGraphColors() {
381
+ if (this.darkMode) {
382
+ return {
383
+ link: '#4b5563', // gray-600 for dark mode
384
+ nodeFill: '#3b82f6', // blue-500 (same for both modes)
385
+ nodeStroke: '#1e40af', // blue-700 (same for both modes)
386
+ text: '#9ca3af' // gray-400 for dark mode
387
+ };
388
+ } else {
389
+ return {
390
+ link: '#cbd5e0', // gray-300 for light mode
391
+ nodeFill: '#3b82f6', // blue-500 (same for both modes)
392
+ nodeStroke: '#1e40af', // blue-700 (same for both modes)
393
+ text: '#6b7280' // gray-500 for light mode
394
+ };
395
+ }
396
+ }
397
+ }
398
+
399
+ // Initialize the app when the page loads
400
+ let app;
401
+ document.addEventListener('DOMContentLoaded', () => {
402
+ app = new GitHistoryUI();
403
+ });