diff --git a/desktop-app/resources/index.html b/desktop-app/resources/index.html index 873b4ac..d0ca830 100644 --- a/desktop-app/resources/index.html +++ b/desktop-app/resources/index.html @@ -238,6 +238,9 @@

Menu

+ diff --git a/desktop-app/resources/js/script.js b/desktop-app/resources/js/script.js index 3d21538..05dc57b 100644 --- a/desktop-app/resources/js/script.js +++ b/desktop-app/resources/js/script.js @@ -1151,23 +1151,6 @@ document.addEventListener("DOMContentLoaded", function () { if (tabId) switchTab(tabId); }; - // "+ Create" button at end of tab list (placed outside tabList to prevent ARIA child violation) - let newBtn = tabList.parentElement.querySelector('.tab-new-btn'); - if (!newBtn) { - newBtn = document.createElement('button'); - newBtn.className = 'tab-new-btn'; - newBtn.title = 'New Tab (Ctrl+T)'; - newBtn.setAttribute('aria-label', 'Open new tab'); - newBtn.innerHTML = ''; - newBtn.addEventListener('click', function() { newTab(); }); - - const resetBtn = document.getElementById('tab-reset-btn'); - if (resetBtn) { - tabList.parentElement.insertBefore(newBtn, resetBtn); - } else { - tabList.parentElement.appendChild(newBtn); - } - } // Auto-scroll active tab into view (paint-aligned to prevent forced reflows) const activeItem = tabList.querySelector('.tab-item.active'); @@ -1224,6 +1207,91 @@ document.addEventListener("DOMContentLoaded", function () { }; renderMobileTabList(tabsArr, currentActiveTabId); + if (typeof tabList.dispatchEvent === 'function') { + tabList.dispatchEvent(new Event('scroll')); + } + } + + // ======================================== + // TAB OVERFLOW — Scroll Buttons, Wheel, Indicators + // ======================================== + + var _tabOverflowInitialized = false; + + function setupTabOverflow() { + if (_tabOverflowInitialized) return; + _tabOverflowInitialized = true; + + var tabBar = document.getElementById('tab-bar'); + var tabList = document.getElementById('tab-list'); + if (!tabBar || !tabList) return; + + // --- Create scroll arrow buttons --- + var scrollLeftBtn = document.createElement('button'); + scrollLeftBtn.className = 'tab-scroll-btn tab-scroll-left'; + scrollLeftBtn.setAttribute('aria-label', 'Scroll tabs left'); + scrollLeftBtn.title = 'Scroll left'; + scrollLeftBtn.innerHTML = ''; + scrollLeftBtn.addEventListener('click', function() { + tabList.scrollBy({ left: -200, behavior: 'smooth' }); + }); + + var scrollRightBtn = document.createElement('button'); + scrollRightBtn.className = 'tab-scroll-btn tab-scroll-right'; + scrollRightBtn.setAttribute('aria-label', 'Scroll tabs right'); + scrollRightBtn.title = 'Scroll right'; + scrollRightBtn.innerHTML = ''; + scrollRightBtn.addEventListener('click', function() { + tabList.scrollBy({ left: 200, behavior: 'smooth' }); + }); + + // Insert scroll buttons flanking the tab-list + tabBar.insertBefore(scrollLeftBtn, tabList); + var newBtn = document.getElementById('tab-new-btn'); + if (newBtn) { + tabBar.insertBefore(scrollRightBtn, newBtn); + } else { + var resetBtn = document.getElementById('tab-reset-btn'); + if (resetBtn) { + tabBar.insertBefore(scrollRightBtn, resetBtn); + } else { + tabBar.appendChild(scrollRightBtn); + } + } + + // --- Overflow detection --- + var _overflowRafId = null; + function updateOverflowState() { + if (_overflowRafId) return; + _overflowRafId = requestAnimationFrame(function() { + _overflowRafId = null; + var hasLeft = tabList.scrollLeft > 1; + var hasRight = tabList.scrollLeft < (tabList.scrollWidth - tabList.clientWidth - 1); + tabBar.classList.toggle('has-overflow-left', hasLeft); + tabBar.classList.toggle('has-overflow-right', hasRight); + }); + } + + tabList.addEventListener('scroll', updateOverflowState); + + // Use ResizeObserver to detect when overflow state changes due to window resize + if (typeof ResizeObserver !== 'undefined') { + var resizeObs = new ResizeObserver(updateOverflowState); + resizeObs.observe(tabList); + } + + // Initial check + updateOverflowState(); + + // --- Mouse wheel scroll: vertical wheel → horizontal scroll --- + tabList.addEventListener('wheel', function(e) { + // Only intercept vertical wheel (don't fight native horizontal wheel/trackpad) + if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) { + e.preventDefault(); + tabList.scrollLeft += e.deltaY; + updateOverflowState(); + } + }, { passive: false }); } function renderMobileTabList(tabsArr, currentActiveTabId) { @@ -1510,6 +1578,14 @@ document.addEventListener("DOMContentLoaded", function () { markdownEditor.scrollTop = activeTab.scrollPos || 0; }); renderTabBar(tabs, activeTabId); + setupTabOverflow(); + + const staticNewBtn = document.getElementById('tab-new-btn'); + if (staticNewBtn) { + staticNewBtn.onclick = function() { + newTab(); + }; + } } // Late-load callback hook for Neutralino command-line files diff --git a/desktop-app/resources/styles.css b/desktop-app/resources/styles.css index 18af84e..bbd0941 100644 --- a/desktop-app/resources/styles.css +++ b/desktop-app/resources/styles.css @@ -1831,23 +1831,29 @@ a:focus { .tab-new-btn { display: flex; align-items: center; - justify-content: center; - width: 24px; + gap: 4px; height: 24px; + padding: 0 8px; border-radius: 5px; background: none; - border: 1px solid transparent; + border: 1px solid var(--border-color); color: var(--text-color); cursor: pointer; - font-size: 16px; + font-size: 12px; flex-shrink: 0; - margin-left: 4px; - transition: background-color 0.15s ease, border-color 0.15s ease; + margin-left: 6px; + align-self: center; + transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease; } .tab-new-btn:hover { - background-color: var(--button-hover); - border-color: var(--border-color); + background-color: rgba(46, 160, 67, 0.1); + border-color: var(--accent-color, #2ea043); + color: var(--accent-color, #2ea043); +} + +.tab-new-btn:active { + background-color: rgba(46, 160, 67, 0.2); } /* Drag-and-drop visual feedback */ @@ -1880,6 +1886,83 @@ a:focus { } } +/* ======================================== + TAB OVERFLOW — Scroll Buttons & Fade Indicators + ======================================== */ + +.tab-scroll-btn { + display: none; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 4px; + background: none; + border: 1px solid transparent; + color: var(--text-color); + cursor: pointer; + font-size: 14px; + flex-shrink: 0; + padding: 0; + transition: background-color 0.15s ease, border-color 0.15s ease, opacity 0.15s ease; + z-index: 2; + opacity: 0.6; +} + +.tab-scroll-btn:hover { + background-color: var(--button-hover); + border-color: var(--border-color); + opacity: 1; +} + +.tab-scroll-btn:active { + background-color: var(--button-active); +} + +/* Show scroll buttons only when overflow exists */ +.tab-bar.has-overflow-left .tab-scroll-left, +.tab-bar.has-overflow-right .tab-scroll-right { + display: flex; +} + +/* Overflow fade indicators — subtle gradient at clipped edges */ +.tab-list::before, +.tab-list::after { + content: ''; + position: sticky; + top: 0; + bottom: 0; + width: 0; + flex-shrink: 0; + pointer-events: none; + z-index: 3; + transition: box-shadow 0.2s ease; +} + +.tab-list::before { + left: 0; +} + +.tab-list::after { + right: 0; +} + +.tab-bar.has-overflow-left .tab-list::before { + box-shadow: 8px 0 12px -4px rgba(0, 0, 0, 0.12); +} + +.tab-bar.has-overflow-right .tab-list::after { + box-shadow: -8px 0 12px -4px rgba(0, 0, 0, 0.12); +} + +[data-theme="dark"] .tab-bar.has-overflow-left .tab-list::before { + box-shadow: 8px 0 12px -4px rgba(0, 0, 0, 0.35); +} + +[data-theme="dark"] .tab-bar.has-overflow-right .tab-list::after { + box-shadow: -8px 0 12px -4px rgba(0, 0, 0, 0.35); +} + /* ======================================== THREE-DOT TAB MENU ======================================== */ @@ -1927,7 +2010,13 @@ a:focus { padding: 0 10px 0 12px !important; gap: 8px !important; } - .tab-new-btn { + .tab-new-btn, + .tab-reset-btn { + height: 32px !important; + font-size: 14px !important; + padding: 0 12px !important; + } + .tab-scroll-btn { width: 32px !important; height: 32px !important; font-size: 18px !important; diff --git a/index.html b/index.html index 53575ba..b6aa523 100644 --- a/index.html +++ b/index.html @@ -297,6 +297,9 @@

Menu

+ diff --git a/script.js b/script.js index 3d21538..05dc57b 100644 --- a/script.js +++ b/script.js @@ -1151,23 +1151,6 @@ document.addEventListener("DOMContentLoaded", function () { if (tabId) switchTab(tabId); }; - // "+ Create" button at end of tab list (placed outside tabList to prevent ARIA child violation) - let newBtn = tabList.parentElement.querySelector('.tab-new-btn'); - if (!newBtn) { - newBtn = document.createElement('button'); - newBtn.className = 'tab-new-btn'; - newBtn.title = 'New Tab (Ctrl+T)'; - newBtn.setAttribute('aria-label', 'Open new tab'); - newBtn.innerHTML = ''; - newBtn.addEventListener('click', function() { newTab(); }); - - const resetBtn = document.getElementById('tab-reset-btn'); - if (resetBtn) { - tabList.parentElement.insertBefore(newBtn, resetBtn); - } else { - tabList.parentElement.appendChild(newBtn); - } - } // Auto-scroll active tab into view (paint-aligned to prevent forced reflows) const activeItem = tabList.querySelector('.tab-item.active'); @@ -1224,6 +1207,91 @@ document.addEventListener("DOMContentLoaded", function () { }; renderMobileTabList(tabsArr, currentActiveTabId); + if (typeof tabList.dispatchEvent === 'function') { + tabList.dispatchEvent(new Event('scroll')); + } + } + + // ======================================== + // TAB OVERFLOW — Scroll Buttons, Wheel, Indicators + // ======================================== + + var _tabOverflowInitialized = false; + + function setupTabOverflow() { + if (_tabOverflowInitialized) return; + _tabOverflowInitialized = true; + + var tabBar = document.getElementById('tab-bar'); + var tabList = document.getElementById('tab-list'); + if (!tabBar || !tabList) return; + + // --- Create scroll arrow buttons --- + var scrollLeftBtn = document.createElement('button'); + scrollLeftBtn.className = 'tab-scroll-btn tab-scroll-left'; + scrollLeftBtn.setAttribute('aria-label', 'Scroll tabs left'); + scrollLeftBtn.title = 'Scroll left'; + scrollLeftBtn.innerHTML = ''; + scrollLeftBtn.addEventListener('click', function() { + tabList.scrollBy({ left: -200, behavior: 'smooth' }); + }); + + var scrollRightBtn = document.createElement('button'); + scrollRightBtn.className = 'tab-scroll-btn tab-scroll-right'; + scrollRightBtn.setAttribute('aria-label', 'Scroll tabs right'); + scrollRightBtn.title = 'Scroll right'; + scrollRightBtn.innerHTML = ''; + scrollRightBtn.addEventListener('click', function() { + tabList.scrollBy({ left: 200, behavior: 'smooth' }); + }); + + // Insert scroll buttons flanking the tab-list + tabBar.insertBefore(scrollLeftBtn, tabList); + var newBtn = document.getElementById('tab-new-btn'); + if (newBtn) { + tabBar.insertBefore(scrollRightBtn, newBtn); + } else { + var resetBtn = document.getElementById('tab-reset-btn'); + if (resetBtn) { + tabBar.insertBefore(scrollRightBtn, resetBtn); + } else { + tabBar.appendChild(scrollRightBtn); + } + } + + // --- Overflow detection --- + var _overflowRafId = null; + function updateOverflowState() { + if (_overflowRafId) return; + _overflowRafId = requestAnimationFrame(function() { + _overflowRafId = null; + var hasLeft = tabList.scrollLeft > 1; + var hasRight = tabList.scrollLeft < (tabList.scrollWidth - tabList.clientWidth - 1); + tabBar.classList.toggle('has-overflow-left', hasLeft); + tabBar.classList.toggle('has-overflow-right', hasRight); + }); + } + + tabList.addEventListener('scroll', updateOverflowState); + + // Use ResizeObserver to detect when overflow state changes due to window resize + if (typeof ResizeObserver !== 'undefined') { + var resizeObs = new ResizeObserver(updateOverflowState); + resizeObs.observe(tabList); + } + + // Initial check + updateOverflowState(); + + // --- Mouse wheel scroll: vertical wheel → horizontal scroll --- + tabList.addEventListener('wheel', function(e) { + // Only intercept vertical wheel (don't fight native horizontal wheel/trackpad) + if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) { + e.preventDefault(); + tabList.scrollLeft += e.deltaY; + updateOverflowState(); + } + }, { passive: false }); } function renderMobileTabList(tabsArr, currentActiveTabId) { @@ -1510,6 +1578,14 @@ document.addEventListener("DOMContentLoaded", function () { markdownEditor.scrollTop = activeTab.scrollPos || 0; }); renderTabBar(tabs, activeTabId); + setupTabOverflow(); + + const staticNewBtn = document.getElementById('tab-new-btn'); + if (staticNewBtn) { + staticNewBtn.onclick = function() { + newTab(); + }; + } } // Late-load callback hook for Neutralino command-line files diff --git a/styles.css b/styles.css index 18af84e..bbd0941 100644 --- a/styles.css +++ b/styles.css @@ -1831,23 +1831,29 @@ a:focus { .tab-new-btn { display: flex; align-items: center; - justify-content: center; - width: 24px; + gap: 4px; height: 24px; + padding: 0 8px; border-radius: 5px; background: none; - border: 1px solid transparent; + border: 1px solid var(--border-color); color: var(--text-color); cursor: pointer; - font-size: 16px; + font-size: 12px; flex-shrink: 0; - margin-left: 4px; - transition: background-color 0.15s ease, border-color 0.15s ease; + margin-left: 6px; + align-self: center; + transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease; } .tab-new-btn:hover { - background-color: var(--button-hover); - border-color: var(--border-color); + background-color: rgba(46, 160, 67, 0.1); + border-color: var(--accent-color, #2ea043); + color: var(--accent-color, #2ea043); +} + +.tab-new-btn:active { + background-color: rgba(46, 160, 67, 0.2); } /* Drag-and-drop visual feedback */ @@ -1880,6 +1886,83 @@ a:focus { } } +/* ======================================== + TAB OVERFLOW — Scroll Buttons & Fade Indicators + ======================================== */ + +.tab-scroll-btn { + display: none; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 4px; + background: none; + border: 1px solid transparent; + color: var(--text-color); + cursor: pointer; + font-size: 14px; + flex-shrink: 0; + padding: 0; + transition: background-color 0.15s ease, border-color 0.15s ease, opacity 0.15s ease; + z-index: 2; + opacity: 0.6; +} + +.tab-scroll-btn:hover { + background-color: var(--button-hover); + border-color: var(--border-color); + opacity: 1; +} + +.tab-scroll-btn:active { + background-color: var(--button-active); +} + +/* Show scroll buttons only when overflow exists */ +.tab-bar.has-overflow-left .tab-scroll-left, +.tab-bar.has-overflow-right .tab-scroll-right { + display: flex; +} + +/* Overflow fade indicators — subtle gradient at clipped edges */ +.tab-list::before, +.tab-list::after { + content: ''; + position: sticky; + top: 0; + bottom: 0; + width: 0; + flex-shrink: 0; + pointer-events: none; + z-index: 3; + transition: box-shadow 0.2s ease; +} + +.tab-list::before { + left: 0; +} + +.tab-list::after { + right: 0; +} + +.tab-bar.has-overflow-left .tab-list::before { + box-shadow: 8px 0 12px -4px rgba(0, 0, 0, 0.12); +} + +.tab-bar.has-overflow-right .tab-list::after { + box-shadow: -8px 0 12px -4px rgba(0, 0, 0, 0.12); +} + +[data-theme="dark"] .tab-bar.has-overflow-left .tab-list::before { + box-shadow: 8px 0 12px -4px rgba(0, 0, 0, 0.35); +} + +[data-theme="dark"] .tab-bar.has-overflow-right .tab-list::after { + box-shadow: -8px 0 12px -4px rgba(0, 0, 0, 0.35); +} + /* ======================================== THREE-DOT TAB MENU ======================================== */ @@ -1927,7 +2010,13 @@ a:focus { padding: 0 10px 0 12px !important; gap: 8px !important; } - .tab-new-btn { + .tab-new-btn, + .tab-reset-btn { + height: 32px !important; + font-size: 14px !important; + padding: 0 12px !important; + } + .tab-scroll-btn { width: 32px !important; height: 32px !important; font-size: 18px !important;