From 4097bd77c20e71fc5a51767f3e059da7bbaf44b2 Mon Sep 17 00:00:00 2001 From: Yonatan Lifshitz Date: Fri, 5 Dec 2025 00:40:02 +0200 Subject: [PATCH 1/8] Scrollable graph tabs --- frontends/web/index.html | 2 ++ frontends/web/scripts/main.js | 2 +- frontends/web/styles/style.css | 50 ++++++++++++++++++++++++++++++++-- 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/frontends/web/index.html b/frontends/web/index.html index 82b5267..05491fd 100644 --- a/frontends/web/index.html +++ b/frontends/web/index.html @@ -170,6 +170,8 @@

Graffiti

+
+
Rename
diff --git a/frontends/web/scripts/main.js b/frontends/web/scripts/main.js index 81b87cb..cfb3fb8 100644 --- a/frontends/web/scripts/main.js +++ b/frontends/web/scripts/main.js @@ -701,7 +701,7 @@ function main() { // Initiate tabs const tabsController = new TabsController( - document.getElementsByClassName("tabs")[0], + document.getElementsByClassName("tab-list-wrapper")[0], document.getElementsByClassName("view")[0], document.getElementById("context-menu") ); diff --git a/frontends/web/styles/style.css b/frontends/web/styles/style.css index 652a521..b491c3f 100644 --- a/frontends/web/styles/style.css +++ b/frontends/web/styles/style.css @@ -106,23 +106,67 @@ body { } #addTab { - float: right; + /* don't float when using flex layout for .tabs; keep as fixed-size control */ + float: none; + order: 2; + flex: 0 0 auto; } /* Style the tab */ .tabs { - overflow: hidden; + /* Use flex so we can make the inner tab list horizontally scrollable + and avoid wrapping into additional rows. */ + display: flex; + align-items: center; background-color: var(--primary-color); } +/* The container that holds the tab buttons. It will stay on a single row + and allow horizontal scrolling when there are too many tabs. */ +.tab-list-wrapper { + order: 1; + display: flex; + flex-wrap: nowrap; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + align-items: center; + flex: 1 1 auto; +} + +/* Make sure individual buttons inside the wrapper don't float/wrap */ +.tab-list-wrapper button { + float: none; + display: inline-flex; +} + +/* Hide the visible scrollbar but keep scrolling functional. + - Firefox: scrollbar-width: none; + - IE/Edge: -ms-overflow-style: none; + - WebKit: set scrollbar height to 0 so no line appears. */ +.tab-list-wrapper { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ +} +.tab-list-wrapper::-webkit-scrollbar { + height: 0; + background: transparent; +} + .tabs .divider { background-color: inherit; - float: left; + /* Use flex sizing instead of float so the divider keeps its intended + dimensions inside the .tabs flex container. */ + float: none; border: white solid 2px; outline: none; cursor: grabbing; height: 48px; width: 20px; + min-width: 20px; + box-sizing: content-box; + flex: 0 0 20px; /* don't grow or shrink; keep fixed width */ + align-self: stretch; /* allow it to span the container height if needed */ + margin: 0 6px; } .tabs .divider.dragover { From 979dd3fbf436a14e8557b68f29f90342bfbb3eee Mon Sep 17 00:00:00 2001 From: Yonatan Lifshitz Date: Fri, 5 Dec 2025 03:16:53 +0200 Subject: [PATCH 2/8] Searchable tabs --- frontends/web/index.html | 7 ++++ frontends/web/scripts/TabsController.js | 47 ++++++++++++++++++++++++- frontends/web/scripts/main.js | 1 + frontends/web/styles/style.css | 42 ++++++++++++++++++++++ 4 files changed, 96 insertions(+), 1 deletion(-) diff --git a/frontends/web/index.html b/frontends/web/index.html index 05491fd..f8d15c4 100644 --- a/frontends/web/index.html +++ b/frontends/web/index.html @@ -169,7 +169,14 @@

Graffiti

 
+ + + +
diff --git a/frontends/web/scripts/TabsController.js b/frontends/web/scripts/TabsController.js index 17bf9c9..f2e0470 100644 --- a/frontends/web/scripts/TabsController.js +++ b/frontends/web/scripts/TabsController.js @@ -1,9 +1,10 @@ const STORAGE_VERSION = 2; class TabsController { - constructor(tabsView, contentView, contextMenu) { + constructor(tabsView, tabsSearchView, contentView, contextMenu) { this.tabs = []; this.tabsView = tabsView; + this.tabsSearchView = tabsSearchView; this.contentView = contentView; this.contextMenu = contextMenu; this.contextMenuOpenedForTab = null; @@ -306,6 +307,26 @@ class TabsController { } }); }; + + + this.tabsSearchView.addEventListener("input", (event) => { + // Hide context menu + realThis.contextMenuOpenedForTab = null; + realThis.contextMenu.classList.remove("visible"); + + setTimeout(() => { + realThis.onSearchTabs(event); + }); + }); + this.tabsSearchView.addEventListener("blur", (event) => { + // Hide context menu + realThis.contextMenuOpenedForTab = null; + realThis.contextMenu.classList.remove("visible"); + + setTimeout(() => { + realThis.clearTabSearch(); + }); + }); } removeCurrentTab() { @@ -362,6 +383,30 @@ class TabsController { } } + onSearchTabs(event) { + // Read the current query from the provided search input element. + const query = (event.target && event.target.value) + ? event.target.value.toLowerCase() + : ""; + + this.tabs.forEach(({ name, tabElement }) => { + const matches = !query || name.toLowerCase().includes(query); + tabElement.style.display = matches ? "" : "none"; + // TODO: If there is a divider immediately before this tab, hide/show it too + }); + } + + clearTabSearch() { + // Clearing the tab is not only a feature - + // for now it solves the need to deal with dividers + if (this.tabsSearchView) { + // Set the value property to an empty string to clear the text + console.log('Clearing tab search input'); + this.tabsSearchView.value = ''; + this.tabsSearchView.dispatchEvent(new Event('input')); + } + } + #addEmptyTab() { const emptyTab = this.addTab("untitled"); this.selectTab(emptyTab); diff --git a/frontends/web/scripts/main.js b/frontends/web/scripts/main.js index cfb3fb8..d694215 100644 --- a/frontends/web/scripts/main.js +++ b/frontends/web/scripts/main.js @@ -702,6 +702,7 @@ function main() { // Initiate tabs const tabsController = new TabsController( document.getElementsByClassName("tab-list-wrapper")[0], + document.getElementsByClassName("tab-search-input")[0], document.getElementsByClassName("view")[0], document.getElementById("context-menu") ); diff --git a/frontends/web/styles/style.css b/frontends/web/styles/style.css index b491c3f..f79db40 100644 --- a/frontends/web/styles/style.css +++ b/frontends/web/styles/style.css @@ -152,6 +152,48 @@ body { background: transparent; } +/* Collapsible search box inside the tabs area. It shows only the icon + when collapsed and expands to a fixed width when focused +*/ +.tab-search { + order: 0; + display: inline-flex; + align-items: center; + background: transparent; + border-radius: 6px; + overflow: hidden; + width: 36px; + transition: width 220ms ease; + flex: 0 0 auto; +} +.tab-search:focus-within { + width: 220px; +} +.tab-search-button { + width: 36px; + background: transparent; + border: none; + color: var(--text-color); + font-size: 16px; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; +} +.tab-search-input { + background: transparent; + border: none; + outline: none; + color: var(--text-color); + padding: 8px 10px; + font-size: 14px; + width: 100%; + min-width: 0; +} +.tab-search-input::placeholder { + color: rgba(255,255,255,0.7); +} + .tabs .divider { background-color: inherit; /* Use flex sizing instead of float so the divider keeps its intended From fcf36810f004620d9a3d4ee788213d8ba8ff03b7 Mon Sep 17 00:00:00 2001 From: Yonatan Lifshitz Date: Fri, 5 Dec 2025 04:29:30 +0200 Subject: [PATCH 3/8] Minor bugfix and UX improvements --- frontends/web/index.html | 3 ++- frontends/web/scripts/TabsController.js | 30 ++++++------------------- frontends/web/styles/style.css | 13 +++++++++-- 3 files changed, 20 insertions(+), 26 deletions(-) diff --git a/frontends/web/index.html b/frontends/web/index.html index f8d15c4..72d0826 100644 --- a/frontends/web/index.html +++ b/frontends/web/index.html @@ -171,8 +171,9 @@

Graffiti

diff --git a/frontends/web/scripts/TabsController.js b/frontends/web/scripts/TabsController.js index f2e0470..53fc196 100644 --- a/frontends/web/scripts/TabsController.js +++ b/frontends/web/scripts/TabsController.js @@ -84,8 +84,13 @@ class TabsController { #addDividers(tabId) { let index = 0; for (const { tabElement } of this.tabs) { - const dividerElement = this.#createDivider(index, tabId); - this.tabsView.insertBefore(dividerElement, tabElement); + // Only add divider before visible tabs + // This creates a better UX when searching tabs + // but dragging from LTR or RTL is inconsistent + if (tabElement.style.display !== "none") { + const dividerElement = this.#createDivider(index, tabId); + this.tabsView.insertBefore(dividerElement, tabElement); + } index++; } this.tabsView.appendChild(this.#createDivider(index, tabId)); @@ -318,15 +323,6 @@ class TabsController { realThis.onSearchTabs(event); }); }); - this.tabsSearchView.addEventListener("blur", (event) => { - // Hide context menu - realThis.contextMenuOpenedForTab = null; - realThis.contextMenu.classList.remove("visible"); - - setTimeout(() => { - realThis.clearTabSearch(); - }); - }); } removeCurrentTab() { @@ -392,21 +388,9 @@ class TabsController { this.tabs.forEach(({ name, tabElement }) => { const matches = !query || name.toLowerCase().includes(query); tabElement.style.display = matches ? "" : "none"; - // TODO: If there is a divider immediately before this tab, hide/show it too }); } - clearTabSearch() { - // Clearing the tab is not only a feature - - // for now it solves the need to deal with dividers - if (this.tabsSearchView) { - // Set the value property to an empty string to clear the text - console.log('Clearing tab search input'); - this.tabsSearchView.value = ''; - this.tabsSearchView.dispatchEvent(new Event('input')); - } - } - #addEmptyTab() { const emptyTab = this.addTab("untitled"); this.selectTab(emptyTab); diff --git a/frontends/web/styles/style.css b/frontends/web/styles/style.css index f79db40..58a5fd1 100644 --- a/frontends/web/styles/style.css +++ b/frontends/web/styles/style.css @@ -163,12 +163,21 @@ body { border-radius: 6px; overflow: hidden; width: 36px; - transition: width 220ms ease; + transition: width 200ms ease; flex: 0 0 auto; } +/* These three keep the search tab bar fun to use */ .tab-search:focus-within { - width: 220px; + width: 200px; } +.tab-search:hover { + width: 200px; + background-color: var(--primary-color-hover); +} +.tab-search-input:not(:placeholder-shown) { + width: 200px; +} + .tab-search-button { width: 36px; background: transparent; From 5128e2b64af898d1c2b2b9defa7a9b2079c10ae8 Mon Sep 17 00:00:00 2001 From: Yonatan Lifshitz Date: Fri, 5 Dec 2025 21:40:22 +0200 Subject: [PATCH 4/8] Added visuals for search results, maybe fixed scrolling bug --- frontends/web/index.html | 7 ++++-- frontends/web/scripts/TabsController.js | 16 +++++++++++++ frontends/web/scripts/main.js | 2 +- frontends/web/styles/style.css | 30 +++++++++++-------------- 4 files changed, 35 insertions(+), 20 deletions(-) diff --git a/frontends/web/index.html b/frontends/web/index.html index 72d0826..af38d5c 100644 --- a/frontends/web/index.html +++ b/frontends/web/index.html @@ -171,8 +171,11 @@

Graffiti

diff --git a/frontends/web/scripts/TabsController.js b/frontends/web/scripts/TabsController.js index 53fc196..af14268 100644 --- a/frontends/web/scripts/TabsController.js +++ b/frontends/web/scripts/TabsController.js @@ -384,11 +384,27 @@ class TabsController { const query = (event.target && event.target.value) ? event.target.value.toLowerCase() : ""; + const iconElement = this.tabsSearchView.querySelector(".tab-search-button img") + var has_matches = false; this.tabs.forEach(({ name, tabElement }) => { const matches = !query || name.toLowerCase().includes(query); + if (matches) { + has_matches = true; + } tabElement.style.display = matches ? "" : "none"; }); + + // Update the search icon color based on whether there are matches. + if (!has_matches) { + iconElement.setAttribute('data-icon-state', "gray"); + } else { + if (query) { + iconElement.setAttribute('data-icon-state', "green"); + } else { + iconElement.setAttribute('data-icon-state', "white"); + } + } } #addEmptyTab() { diff --git a/frontends/web/scripts/main.js b/frontends/web/scripts/main.js index d694215..59ee781 100644 --- a/frontends/web/scripts/main.js +++ b/frontends/web/scripts/main.js @@ -702,7 +702,7 @@ function main() { // Initiate tabs const tabsController = new TabsController( document.getElementsByClassName("tab-list-wrapper")[0], - document.getElementsByClassName("tab-search-input")[0], + document.getElementsByClassName("tab-search")[0], document.getElementsByClassName("view")[0], document.getElementById("context-menu") ); diff --git a/frontends/web/styles/style.css b/frontends/web/styles/style.css index 58a5fd1..f1fcb96 100644 --- a/frontends/web/styles/style.css +++ b/frontends/web/styles/style.css @@ -128,7 +128,6 @@ body { display: flex; flex-wrap: nowrap; overflow-x: auto; - -webkit-overflow-scrolling: touch; align-items: center; flex: 1 1 auto; } @@ -139,19 +138,6 @@ body { display: inline-flex; } -/* Hide the visible scrollbar but keep scrolling functional. - - Firefox: scrollbar-width: none; - - IE/Edge: -ms-overflow-style: none; - - WebKit: set scrollbar height to 0 so no line appears. */ -.tab-list-wrapper { - -ms-overflow-style: none; /* IE and Edge */ - scrollbar-width: none; /* Firefox */ -} -.tab-list-wrapper::-webkit-scrollbar { - height: 0; - background: transparent; -} - /* Collapsible search box inside the tabs area. It shows only the icon when collapsed and expands to a fixed width when focused */ @@ -166,7 +152,6 @@ body { transition: width 200ms ease; flex: 0 0 auto; } -/* These three keep the search tab bar fun to use */ .tab-search:focus-within { width: 200px; } @@ -174,8 +159,19 @@ body { width: 200px; background-color: var(--primary-color-hover); } -.tab-search-input:not(:placeholder-shown) { - width: 200px; + +.tab-search-button img { + filter: none; +} + +.tab-search-button img[data-icon-state="white"] { + filter: brightness(0) saturate(100%) invert(100%) sepia(11%) saturate(0%) hue-rotate(14deg) brightness(103%) contrast(100%) +} +.tab-search-button img[data-icon-state="gray"] { + filter: brightness(0) saturate(100%) invert(67%) sepia(82%) saturate(0%) hue-rotate(133deg) brightness(95%) contrast(93%) +} +.tab-search-button img[data-icon-state="green"] { + filter: brightness(0) saturate(100%) invert(92%) sepia(15%) saturate(461%) hue-rotate(46deg) brightness(102%) contrast(102%); } .tab-search-button { From a9d332a59f90bef7d871d12af04f2f72deb538be Mon Sep 17 00:00:00 2001 From: Yonatan Lifshitz Date: Sun, 7 Dec 2025 00:37:09 +0200 Subject: [PATCH 5/8] Checkpoint - good functionality, need UI improvements and code improvments --- .../icons/icon_upside_down_triangle.svg | 3 + frontends/web/index.html | 4 ++ frontends/web/scripts/TabsController.js | 41 +++++++++++ frontends/web/scripts/main.js | 9 ++- frontends/web/styles/style.css | 70 ++++++++++++++++++- 5 files changed, 124 insertions(+), 3 deletions(-) create mode 100644 frontends/web/images/icons/icon_upside_down_triangle.svg diff --git a/frontends/web/images/icons/icon_upside_down_triangle.svg b/frontends/web/images/icons/icon_upside_down_triangle.svg new file mode 100644 index 0000000..e7b4c85 --- /dev/null +++ b/frontends/web/images/icons/icon_upside_down_triangle.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontends/web/index.html b/frontends/web/index.html index af38d5c..f653f3d 100644 --- a/frontends/web/index.html +++ b/frontends/web/index.html @@ -179,6 +179,10 @@

Graffiti

+ +
diff --git a/frontends/web/scripts/TabsController.js b/frontends/web/scripts/TabsController.js index af14268..8ef6b5a 100644 --- a/frontends/web/scripts/TabsController.js +++ b/frontends/web/scripts/TabsController.js @@ -10,6 +10,7 @@ class TabsController { this.contextMenuOpenedForTab = null; this.selectedTab = null; this.tabIdCounter = 0; + this.updateTabArrowVisibility = () => {}; this.#initiateContextMenu(); } @@ -323,8 +324,46 @@ class TabsController { realThis.onSearchTabs(event); }); }); + + const tabsScrollBtn = document.getElementsByClassName("tab-arrow-button")[0]; + if (!tabsScrollBtn || !this.tabsView) return; + + console.log("Initiating tab arrow visibility controller"); + this.updateTabArrowVisibility = () => { + // If the content is wider than the visible area, show the button. + const overflowing = this.tabsView.scrollWidth > this.tabsView.clientWidth + 1; + const inMultiRow = this.tabsView.classList.contains("multirow"); + console.log("Updating tab arrow visibility:", { overflowing, inMultiRow }); + tabsScrollBtn.style.display = overflowing | inMultiRow ? "" : "none"; + }; + + // Initial update + setTimeout(this.updateTabArrowVisibility, 0); + + // Update on window resize + window.addEventListener("resize", this.updateTabArrowVisibility); + + // Observe changes to the tab list (tabs added/removed, style changes from search) + try { + const mo = new MutationObserver(() => this.updateTabArrowVisibility()); + mo.observe(tabListWrapper, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ["style", "class"], + }); + } catch (err) { + // MutationObserver shouldn't fail in modern browsers; fail silently. + } + } + + toggleTabBarMode() { + console.log("Toggling tab bar mode"); + const tabListWrapper = this.tabsView; + tabListWrapper.classList.toggle("multirow"); } + removeCurrentTab() { this.#removeTab(this.selectedTab); } @@ -405,6 +444,8 @@ class TabsController { iconElement.setAttribute('data-icon-state', "white"); } } + // Search results may have changed tab list size; update arrow visibility. + this.updateTabArrowVisibility(); } #addEmptyTab() { diff --git a/frontends/web/scripts/main.js b/frontends/web/scripts/main.js index 59ee781..6c871e1 100644 --- a/frontends/web/scripts/main.js +++ b/frontends/web/scripts/main.js @@ -634,6 +634,10 @@ function event_showChangelog() { }); } +function event_toggleTabBarMode() { + tabsController.toggleTabBarMode(); +} + function event_showManageTokenDialog(isFromAuthEvent = false) { const currentToken = localStorage.getItem(LOCAL_STORAGE_TOKEN_KEY); const isWindows = navigator.userAgent.includes("Windows NT"); @@ -756,7 +760,7 @@ function initiateConnectionUrl() { function initiateHotkeys() { hotkeys( - "esc,ctrl+z,ctrl+shift+z,ctrl+y,ctrl+s,ctrl+alt+s,ctrl+o,ctrl+i,ctrl+alt+shift+i,ctrl+q,ctrl+f,ctrl+shift+f,ctrl+k,ctrl+e,ctrl+shift+q,ctrl+shift+p,delete,ctrl+delete,ctrl+c,home,ctrl+home,shift+`,/,shift+/,ctrl+shift+/,ctrl+a,1,2,3,4,5,6,7,8,9", + "esc,ctrl+z,ctrl+shift+z,ctrl+y,ctrl+s,ctrl+alt+s,ctrl+o,ctrl+i,ctrl+alt+shift+i,ctrl+q,ctrl+f,ctrl+shift+f,ctrl+k,ctrl+e,ctrl+shift+q,ctrl+shift+p,ctrl+t,delete,ctrl+delete,ctrl+c,home,ctrl+home,shift+`,/,shift+/,ctrl+shift+/,ctrl+a,1,2,3,4,5,6,7,8,9", function (event, handler) { if (!window.commandPalette.isOpen()) { switch (handler.key) { @@ -794,6 +798,9 @@ function initiateHotkeys() { case "ctrl+shift+q": event_addTextNode(); return false; + case "ctrl+t": + event_toggleTabBarMode(); + return false; case "delete": event_delete(); return false; diff --git a/frontends/web/styles/style.css b/frontends/web/styles/style.css index f1fcb96..023b155 100644 --- a/frontends/web/styles/style.css +++ b/frontends/web/styles/style.css @@ -110,14 +110,21 @@ body { float: none; order: 2; flex: 0 0 auto; + align-self: flex-start; } /* Style the tab */ .tabs { /* Use flex so we can make the inner tab list horizontally scrollable - and avoid wrapping into additional rows. */ + and avoid wrapping into additional rows. */ display: flex; - align-items: center; + /* Keep children aligned to the top so controls don't vertically center + when the tab list grows to multiple rows. */ + float: none; + order: 2; + flex: 0 0 auto; + align-self: flex-start; + align-items: flex-start; background-color: var(--primary-color); } @@ -132,12 +139,38 @@ body { flex: 1 1 auto; } +/* When `.multirow` is applied the tab list is allowed to wrap into + multiple rows instead of remaining on a single horizontally-scrollable + row. */ +.tab-list-wrapper.multirow { + flex-wrap: wrap; + overflow-x: visible; +} + /* Make sure individual buttons inside the wrapper don't float/wrap */ .tab-list-wrapper button { float: none; display: inline-flex; } +.tab-list-wrapper.multirow button { + flex: 0 0 auto; +} + + +/* Hide the visible scrollbar but keep scrolling functional. + - Firefox: scrollbar-width: none; + - IE/Edge: -ms-overflow-style: none; + - WebKit: set scrollbar height to 0 so no line appears. */ +/* .tab-list-wrapper { + -ms-overflow-style: none; + scrollbar-width: none; +} +.tab-list-wrapper::-webkit-scrollbar { + height: 0; + background: transparent; +} */ + /* Collapsible search box inside the tabs area. It shows only the icon when collapsed and expands to a fixed width when focused */ @@ -151,6 +184,7 @@ body { width: 36px; transition: width 200ms ease; flex: 0 0 auto; + align-self: flex-start; } .tab-search:focus-within { width: 200px; @@ -160,6 +194,25 @@ body { background-color: var(--primary-color-hover); } +/* When the tab list is in multi-row mode, overlay the search control so + it doesn't push the add/arrow buttons. No reserved space is used. */ +.tabs.multirow-mode { + position: relative; +} +.tabs.multirow-mode .tab-search { + position: absolute; + left: 8px; + top: 6px; + width: 36px; + transform: none; + z-index: 2; +} +.tabs.multirow-mode .tab-search:focus-within, +.tabs.multirow-mode .tab-search:hover { + width: 200px; + background-color: var(--primary-color-hover); +} + .tab-search-button img { filter: none; } @@ -185,6 +238,19 @@ body { align-items: center; justify-content: center; } + +/* Arrow button shown when tab list is scrollable */ +.tab-arrow-button { + float: none; + order: 2; + flex: 0 0 auto; + align-self: flex-start; +} +.tab-arrow-button img { + display: block; + filter: brightness(0) saturate(100%) invert(100%) sepia(11%) saturate(0%) hue-rotate(14deg) brightness(103%) contrast(100%) +} + .tab-search-input { background: transparent; border: none; From cf39e9b73acc918fc968ddd07ee974b1e5613604 Mon Sep 17 00:00:00 2001 From: Yonatan Lifshitz Date: Sun, 7 Dec 2025 01:10:26 +0200 Subject: [PATCH 6/8] Working functionality + nice UI, need code improvements --- frontends/web/index.html | 6 +-- frontends/web/scripts/TabsController.js | 9 ++-- frontends/web/styles/style.css | 62 ++++++++----------------- 3 files changed, 28 insertions(+), 49 deletions(-) diff --git a/frontends/web/index.html b/frontends/web/index.html index f653f3d..96ea7d8 100644 --- a/frontends/web/index.html +++ b/frontends/web/index.html @@ -180,10 +180,10 @@

Graffiti

- - +
diff --git a/frontends/web/scripts/TabsController.js b/frontends/web/scripts/TabsController.js index 8ef6b5a..e40a432 100644 --- a/frontends/web/scripts/TabsController.js +++ b/frontends/web/scripts/TabsController.js @@ -10,7 +10,6 @@ class TabsController { this.contextMenuOpenedForTab = null; this.selectedTab = null; this.tabIdCounter = 0; - this.updateTabArrowVisibility = () => {}; this.#initiateContextMenu(); } @@ -325,7 +324,7 @@ class TabsController { }); }); - const tabsScrollBtn = document.getElementsByClassName("tab-arrow-button")[0]; + const tabsScrollBtn = document.getElementsByClassName("tab-expand-button")[0]; if (!tabsScrollBtn || !this.tabsView) return; console.log("Initiating tab arrow visibility controller"); @@ -358,9 +357,13 @@ class TabsController { } toggleTabBarMode() { - console.log("Toggling tab bar mode"); const tabListWrapper = this.tabsView; tabListWrapper.classList.toggle("multirow"); + const iconElement = document.getElementsByClassName("tab-expand-button")[0].querySelector("img"); + iconElement.setAttribute( + "expand-icon-state", + tabListWrapper.classList.contains("multirow") ? "up" : "down" + ); } diff --git a/frontends/web/styles/style.css b/frontends/web/styles/style.css index 023b155..b54e17b 100644 --- a/frontends/web/styles/style.css +++ b/frontends/web/styles/style.css @@ -105,26 +105,25 @@ body { background: #343434; } -#addTab { +.add-tab-button { /* don't float when using flex layout for .tabs; keep as fixed-size control */ float: none; order: 2; flex: 0 0 auto; align-self: flex-start; + height: 52px; + width: 40px; + display: inline-flex; + align-items: center; + justify-content: center; } /* Style the tab */ .tabs { - /* Use flex so we can make the inner tab list horizontally scrollable - and avoid wrapping into additional rows. */ display: flex; - /* Keep children aligned to the top so controls don't vertically center - when the tab list grows to multiple rows. */ float: none; order: 2; flex: 0 0 auto; - align-self: flex-start; - align-items: flex-start; background-color: var(--primary-color); } @@ -135,7 +134,6 @@ body { display: flex; flex-wrap: nowrap; overflow-x: auto; - align-items: center; flex: 1 1 auto; } @@ -157,20 +155,6 @@ body { flex: 0 0 auto; } - -/* Hide the visible scrollbar but keep scrolling functional. - - Firefox: scrollbar-width: none; - - IE/Edge: -ms-overflow-style: none; - - WebKit: set scrollbar height to 0 so no line appears. */ -/* .tab-list-wrapper { - -ms-overflow-style: none; - scrollbar-width: none; -} -.tab-list-wrapper::-webkit-scrollbar { - height: 0; - background: transparent; -} */ - /* Collapsible search box inside the tabs area. It shows only the icon when collapsed and expands to a fixed width when focused */ @@ -194,25 +178,6 @@ body { background-color: var(--primary-color-hover); } -/* When the tab list is in multi-row mode, overlay the search control so - it doesn't push the add/arrow buttons. No reserved space is used. */ -.tabs.multirow-mode { - position: relative; -} -.tabs.multirow-mode .tab-search { - position: absolute; - left: 8px; - top: 6px; - width: 36px; - transform: none; - z-index: 2; -} -.tabs.multirow-mode .tab-search:focus-within, -.tabs.multirow-mode .tab-search:hover { - width: 200px; - background-color: var(--primary-color-hover); -} - .tab-search-button img { filter: none; } @@ -240,16 +205,27 @@ body { } /* Arrow button shown when tab list is scrollable */ -.tab-arrow-button { +.tab-expand-button { float: none; order: 2; flex: 0 0 auto; align-self: flex-start; + width: 40px; + display: inline-flex; + align-items: center; + justify-content: center; } -.tab-arrow-button img { +.tab-expand-button img { display: block; + transform: none; filter: brightness(0) saturate(100%) invert(100%) sepia(11%) saturate(0%) hue-rotate(14deg) brightness(103%) contrast(100%) } +.tab-expand-button img[expand-icon-state="down"] { + transform: none; +} +.tab-expand-button img[expand-icon-state="up"] { + transform: scaleY(-1);; +} .tab-search-input { background: transparent; From 776b485949c7689a896dea2f7ab9e84aabc91c9d Mon Sep 17 00:00:00 2001 From: Yonatan Lifshitz Date: Sun, 7 Dec 2025 01:16:21 +0200 Subject: [PATCH 7/8] Added to command palette --- frontends/web/scripts/TabsController.js | 3 +-- frontends/web/scripts/commandPalette.js | 10 ++++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/frontends/web/scripts/TabsController.js b/frontends/web/scripts/TabsController.js index e40a432..bfad442 100644 --- a/frontends/web/scripts/TabsController.js +++ b/frontends/web/scripts/TabsController.js @@ -324,15 +324,14 @@ class TabsController { }); }); + // TODO: maybe fetch that in a nicer way for class params const tabsScrollBtn = document.getElementsByClassName("tab-expand-button")[0]; if (!tabsScrollBtn || !this.tabsView) return; - console.log("Initiating tab arrow visibility controller"); this.updateTabArrowVisibility = () => { // If the content is wider than the visible area, show the button. const overflowing = this.tabsView.scrollWidth > this.tabsView.clientWidth + 1; const inMultiRow = this.tabsView.classList.contains("multirow"); - console.log("Updating tab arrow visibility:", { overflowing, inMultiRow }); tabsScrollBtn.style.display = overflowing | inMultiRow ? "" : "none"; }; diff --git a/frontends/web/scripts/commandPalette.js b/frontends/web/scripts/commandPalette.js index 23b11cf..5422e5d 100644 --- a/frontends/web/scripts/commandPalette.js +++ b/frontends/web/scripts/commandPalette.js @@ -445,6 +445,16 @@ class CommandPalette { handler: () => { event_copyNodeText(); } + }, + { + id: "ExpandTabBar", + title: "Expand tab bar to show all tabs or collapse it", + hotkey: "Ctrl+t", + icon: iconFor("upside_down_triangle"), + section: "Tabs", + handler: () => { + event_toggleTabBarMode(); + } } ]; } From e317cff1ea2dfde5e4ae756cb7d042e89dbeb4a9 Mon Sep 17 00:00:00 2001 From: Yonatan Lifshitz Date: Sun, 7 Dec 2025 20:47:17 +0200 Subject: [PATCH 8/8] Fixed super minor bug --- frontends/web/scripts/TabsController.js | 1 + 1 file changed, 1 insertion(+) diff --git a/frontends/web/scripts/TabsController.js b/frontends/web/scripts/TabsController.js index bfad442..3e9dd8b 100644 --- a/frontends/web/scripts/TabsController.js +++ b/frontends/web/scripts/TabsController.js @@ -363,6 +363,7 @@ class TabsController { "expand-icon-state", tabListWrapper.classList.contains("multirow") ? "up" : "down" ); + this.updateTabArrowVisibility(); }