diff --git a/main.js b/main.js index 13f3ba5..45b12e1 100644 --- a/main.js +++ b/main.js @@ -99,74 +99,13 @@ function createWindow(startUrl) { win.loadURL(url); }); - // ensure all embedded tags also use the same window + // ensure all embedded tags behave predictably without heavy injections win.webContents.on('did-attach-webview', (event, webContents) => { - // Set up webview with preload script to provide electronAPI - fixed injection - webContents.on('dom-ready', () => { - // Simpler, more reliable API injection that doesn't require cloning - webContents.executeJavaScript(` - if (!window.electronAPI) { - // Create a simple bridge without complex objects - window.electronAPI = { - invoke: function(channel) { - const args = Array.prototype.slice.call(arguments, 1); - return new Promise(function(resolve, reject) { - try { - const ipcRenderer = require('electron').ipcRenderer; - ipcRenderer.invoke(channel, ...args).then(resolve).catch(reject); - } catch (err) { - reject(err); - } - }); - } - }; - console.log('electronAPI injected successfully'); - } - `).catch(err => { - console.error('Failed to inject electronAPI:', err); - // Fallback: inject minimal API - webContents.executeJavaScript(` - window.electronAPI = { invoke: function() { return Promise.resolve(); } }; - `).catch(() => {}); - }); - }); - - // intercept window.open() inside webview + // Let the renderer/webview handle navigation; avoid extra JS injection that can stall webContents.setWindowOpenHandler(({ url }) => { webContents.loadURL(url); - // record history for webview navigations - recordHistory('site-history.json', url); - const m = /[?&](?:q|query)=([^&]+)/.exec(url); - if (m && m[1]) { - const query = decodeURIComponent(m[1].replace(/\+/g, ' ')); - recordHistory('search-history.json', query); - } return { action: 'deny' }; }); - // intercept legacy new-window on webview - webContents.on('new-window', (e, url) => { - e.preventDefault(); - webContents.loadURL(url); - // record history for webview navigations - recordHistory('site-history.json', url); - const m = /[?&](?:q|query)=([^&]+)/.exec(url); - if (m && m[1]) { - const query = decodeURIComponent(m[1].replace(/\+/g, ' ')); - recordHistory('search-history.json', query); - } - }); - // intercept navigation on webview (e.g. user clicks link) - webContents.on('will-navigate', (e, url) => { - e.preventDefault(); - webContents.loadURL(url); - // record history for webview navigations - recordHistory('site-history.json', url); - const m = /[?&](?:q|query)=([^&]+)/.exec(url); - if (m && m[1]) { - const query = decodeURIComponent(m[1].replace(/\+/g, ' ')); - recordHistory('search-history.json', query); - } - }); }); win.loadFile('renderer/index.html'); @@ -189,69 +128,7 @@ function createWindow(startUrl) { perfMonitor.trackLoadTime(win.webContents.getURL(), loadTime); }); - // Debounced history recording to prevent excessive I/O - let historyTimeout; - const recordHistory = async (fileName, entry) => { - // Clear existing timeout - if (historyTimeout) { - clearTimeout(historyTimeout); - } - - // Debounce history recording by 500ms - historyTimeout = setTimeout(async () => { - if (fileName === 'site-history.json') { - // Save to both file and send to renderer - const filePath = path.join(__dirname, fileName); - let data = []; - try { - const fileContent = fs.readFileSync(filePath, 'utf8'); - data = JSON.parse(fileContent); - } catch {} - - if (data[0] !== entry) { - data.unshift(entry); - if (data.length > 100) data.pop(); - - // Use async file operations to prevent blocking - try { - await fs.promises.writeFile(filePath, JSON.stringify(data, null, 2)); - } catch (err) { - console.error('Error writing site history:', err); - } - } - // Also send to renderer for localStorage - win.webContents.send('record-site-history', entry); - } else { - // Keep search history in JSON file for now - const filePath = path.join(__dirname, fileName); - let data = []; - try { - const fileContent = fs.readFileSync(filePath, 'utf8'); - data = JSON.parse(fileContent); - } catch {} - - if (data[0] !== entry) { - data.unshift(entry); - if (data.length > 100) data.pop(); - - try { - await fs.promises.writeFile(filePath, JSON.stringify(data, null, 2)); - } catch (err) { - console.error('Error writing search history:', err); - } - } - } - }, 500); - }; - - win.webContents.on('did-navigate', (event, url) => { - recordHistory('site-history.json', url); - const m = /[?&](?:q|query)=([^&]+)/.exec(url); - if (m && m[1]) { - const query = decodeURIComponent(m[1].replace(/\+/g, ' ')); - recordHistory('search-history.json', query); - } - }); + // Renderer manages history; no main-process recording here } // This method will be called when Electron has finished initialization @@ -350,10 +227,9 @@ ipcMain.handle('window-close', event => { // Site history is now handled via localStorage in the renderer // But keep these handlers for compatibility and potential future use ipcMain.handle('load-site-history', async () => { - // Read from the site history file for settings page const filePath = path.join(__dirname, 'site-history.json'); try { - const data = fs.readFileSync(filePath, 'utf-8'); + const data = await fs.promises.readFile(filePath, 'utf-8'); return JSON.parse(data); } catch (err) { return []; @@ -361,10 +237,9 @@ ipcMain.handle('load-site-history', async () => { }); ipcMain.handle('save-site-history', async (event, history) => { - // Save to both file and localStorage const filePath = path.join(__dirname, 'site-history.json'); try { - fs.writeFileSync(filePath, JSON.stringify(history, null, 2)); + await fs.promises.writeFile(filePath, JSON.stringify(history, null, 2)); return true; } catch (err) { return false; @@ -374,7 +249,7 @@ ipcMain.handle('save-site-history', async (event, history) => { ipcMain.handle('clear-site-history', async () => { const filePath = path.join(__dirname, 'site-history.json'); try { - fs.writeFileSync(filePath, JSON.stringify([], null, 2)); + await fs.promises.writeFile(filePath, JSON.stringify([], null, 2)); return true; } catch (err) { return false; @@ -384,7 +259,7 @@ ipcMain.handle('clear-site-history', async () => { ipcMain.handle('load-search-history', async () => { const filePath = path.join(__dirname, 'search-history.json'); try { - const data = fs.readFileSync(filePath, 'utf-8'); + const data = await fs.promises.readFile(filePath, 'utf-8'); return JSON.parse(data); } catch (err) { return []; @@ -394,7 +269,7 @@ ipcMain.handle('load-search-history', async () => { ipcMain.handle('save-search-history', async (event, history) => { const filePath = path.join(__dirname, 'search-history.json'); try { - fs.writeFileSync(filePath, JSON.stringify(history, null, 2)); + await fs.promises.writeFile(filePath, JSON.stringify(history, null, 2)); return true; } catch (err) { return false; @@ -410,24 +285,24 @@ ipcMain.on('homepage-changed', (event, url) => { ipcMain.handle('load-bookmarks', async () => { try { const bookmarksPath = path.join(__dirname, 'bookmarks.json'); - if (fs.existsSync(bookmarksPath)) { - const data = fs.readFileSync(bookmarksPath, 'utf8'); - const bookmarks = JSON.parse(data); - console.log(`Loaded ${bookmarks.length} bookmarks from file`); - return bookmarks; + try { + await fs.promises.access(bookmarksPath); + } catch { + console.log('No bookmarks file found, starting with empty array'); + return []; } - console.log('No bookmarks file found, starting with empty array'); - return []; + const data = await fs.promises.readFile(bookmarksPath, 'utf8'); + const bookmarks = JSON.parse(data); + console.log(`Loaded ${bookmarks.length} bookmarks from file`); + return bookmarks; } catch (error) { console.error('Error loading bookmarks:', error); // Try to create a backup if the file is corrupted const bookmarksPath = path.join(__dirname, 'bookmarks.json'); const backupPath = path.join(__dirname, `bookmarks.backup.${Date.now()}.json`); try { - if (fs.existsSync(bookmarksPath)) { - fs.copyFileSync(bookmarksPath, backupPath); - console.log(`Corrupted bookmarks file backed up to: ${backupPath}`); - } + await fs.promises.copyFile(bookmarksPath, backupPath); + console.log(`Corrupted bookmarks file backed up to: ${backupPath}`); } catch (backupError) { console.error('Failed to create backup:', backupError); } @@ -438,14 +313,12 @@ ipcMain.handle('load-bookmarks', async () => { ipcMain.handle('save-bookmarks', async (event, bookmarks) => { try { const bookmarksPath = path.join(__dirname, 'bookmarks.json'); - - // Create a backup before saving - if (fs.existsSync(bookmarksPath)) { + try { + await fs.promises.access(bookmarksPath); const backupPath = path.join(__dirname, 'bookmarks.backup.json'); - fs.copyFileSync(bookmarksPath, backupPath); - } - - fs.writeFileSync(bookmarksPath, JSON.stringify(bookmarks, null, 2)); + await fs.promises.copyFile(bookmarksPath, backupPath); + } catch {} + await fs.promises.writeFile(bookmarksPath, JSON.stringify(bookmarks, null, 2)); console.log(`Saved ${bookmarks.length} bookmarks to file`); return true; } catch (error) { @@ -514,21 +387,16 @@ ipcMain.handle('save-site-history-entry', async (event, url) => { const filePath = path.join(__dirname, 'site-history.json'); try { let data = []; - try { - data = JSON.parse(fs.readFileSync(filePath, 'utf8')); + try { + const raw = await fs.promises.readFile(filePath, 'utf8'); + data = JSON.parse(raw); } catch {} - // Remove if already exists to avoid duplicates data = data.filter(item => item !== url); - // Add to beginning + // Add to beginning and clamp size data.unshift(url); - // Keep only last 100 entries - if (data.length > 100) { - data = data.slice(0, 100); - } - - fs.writeFileSync(filePath, JSON.stringify(data, null, 2)); - console.log('[MAIN] Saved site history entry:', url); + if (data.length > 100) data = data.slice(0, 100); + await fs.promises.writeFile(filePath, JSON.stringify(data, null, 2)); return true; } catch (err) { console.error('[MAIN] Error saving site history entry:', err); diff --git a/renderer/script.js b/renderer/script.js index 077a5be..91935c4 100644 --- a/renderer/script.js +++ b/renderer/script.js @@ -53,6 +53,30 @@ let activeTabId = null; const allowedInternalPages = ['settings', 'home']; let bookmarks = []; +// Efficient render scheduling to avoid redundant DOM work +let tabsRenderPending = false; +function scheduleRenderTabs() { + if (tabsRenderPending) return; + tabsRenderPending = true; + requestAnimationFrame(() => { + tabsRenderPending = false; + renderTabs(); + }); +} + +// Derive a stable, safe label for a tab without throwing on non-URLs +function getTabLabel(tab) { + if (tab.title && tab.title !== 'New Tab') return tab.title; + const u = tab.url || ''; + try { + if (u.startsWith('http')) return new URL(u).hostname; + if (u.startsWith('browser://')) return u.replace('browser://', ''); + return u || 'New Tab'; + } catch { + return u || 'New Tab'; + } +} + // Load bookmarks on startup async function loadBookmarks() { try { @@ -107,9 +131,9 @@ function createTab(inputUrl) { isHome: true }; tabs.push(tab); - setActiveTab(id); - // Render the tab bar so the new home tab appears - renderTabs(); + setActiveTab(id); + // Render the tab bar so the new home tab appears + scheduleRenderTabs(); return id; } @@ -208,7 +232,7 @@ function createTab(inputUrl) { }); setActiveTab(id); - renderTabs(); + scheduleRenderTabs(); } @@ -236,7 +260,7 @@ function updateTabMetadata(id, key, value) { const tab = tabs.find(t => t.id === id); if (tab) { tab[key] = value; - renderTabs(); + scheduleRenderTabs(); } } @@ -276,7 +300,7 @@ function navigate() { tab.url = input; webview.src = resolved; - renderTabs(); + scheduleRenderTabs(); updateNavButtons(); } @@ -363,7 +387,7 @@ function convertHomeTabToWebview(tabId, inputUrl, resolvedUrl) { updateNavButtons(); // Activate converted webview tab and update UI setActiveTab(tabId); - renderTabs(); + scheduleRenderTabs(); } function handleNavigation(tabId, newUrl) { @@ -406,7 +430,7 @@ function handleNavigation(tabId, newUrl) { urlBox.value = displayUrl === 'browser://home' ? '' : displayUrl; } - renderTabs(); + scheduleRenderTabs(); updateNavButtons(); } @@ -439,7 +463,7 @@ function setActiveTab(id) { if (tab) { // If the tab URL represents the home page, keep the URL bar blank. urlBox.value = tab.url === 'browser://home' ? '' : tab.url; - renderTabs(); + scheduleRenderTabs(); updateNavButtons(); updateZoomUI(); // ← update zoom display for new active tab } @@ -455,7 +479,7 @@ function closeTab(id) { if (tabs.length > 0) setActiveTab(tabs[0].id); } - renderTabs(); + scheduleRenderTabs(); updateNavButtons(); } @@ -475,7 +499,7 @@ function renderTabs() { el.appendChild(icon); } - el.appendChild(document.createTextNode(tab.title || new URL(tab.url).hostname)); + el.appendChild(document.createTextNode(getTabLabel(tab))); const closeBtn = document.createElement('button'); closeBtn.textContent = '×'; @@ -572,17 +596,7 @@ menuBtn.addEventListener('click', () => { }); window.addEventListener('DOMContentLoaded', () => { - // Add some debug info - console.log('[DEBUG] Site history initialized, current entries:', getSiteHistory().length); - - // Add test entries if none exist (for debugging) - if (getSiteHistory().length === 0) { - console.log('[DEBUG] No existing history, adding test entries for debugging'); - addToSiteHistory('https://www.google.com'); - addToSiteHistory('https://github.com'); - console.log('[DEBUG] Test entries added, history now:', getSiteHistory()); - } - + // Initial boot createTab(); // Handle IPC messages from the static home webview (bookmarks navigation) const staticHome = document.getElementById('home-webview'); @@ -651,65 +665,7 @@ window.addEventListener('DOMContentLoaded', () => { document.getElementById('zoom-percent').textContent = `${Math.round(z * 100)}%`; }); - // menu‐related code (moved here so #context-menu exists) - const items = contextMenu ? contextMenu.querySelectorAll('li') : []; - - function showContextMenu(x, y) { - if (!menu) return; - menu.style.top = `${y}px`; - menu.style.left = `${x}px`; - menu.classList.add('visible'); - } - - document.addEventListener('contextmenu', e => { - if (e.target.tagName === 'WEBVIEW' || - e.composedPath().some(el => el.id === 'webviews')) { - e.preventDefault(); - showContextMenu(e.clientX, e.clientY); - } - }); - - document.addEventListener('click', () => { - if (menu) menu.classList.remove('visible'); - }); - - items.forEach(item => { - item.addEventListener('click', async () => { - const action = item.dataset.action; - const win = remote.getCurrentWindow(); - - switch (action) { - case 'save-page': { - const { canceled, filePath } = await remote.dialog.showSaveDialog(win, { defaultPath: 'page.html' }); - if (!canceled && filePath) win.webContents.savePage(filePath, 'HTMLComplete'); - break; - } - case 'select-all': - document.execCommand('selectAll'); - break; - case 'screenshot': { - const image = await win.webContents.capturePage(); - const { canceled, filePath } = await remote.dialog.showSaveDialog(win, { defaultPath: 'screenshot.png' }); - if (!canceled && filePath) fs.writeFileSync(filePath, image.toPNG()); - break; - } - case 'view-source': { - const html = document.documentElement.outerHTML; - const { canceled, filePath } = await remote.dialog.showSaveDialog(win, { defaultPath: 'source.html' }); - if (!canceled && filePath) fs.writeFileSync(filePath, html); - break; - } - case 'inspect-accessibility': - win.webContents.inspectAccessibilityNode(e.clientX, e.clientY); - break; - case 'inspect-element': - win.webContents.inspectElement(e.clientX, e.clientY); - break; - } - - menu.classList.remove('visible'); - }); - }); + // (Removed broken duplicate context menu wiring) // Migrate existing site history from JSON file to localStorage (one-time migration) const migrateSiteHistory = async () => { diff --git a/renderer/settings.css b/renderer/settings.css index c5d83a5..d545028 100644 --- a/renderer/settings.css +++ b/renderer/settings.css @@ -29,11 +29,73 @@ body { .container { background-color: var(--dark-purple); - padding: 2rem; + padding: 0; border-radius: 16px; box-shadow: 0 0 10px rgba(0,0,0,0.5); - max-width: 500px; + max-width: 1100px; width: 100%; + display: flex; + overflow: hidden; +} + +/* Sidebar + content layout */ +.sidebar { + width: 260px; + background: rgba(0,0,0,0.2); + border-right: 1px solid rgba(255,255,255,0.08); + padding: 1.25rem 1rem; +} + +.sidebar h1 { + font-size: 1.2rem; + margin: 0 0 1rem 0; + color: var(--primary); +} + +.tabs { + display: flex; + flex-direction: column; + gap: 4px; +} + +.tab-link { + text-align: left; + background: transparent; + color: var(--text); + border: none; + border-radius: 8px; + padding: 10px 12px; + cursor: pointer; + transition: background 0.15s ease, color 0.15s ease; + font-size: 14px; + font-family: inherit; + width: 100%; + position: relative; + z-index: 1; +} + +.tab-link:hover { + background: rgba(255,255,255,0.06); +} + +.tab-link.active { + background: rgba(123, 46, 255, 0.18); + color: #fff; + border: 1px solid rgba(123,46,255,0.35); +} + +.content { + flex: 1; + padding: 1.25rem 1.5rem 2rem 1.5rem; + overflow: auto; +} + +.tab-panel { + display: none; +} + +.tab-panel.active { + display: block; } h1 { @@ -132,10 +194,20 @@ button:hover { /* small-screen adjustments */ @media (max-width: 480px) { .container { - padding: 1rem; - border-radius: 0; - box-shadow: none; + padding: 0; + border-radius: 12px; + box-shadow: 0 0 8px rgba(0,0,0,0.35); + flex-direction: column; + max-width: 100%; } + .sidebar { + width: 100%; + border-right: none; + border-bottom: 1px solid rgba(255,255,255,0.08); + padding-bottom: 0.5rem; + } + .tabs { flex-direction: row; flex-wrap: wrap; gap: 6px; } + .tab-link { flex: 1 1 auto; } h1 { font-size: 1.25rem; } diff --git a/renderer/settings.html b/renderer/settings.html index 6f84d6b..1bb1358 100644 --- a/renderer/settings.html +++ b/renderer/settings.html @@ -240,25 +240,35 @@ -
-

