gh-here 2.1.0 → 3.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.
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Modal management utilities
3
+ */
4
+
5
+ export function createModal(content, className = '') {
6
+ const modal = document.createElement('div');
7
+ modal.className = `modal-overlay ${className}`;
8
+ modal.innerHTML = content;
9
+
10
+ document.body.appendChild(modal);
11
+
12
+ return {
13
+ element: modal,
14
+ close: () => modal.remove()
15
+ };
16
+ }
17
+
18
+ export function showDraftDialog(filePath) {
19
+ return new Promise(resolve => {
20
+ const modal = document.createElement('div');
21
+ modal.className = 'modal-overlay';
22
+ modal.innerHTML = `
23
+ <div class="modal-content draft-modal">
24
+ <h3>Unsaved Changes Found</h3>
25
+ <p>You have unsaved changes for this file. What would you like to do?</p>
26
+ <div class="draft-actions">
27
+ <button class="btn btn-primary" data-action="load">Load Draft</button>
28
+ <button class="btn btn-secondary" data-action="discard">Discard Draft</button>
29
+ <button class="btn btn-secondary" data-action="cancel">Cancel</button>
30
+ </div>
31
+ </div>
32
+ `;
33
+
34
+ modal.addEventListener('click', e => {
35
+ if (e.target.matches('[data-action]') || e.target === modal) {
36
+ const action = e.target.dataset?.action || 'cancel';
37
+ modal.remove();
38
+ resolve(action);
39
+ }
40
+ });
41
+
42
+ document.body.appendChild(modal);
43
+ });
44
+ }
45
+
46
+ export function showConfirmDialog(message, confirmText = 'Confirm', cancelText = 'Cancel') {
47
+ return new Promise(resolve => {
48
+ const modal = document.createElement('div');
49
+ modal.className = 'modal-overlay';
50
+ modal.innerHTML = `
51
+ <div class="modal-content">
52
+ <p>${message}</p>
53
+ <div class="modal-actions">
54
+ <button class="btn btn-secondary" data-action="cancel">${cancelText}</button>
55
+ <button class="btn btn-primary" data-action="confirm">${confirmText}</button>
56
+ </div>
57
+ </div>
58
+ `;
59
+
60
+ modal.addEventListener('click', e => {
61
+ if (e.target.matches('[data-action]') || e.target === modal) {
62
+ const confirmed = e.target.dataset?.action === 'confirm';
63
+ modal.remove();
64
+ resolve(confirmed);
65
+ }
66
+ });
67
+
68
+ document.body.appendChild(modal);
69
+ });
70
+ }
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Client-side navigation handler for smooth page transitions
3
+ * Optimized with request cancellation and efficient DOM updates
4
+ */
5
+
6
+ export class NavigationHandler {
7
+ constructor() {
8
+ this.mainContentWrapper = document.querySelector('.main-content-wrapper');
9
+ this.isNavigating = false;
10
+ this.abortController = null;
11
+
12
+ // Clear any stuck navigation state on initialization
13
+ this.removeNavigatingClass();
14
+
15
+ this.setupListeners();
16
+ }
17
+
18
+ setupListeners() {
19
+ // Listen for navigation events from file tree and keyboard
20
+ document.addEventListener('navigate', this.handleNavigateEvent.bind(this));
21
+
22
+ // Intercept all internal link clicks (event delegation - no duplicates)
23
+ document.addEventListener('click', this.handleLinkClick.bind(this));
24
+
25
+ // Handle back/forward buttons
26
+ window.addEventListener('popstate', this.handlePopState.bind(this));
27
+ }
28
+
29
+ handleNavigateEvent(e) {
30
+ const { path } = e.detail;
31
+ this.navigateTo(path);
32
+ }
33
+
34
+ handleLinkClick(e) {
35
+ const link = e.target.closest('a');
36
+ if (!link) return;
37
+
38
+ // Skip if clicking inside quick actions or other interactive elements
39
+ if (e.target.closest('.quick-actions, button, .file-action-btn')) {
40
+ return;
41
+ }
42
+
43
+ // Only intercept left clicks on same-origin links without modifier keys
44
+ if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey ||
45
+ e.button !== 0 || link.target === '_blank' || link.download) {
46
+ return;
47
+ }
48
+
49
+ if (!this.isSameOrigin(link.href)) return;
50
+
51
+ const url = new URL(link.href);
52
+ // Only intercept navigation within our app
53
+ if (url.origin === window.location.origin &&
54
+ (url.pathname === '/' || url.pathname === window.location.pathname)) {
55
+ e.preventDefault();
56
+ e.stopPropagation();
57
+ const path = url.searchParams.get('path') || '';
58
+ const view = url.searchParams.get('view') || '';
59
+ this.navigateTo(path, true, view);
60
+ }
61
+ }
62
+
63
+ handlePopState(e) {
64
+ const url = new URL(window.location.href);
65
+ const path = e.state?.path ?? (url.searchParams.get('path') || '');
66
+ const view = e.state?.view ?? (url.searchParams.get('view') || '');
67
+ this.navigateTo(path, false, view);
68
+ }
69
+
70
+ isSameOrigin(href) {
71
+ try {
72
+ return new URL(href, window.location.origin).origin === window.location.origin;
73
+ } catch {
74
+ return false;
75
+ }
76
+ }
77
+
78
+ // Helper to safely remove navigating class
79
+ removeNavigatingClass() {
80
+ // Re-find the element in case reference is stale
81
+ const wrapper = document.querySelector('.main-content-wrapper');
82
+ if (wrapper) {
83
+ wrapper.classList.remove('navigating');
84
+ }
85
+ }
86
+
87
+ async navigateTo(path, pushState = true, view = '') {
88
+ // Cancel any in-flight request
89
+ if (this.abortController) {
90
+ this.abortController.abort();
91
+ }
92
+
93
+ if (this.isNavigating) return;
94
+ this.isNavigating = true;
95
+
96
+ // Preserve existing query parameters (like showGitignored) while updating path and view
97
+ let url = '/';
98
+ const currentParams = new URLSearchParams(window.location.search);
99
+ const params = new URLSearchParams(currentParams); // Start with existing params
100
+
101
+ // Update path parameter
102
+ if (path) {
103
+ params.set('path', path);
104
+ } else {
105
+ params.delete('path');
106
+ }
107
+
108
+ // Update view parameter
109
+ if (view) {
110
+ params.set('view', view);
111
+ } else {
112
+ params.delete('view');
113
+ }
114
+
115
+ const queryString = params.toString();
116
+ if (queryString) {
117
+ url += `?${queryString}`;
118
+ }
119
+ this.abortController = new AbortController();
120
+
121
+ try {
122
+ // Re-find element in case reference is stale
123
+ this.mainContentWrapper = document.querySelector('.main-content-wrapper');
124
+
125
+ // Optimize: Use class instead of inline style
126
+ if (this.mainContentWrapper) {
127
+ this.mainContentWrapper.classList.add('navigating');
128
+ }
129
+
130
+ // Fetch with abort signal
131
+ const response = await fetch(url, { signal: this.abortController.signal });
132
+ if (!response.ok) {
133
+ throw new Error(`HTTP ${response.status}`);
134
+ }
135
+
136
+ const html = await response.text();
137
+ const parser = new DOMParser();
138
+ const doc = parser.parseFromString(html, 'text/html');
139
+
140
+ // Extract the new content - update main-content-wrapper and sidebar if needed
141
+ const newMainContent = doc.querySelector('.main-content-wrapper');
142
+ const newBreadcrumb = doc.querySelector('.breadcrumb-section');
143
+ const newSidebar = doc.querySelector('.file-tree-sidebar');
144
+
145
+ if (!newMainContent) {
146
+ throw new Error('Content not found: missing .main-content-wrapper in response');
147
+ }
148
+
149
+ // If current wrapper doesn't exist, create it (shouldn't happen, but handle gracefully)
150
+ if (!this.mainContentWrapper) {
151
+ const main = document.querySelector('main');
152
+ if (!main) {
153
+ throw new Error('Content not found: missing <main> element');
154
+ }
155
+ // Create wrapper if it doesn't exist (edge case)
156
+ const wrapper = document.createElement('div');
157
+ wrapper.className = 'main-content-wrapper';
158
+ main.appendChild(wrapper);
159
+ this.mainContentWrapper = wrapper;
160
+ }
161
+
162
+ // Batch DOM updates
163
+ requestAnimationFrame(() => {
164
+ // Re-find in case reference changed
165
+ const wrapper = document.querySelector('.main-content-wrapper');
166
+ const main = document.querySelector('main');
167
+
168
+ if (!wrapper) {
169
+ this.removeNavigatingClass();
170
+ return;
171
+ }
172
+
173
+ // Update sidebar visibility based on path (always check, even if not in new content)
174
+ if (main) {
175
+ const currentSidebar = main.querySelector('.file-tree-sidebar');
176
+ const hasPath = path && path !== '';
177
+
178
+ if (currentSidebar) {
179
+ // Always update visibility based on path
180
+ if (hasPath) {
181
+ currentSidebar.classList.remove('hidden');
182
+ // Update sidebar content if new sidebar exists and structure changed
183
+ if (newSidebar) {
184
+ const newSidebarContent = newSidebar.innerHTML;
185
+ if (newSidebarContent !== currentSidebar.innerHTML) {
186
+ currentSidebar.innerHTML = newSidebarContent;
187
+ }
188
+ }
189
+ } else {
190
+ // Hide sidebar when navigating to root
191
+ currentSidebar.classList.add('hidden');
192
+ }
193
+ } else if (hasPath && newSidebar) {
194
+ // Sidebar doesn't exist but should - insert it before main-content-wrapper
195
+ const sidebarClone = newSidebar.cloneNode(true);
196
+ sidebarClone.classList.remove('hidden');
197
+ main.insertBefore(sidebarClone, wrapper);
198
+ }
199
+ }
200
+
201
+ // Update breadcrumbs if they exist
202
+ if (newBreadcrumb) {
203
+ const oldBreadcrumb = wrapper.parentElement?.querySelector('.breadcrumb-section');
204
+ if (oldBreadcrumb) {
205
+ oldBreadcrumb.replaceWith(newBreadcrumb.cloneNode(true));
206
+ }
207
+ }
208
+
209
+ // Update main content wrapper content
210
+ wrapper.innerHTML = newMainContent.innerHTML;
211
+ wrapper.className = newMainContent.className;
212
+
213
+ // Update browser URL
214
+ if (pushState) {
215
+ window.history.pushState({ path, view }, '', url);
216
+ }
217
+
218
+ // Fade in new content - always remove navigating class
219
+ wrapper.classList.remove('navigating');
220
+ this.mainContentWrapper = wrapper; // Update reference
221
+
222
+ // Notify that content has changed
223
+ document.dispatchEvent(new CustomEvent('content-loaded'));
224
+ });
225
+ } catch (error) {
226
+ if (error.name === 'AbortError') {
227
+ // Request was cancelled - still need to clean up
228
+ this.removeNavigatingClass();
229
+ this.isNavigating = false;
230
+ this.abortController = null;
231
+ return;
232
+ }
233
+
234
+ // Log more detailed error info
235
+ console.error('Navigation failed:', {
236
+ error: error.message,
237
+ url,
238
+ path,
239
+ view,
240
+ hasWrapper: !!this.mainContentWrapper,
241
+ timestamp: new Date().toISOString()
242
+ });
243
+
244
+ // Always restore state on error
245
+ this.removeNavigatingClass();
246
+
247
+ // Fall back to full page load
248
+ window.location.href = url;
249
+ } finally {
250
+ this.isNavigating = false;
251
+ this.abortController = null;
252
+ }
253
+ }
254
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Notification system
3
+ */
4
+
5
+ import { CONFIG } from './constants.js';
6
+
7
+ export function showNotification(message, type = 'info') {
8
+ const existingNotifications = document.querySelectorAll('.notification');
9
+ existingNotifications.forEach(n => n.remove());
10
+
11
+ const notification = document.createElement('div');
12
+ notification.className = `notification notification-${type}`;
13
+ notification.textContent = message;
14
+
15
+ document.body.appendChild(notification);
16
+
17
+ setTimeout(() => {
18
+ if (notification.parentNode) {
19
+ notification.style.opacity = '0';
20
+ setTimeout(() => notification.remove(), 300);
21
+ }
22
+ }, CONFIG.NOTIFICATION_DURATION);
23
+ }
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Context-aware file search
3
+ * - Directory pages: Global repository search
4
+ * - File view pages: Filter sidebar tree
5
+ */
6
+
7
+ export class SearchHandler {
8
+ constructor() {
9
+ this.fileTree = document.getElementById('file-tree');
10
+ this.fileTable = document.getElementById('file-table');
11
+ this.debounceTimer = null;
12
+ this.searchOverlay = null;
13
+ this.treeItems = [];
14
+
15
+ // Determine context based on page elements
16
+ this.isFileViewContext = !!this.fileTree && !this.fileTable;
17
+
18
+ // Use correct search input for context
19
+ const searchId = this.isFileViewContext ? 'file-search' : 'root-file-search';
20
+ this.searchInput = document.getElementById(searchId);
21
+
22
+ this.init();
23
+ }
24
+
25
+ init() {
26
+ if (!this.searchInput) return;
27
+
28
+ if (this.isFileViewContext) {
29
+ this.updateTreeItems();
30
+ document.addEventListener('filetree-loaded', () => this.updateTreeItems());
31
+ }
32
+
33
+ this.setupListeners();
34
+ }
35
+
36
+ setupListeners() {
37
+ if (!this.searchInput) return;
38
+
39
+ this.searchInput.addEventListener('input', () => this.handleSearchInput());
40
+
41
+ if (!this.isFileViewContext) {
42
+ // Global search: show results on focus
43
+ this.searchInput.addEventListener('focus', () => this.handleSearchFocus());
44
+
45
+ // Close results on click outside
46
+ document.addEventListener('click', (e) => {
47
+ if (this.searchOverlay &&
48
+ !e.target.closest('.search-container') &&
49
+ !e.target.closest('.search-results-overlay')) {
50
+ this.hideResults();
51
+ }
52
+ });
53
+ }
54
+ }
55
+
56
+ updateTreeItems() {
57
+ if (this.fileTree) {
58
+ this.treeItems = Array.from(this.fileTree.querySelectorAll('.tree-item'));
59
+ }
60
+ }
61
+
62
+ handleSearchFocus() {
63
+ if (this.isFileViewContext) return;
64
+
65
+ const query = this.searchInput.value.trim();
66
+ if (query) {
67
+ this.performGlobalSearch(query);
68
+ }
69
+ }
70
+
71
+ handleSearchInput() {
72
+ if (this.debounceTimer) {
73
+ clearTimeout(this.debounceTimer);
74
+ }
75
+
76
+ const query = this.searchInput.value.trim();
77
+
78
+ if (!query) {
79
+ if (this.isFileViewContext) {
80
+ this.clearTreeFilter();
81
+ } else {
82
+ this.hideResults();
83
+ }
84
+ return;
85
+ }
86
+
87
+ this.debounceTimer = setTimeout(() => {
88
+ if (this.isFileViewContext) {
89
+ this.filterTree(query);
90
+ } else {
91
+ this.performGlobalSearch(query);
92
+ }
93
+ }, 200);
94
+ }
95
+
96
+ // File view context: Filter sidebar tree
97
+ filterTree(query) {
98
+ if (!this.fileTree || this.treeItems.length === 0) return;
99
+
100
+ const queryLower = query.toLowerCase();
101
+ const containers = this.fileTree.querySelectorAll('.tree-children');
102
+
103
+ // Hide all items first
104
+ this.treeItems.forEach(item => item.style.display = 'none');
105
+ containers.forEach(container => container.style.display = 'none');
106
+
107
+ // Show matching items and their parent folders
108
+ this.treeItems.forEach(item => {
109
+ const label = item.querySelector('.tree-label');
110
+ const fileName = label?.textContent?.toLowerCase() || '';
111
+
112
+ if (fileName.includes(queryLower)) {
113
+ item.style.display = '';
114
+
115
+ // Show all parent folders
116
+ let parent = item.parentElement;
117
+ while (parent && parent !== this.fileTree) {
118
+ if (parent.classList.contains('tree-children')) {
119
+ parent.style.display = '';
120
+ }
121
+ const parentItem = parent.previousElementSibling;
122
+ if (parentItem?.classList.contains('tree-item')) {
123
+ parentItem.style.display = '';
124
+ }
125
+ parent = parent.parentElement;
126
+ }
127
+ }
128
+ });
129
+ }
130
+
131
+ clearTreeFilter() {
132
+ if (!this.fileTree) return;
133
+
134
+ this.treeItems.forEach(item => item.style.display = '');
135
+ this.fileTree.querySelectorAll('.tree-children').forEach(container => {
136
+ container.style.display = '';
137
+ });
138
+ }
139
+
140
+ // Directory context: Global repository search
141
+ async performGlobalSearch(query) {
142
+ try {
143
+ const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
144
+ const data = await response.json();
145
+
146
+ if (data.success) {
147
+ this.showResults(data.results, query);
148
+ }
149
+ } catch (error) {
150
+ console.error('Search failed:', error);
151
+ }
152
+ }
153
+
154
+ showResults(results, query) {
155
+ this.hideResults();
156
+
157
+ this.searchOverlay = document.createElement('div');
158
+ this.searchOverlay.className = 'search-results-overlay';
159
+
160
+ if (results.length === 0) {
161
+ this.searchOverlay.innerHTML = `
162
+ <div class="search-results-container">
163
+ <div class="search-results-header">
164
+ <span class="search-results-count">No results for "${query}"</span>
165
+ </div>
166
+ </div>
167
+ `;
168
+ } else {
169
+ const resultsList = results.map(result => {
170
+ const icon = result.isDirectory
171
+ ? '<svg class="result-icon" viewBox="0 0 16 16" width="16" height="16"><path d="M1.75 1A1.75 1.75 0 0 0 0 2.75v10.5C0 14.216.784 15 1.75 15h12.5A1.75 1.75 0 0 0 16 13.25v-8.5A1.75 1.75 0 0 0 14.25 3H7.5a.25.25 0 0 1-.2-.1l-.9-1.2C6.07 1.26 5.55 1 5 1H1.75Z"></path></svg>'
172
+ : '<svg class="result-icon file-icon" viewBox="0 0 16 16" width="16" height="16"><path d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h9.5a.25.25 0 0 0 .25-.25V6h-2.75A1.75 1.75 0 0 1 9 4.25V1.5Zm6.75.062V4.25c0 .138.112.25.25.25h2.688l-.011-.013-2.914-2.914-.013-.011Z"></path></svg>';
173
+
174
+ return `
175
+ <a href="/?path=${encodeURIComponent(result.path)}" class="search-result-item" data-path="${result.path}">
176
+ ${icon}
177
+ <div class="search-result-content">
178
+ <div class="search-result-path">${this.highlightMatch(result.path, query)}</div>
179
+ </div>
180
+ </a>
181
+ `;
182
+ }).join('');
183
+
184
+ this.searchOverlay.innerHTML = `
185
+ <div class="search-results-container">
186
+ <div class="search-results-header">
187
+ <span class="search-results-count">${results.length} result${results.length === 1 ? '' : 's'}</span>
188
+ </div>
189
+ <div class="search-results-list">
190
+ ${resultsList}
191
+ </div>
192
+ </div>
193
+ `;
194
+ }
195
+
196
+ const searchContainer = this.searchInput.closest('.search-container');
197
+ if (searchContainer) {
198
+ searchContainer.appendChild(this.searchOverlay);
199
+ }
200
+
201
+ this.searchOverlay.querySelectorAll('.search-result-item').forEach(item => {
202
+ item.addEventListener('click', (e) => {
203
+ e.preventDefault();
204
+ window.location.href = item.getAttribute('href');
205
+ });
206
+ });
207
+ }
208
+
209
+ highlightMatch(text, query) {
210
+ if (!query) return text;
211
+
212
+ const lowerText = text.toLowerCase();
213
+ const lowerQuery = query.toLowerCase();
214
+ const index = lowerText.indexOf(lowerQuery);
215
+
216
+ if (index === -1) return text;
217
+
218
+ const before = text.substring(0, index);
219
+ const match = text.substring(index, index + query.length);
220
+ const after = text.substring(index + query.length);
221
+
222
+ return `${before}<mark class="search-highlight">${match}</mark>${after}`;
223
+ }
224
+
225
+ hideResults() {
226
+ if (this.searchOverlay) {
227
+ this.searchOverlay.remove();
228
+ this.searchOverlay = null;
229
+ }
230
+ }
231
+
232
+ focusSearch() {
233
+ if (this.searchInput) {
234
+ this.searchInput.focus();
235
+ this.searchInput.select();
236
+ }
237
+ }
238
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Theme management
3
+ */
4
+
5
+ import { THEME, STORAGE_KEYS } from './constants.js';
6
+
7
+ export class ThemeManager {
8
+ constructor() {
9
+ this.html = document.documentElement;
10
+ this.themeToggle = null;
11
+ this.currentTheme = THEME.DARK;
12
+ this.init();
13
+ }
14
+
15
+ init() {
16
+ const systemPrefersDark = window.matchMedia?.('(prefers-color-scheme: dark)').matches;
17
+ const systemTheme = systemPrefersDark ? THEME.DARK : THEME.LIGHT;
18
+ const savedTheme = localStorage.getItem(STORAGE_KEYS.THEME) || systemTheme;
19
+
20
+ this.currentTheme = savedTheme;
21
+ this.setTheme(savedTheme);
22
+
23
+ // Setup listeners - try immediately, then retry after DOM is ready
24
+ this.setupListeners();
25
+
26
+ if (document.readyState === 'loading') {
27
+ document.addEventListener('DOMContentLoaded', () => this.setupListeners());
28
+ }
29
+
30
+ // Also re-setup after content changes (navigation)
31
+ document.addEventListener('content-loaded', () => this.setupListeners());
32
+ }
33
+
34
+ setupListeners() {
35
+ // Re-find the theme toggle button (it might be replaced after navigation)
36
+ const themeToggle = document.getElementById('theme-toggle');
37
+
38
+ if (themeToggle) {
39
+ // Remove old listener if button was replaced
40
+ if (this.themeToggle && this.themeToggle !== themeToggle && this.themeToggleClickHandler) {
41
+ this.themeToggle.removeEventListener('click', this.themeToggleClickHandler);
42
+ }
43
+
44
+ // Set up new listener if we don't have one yet or button changed
45
+ if (!this.themeToggle || this.themeToggle !== themeToggle || !this.themeToggleClickHandler) {
46
+ this.themeToggle = themeToggle;
47
+ this.themeToggleClickHandler = () => this.toggleTheme();
48
+ this.themeToggle.addEventListener('click', this.themeToggleClickHandler);
49
+ }
50
+
51
+ // Update icon for current theme
52
+ this.updateThemeIcon(this.currentTheme);
53
+ }
54
+
55
+ // Setup system theme listener (only once)
56
+ if (window.matchMedia && !this.mediaQueryListener) {
57
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
58
+ this.mediaQueryListener = (e) => {
59
+ if (!localStorage.getItem(STORAGE_KEYS.THEME)) {
60
+ const newTheme = e.matches ? THEME.DARK : THEME.LIGHT;
61
+ this.setTheme(newTheme);
62
+ }
63
+ };
64
+ // Use addEventListener if available (modern), fallback to addListener
65
+ if (mediaQuery.addEventListener) {
66
+ mediaQuery.addEventListener('change', this.mediaQueryListener);
67
+ } else {
68
+ mediaQuery.addListener(this.mediaQueryListener);
69
+ }
70
+ }
71
+ }
72
+
73
+ toggleTheme() {
74
+ const newTheme = this.currentTheme === THEME.DARK ? THEME.LIGHT : THEME.DARK;
75
+
76
+ this.currentTheme = newTheme;
77
+ this.setTheme(newTheme);
78
+ localStorage.setItem(STORAGE_KEYS.THEME, newTheme);
79
+
80
+ if (typeof monaco !== 'undefined') {
81
+ const monacoTheme = newTheme === THEME.DARK ? 'vs-dark' : 'vs';
82
+ monaco.editor.setTheme(monacoTheme);
83
+ }
84
+ }
85
+
86
+ setTheme(theme) {
87
+ this.currentTheme = theme;
88
+ this.html.setAttribute('data-theme', theme);
89
+ this.updateThemeIcon(theme);
90
+ }
91
+
92
+ updateThemeIcon(theme) {
93
+ const iconSvg = this.themeToggle?.querySelector('.theme-icon');
94
+ if (!iconSvg) {
95
+ return;
96
+ }
97
+
98
+ if (theme === THEME.DARK) {
99
+ iconSvg.innerHTML = '<path d="M8 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0V.75A.75.75 0 0 1 8 0zm0 13a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 8 13zM2.343 2.343a.75.75 0 0 1 1.061 0l1.06 1.061a.75.75 0 0 1-1.06 1.06l-1.06-1.06a.75.75 0 0 1 0-1.061zm9.193 9.193a.75.75 0 0 1 1.06 0l1.061 1.06a.75.75 0 0 1-1.06 1.061l-1.061-1.06a.75.75 0 0 1 0-1.061zM16 8a.75.75 0 0 1-.75.75h-1.5a.75.75 0 0 1 0-1.5h1.5A.75.75 0 0 1 16 8zM3 8a.75.75 0 0 1-.75.75H.75a.75.75 0 0 1 0-1.5h1.5A.75.75 0 0 1 3 8zm10.657-5.657a.75.75 0 0 1 0 1.061l-1.061 1.06a.75.75 0 1 1-1.06-1.06l1.06-1.06a.75.75 0 0 1 1.061 0zm-9.193 9.193a.75.75 0 0 1 0 1.06l-1.06 1.061a.75.75 0 1 1-1.061-1.06l1.06-1.061a.75.75 0 0 1 1.061 0z"></path>';
100
+ } else {
101
+ iconSvg.innerHTML = '<path d="M9.598 1.591a.75.75 0 0 1 .785-.175 7 7 0 1 1-8.967 8.967.75.75 0 0 1 .961-.96 5.5 5.5 0 0 0 7.046-7.046.75.75 0 0 1 .175-.786zm1.616 1.945a7 7 0 0 1-7.678 7.678 5.5 5.5 0 1 0 7.678-7.678z"></path>';
102
+ }
103
+ }
104
+
105
+ getCurrentTheme() {
106
+ return this.currentTheme;
107
+ }
108
+ }