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 @@
+
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;