⚙️ Browser Settings

+
+ -
- - -
+
+ +
+

General

+
+ + +
+

Settings are stored locally on this device.

+
Loading debug info...
+
-

Settings are stored locally on this device.

- -
Loading debug info...
- - -
-

🎨 Browser Customization

- - -
-

Theme Presets

+ +
+

🎨 Appearance

+ +
+

Theme Presets

-
+
- -
-

Custom Colors

+ +
+

Custom Colors

@@ -340,11 +350,11 @@
-
+
- -
-

Home Page Layout

+ +
+

Home Page Layout

-
+
- -
-

Logo & Branding

+ +
+

Logo & Branding

-
+
- -
-

Theme Management

+ +
+

Theme Management

@@ -386,11 +396,11 @@
-
+
- -
-

Preview

+ +
+

Preview

@@ -403,21 +413,34 @@
-
-
+
+ - -
-

Search History

-
    - -
    -
    -

    Site History

    -
      - - -
      + +
      +

      History

      +
      +

      Search History

      +
        + +
        +
        +

        Site History

        +
          +
          + + +
          +
          +
          + + +
          +

          About

          +

          Nebula Browser Experimental Settings

          +

          Version info and links can go here.

          +
          +
          diff --git a/renderer/settings.js b/renderer/settings.js index 31c7d43..83705e7 100644 --- a/renderer/settings.js +++ b/renderer/settings.js @@ -1,10 +1,30 @@ -// Use require('electron') since webviews have nodeIntegrationInSubFrames: true -// In settings webview we use the same preload API as main windows -const { ipcRenderer } = require('electron'); +// Try to get ipcRenderer, but don't fail if it's not available +let ipcRenderer = null; +try { + if (typeof require !== 'undefined') { + const electron = require('electron'); + ipcRenderer = electron.ipcRenderer; + } +} catch (e) { + console.log('[SETTINGS] Electron IPC not available, some features may be limited'); +} -const clearBtn = document.getElementById('clear-data-btn'); +let clearBtn = document.getElementById('clear-data-btn'); const statusDiv = document.getElementById('status'); const statusText = document.getElementById('status-text'); +const TAB_STORAGE_KEY = 'nebula-settings-active-tab'; + +function showStatus(message) { + if (statusText && statusDiv) { + statusText.textContent = message; + statusDiv.classList.remove('hidden'); + setTimeout(() => { + statusDiv.classList.add('hidden'); + }, 2000); + } else { + console.log('[STATUS]', message); + } +} function showStatus(message) { statusText.textContent = message; @@ -14,23 +34,118 @@ function showStatus(message) { }, 2000); } -clearBtn.onclick = async () => { - statusDiv.classList.remove('hidden'); - statusText.textContent = 'Clearing all browser data...'; - - try { - const ok = await ipcRenderer.invoke('clear-browser-data'); - showStatus(ok - ? 'All browser data and bookmarks cleared!' - : 'Failed to clear browser data.'); - } catch (error) { - console.error('Error clearing browser data:', error); - showStatus('An error occurred while clearing data.'); - } finally { - // Send theme update to host after clearing - const currentTheme = window.browserCustomizer ? window.browserCustomizer.currentTheme : null; - if (currentTheme && window.electronAPI && typeof window.electronAPI.sendToHost === 'function') { - window.electronAPI.sendToHost('theme-update', currentTheme); +function attachClearHandler(btn) { + if (!btn) return; + btn.onclick = async () => { + if (statusDiv && statusText) { + statusDiv.classList.remove('hidden'); + statusText.textContent = 'Clearing all browser data...'; } + + try { + if (ipcRenderer) { + const ok = await ipcRenderer.invoke('clear-browser-data'); + showStatus(ok + ? 'All browser data and bookmarks cleared!' + : 'Failed to clear browser data.'); + } else { + showStatus('Clear data feature not available in this context.'); + } + } catch (error) { + console.error('Error clearing browser data:', error); + showStatus('An error occurred while clearing data.'); + } finally { + const currentTheme = window.browserCustomizer ? window.browserCustomizer.currentTheme : null; + if (currentTheme && window.electronAPI && typeof window.electronAPI.sendToHost === 'function') { + window.electronAPI.sendToHost('theme-update', currentTheme); + } + } + }; +} + +// Try attaching immediately, and again on DOMContentLoaded +attachClearHandler(clearBtn); +window.addEventListener('DOMContentLoaded', () => { + if (!clearBtn) { + clearBtn = document.getElementById('clear-data-btn'); + attachClearHandler(clearBtn); } -}; +}); + +// Tabs: simple controller +function activateTab(tabName) { + console.log('[TABS] Activating tab:', tabName); + const links = document.querySelectorAll('.tab-link'); + const panels = document.querySelectorAll('.tab-panel'); + console.log('[TABS] Found', links.length, 'tab links and', panels.length, 'panels'); + + links.forEach(l => { + const isActive = l.dataset.tab === tabName; + l.classList.toggle('active', isActive); + l.setAttribute('aria-selected', isActive ? 'true' : 'false'); + if (isActive) l.focus({ preventScroll: true }); + }); + panels.forEach(p => { + const isActive = p.id === `panel-${tabName}`; + p.classList.toggle('active', isActive); + p.hidden = !isActive; + console.log('[TABS] Panel', p.id, 'active:', isActive); + }); + try { localStorage.setItem(TAB_STORAGE_KEY, tabName); } catch {} +} + +function initTabs() { + console.log('[TABS] Initializing tabs...'); + const links = document.querySelectorAll('.tab-link'); + console.log('[TABS] Found tab links:', links.length); + + // Direct listeners (for accessibility focus handling) + links.forEach((link, index) => { + console.log('[TABS] Setting up listener for tab', index, 'with data-tab:', link.dataset.tab); + link.addEventListener('click', (e) => { + console.log('[TABS] Tab clicked:', link.dataset.tab); + e.preventDefault(); + e.stopPropagation(); + const name = link.dataset.tab; + if (!name) return; + if (location.hash !== `#${name}`) { + history.replaceState(null, '', `#${name}`); + } + activateTab(name); + }); + }); + + // Delegation as a fallback if elements are re-rendered + const tabContainer = document.querySelector('.tabs'); + if (tabContainer) { + console.log('[TABS] Setting up delegation on tabs container'); + tabContainer.addEventListener('click', (e) => { + console.log('[TABS] Container click detected'); + const btn = e.target && e.target.closest ? e.target.closest('.tab-link') : null; + if (!btn || !tabContainer.contains(btn)) return; + console.log('[TABS] Delegated click for tab:', btn.dataset.tab); + const name = btn.dataset.tab; + if (!name) return; + if (location.hash !== `#${name}`) { + history.replaceState(null, '', `#${name}`); + } + activateTab(name); + }); + } + + // Resolve initial tab: hash > storage > default 'general' + let initial = (location.hash || '').replace('#', '') || null; + if (!initial) { + try { initial = localStorage.getItem(TAB_STORAGE_KEY) || null; } catch {} + } + if (!initial) initial = 'general'; + console.log('[TABS] Initial tab:', initial); + activateTab(initial); +} + +// Initialize tabs after DOM is ready but before customization init uses the DOM +window.addEventListener('DOMContentLoaded', () => { + console.log('[TABS] DOM loaded, initializing tabs...'); + initTabs(); + console.log('[TABS] Tabs initialized'); +}); diff --git a/site-history.json b/site-history.json index 32960f8..1e23822 100644 --- a/site-history.json +++ b/site-history.json @@ -1,2 +1,3 @@ [ + "https://www.youtube.com/" ] \ No newline at end of file