From 984d0430d3f09028bf2cdaf31cd7f42676b1d135 Mon Sep 17 00:00:00 2001 From: fs Date: Sun, 22 Mar 2026 17:26:36 +0100 Subject: [PATCH] fix: restyle tab overflow chevrons to subtle ghost icons with fade masks Replace prominent outlined chevron buttons with borderless ghost icons and gradient fade masks for a cleaner overflow indicator. Reset shell-level button custom properties to prevent global button styles from bleeding through. Co-Authored-By: Claude Opus 4.6 (1M context) --- web/css/components/app-tabs.css | 66 +++++++++++++++++++++++++-------- web/js/components/app-tabs.js | 2 + 2 files changed, 53 insertions(+), 15 deletions(-) diff --git a/web/css/components/app-tabs.css b/web/css/components/app-tabs.css index 95bf390..eb1fee6 100644 --- a/web/css/components/app-tabs.css +++ b/web/css/components/app-tabs.css @@ -30,47 +30,61 @@ border-bottom: 1px solid var(--tabs-border); } - /* Chevron scroll buttons — styled like .outline.secondary.small */ + /* Chevron scroll buttons — ghost icons, visible only on overflow. + Reset shell-level button custom properties so the global button + styles (app-shell.css) do not bleed through. */ .app-tabs-nav-wrap > .app-tabs-chevron { + --app-background-color: transparent; + --app-border-color: transparent; + --app-color: var(--tabs-muted); + --app-box-shadow: none; flex: 0 0 auto; display: none; align-items: center; justify-content: center; - border: 1px solid var(--app-border); - border-radius: var(--app-border-radius); - background-color: transparent; - color: var(--app-secondary); - font-size: var(--text-sm); - line-height: var(--leading-none); + border: none; + border-radius: 50%; + background: transparent; + color: var(--tabs-muted); + font-size: var(--text-xs); + line-height: 1; + width: 24px; + height: 24px; + padding: 0; + box-shadow: none; cursor: pointer; - z-index: 1; + z-index: 2; margin-block: auto; transition: background-color var(--app-transition), - border-color var(--app-transition), - color var(--app-transition); + color var(--app-transition), + opacity var(--app-transition); } .app-tabs-nav-wrap > .app-tabs-chevron:hover, .app-tabs-nav-wrap > .app-tabs-chevron:focus-visible { - border-color: var(--app-secondary-hover-border); - color: var(--app-secondary-hover); + --app-background-color: color-mix(in srgb, var(--app-contrast) 8%, transparent); + --app-border-color: transparent; + --app-color: var(--app-contrast); + --app-box-shadow: none; + background: color-mix(in srgb, var(--app-contrast) 8%, transparent); + color: var(--app-contrast); } .app-tabs-nav-wrap > .app-tabs-chevron[disabled] { - opacity: 0.25; + opacity: 0; cursor: default; pointer-events: none; } .app-tabs-nav-wrap > .app-tabs-chevron--start { order: -1; - margin-inline-end: 4px; + margin-inline-end: 2px; } .app-tabs-nav-wrap > .app-tabs-chevron--end { order: 1; - margin-inline-start: 4px; + margin-inline-start: 2px; } /* Show chevrons when overflow is detected */ @@ -78,6 +92,28 @@ display: flex; } + /* Gradient fade masks — only rendered when the corresponding chevron is visible */ + .app-tabs-nav-wrap.has-overflow:not(.at-start)::before, + .app-tabs-nav-wrap.has-overflow:not(.at-end)::after { + content: ''; + position: absolute; + top: 0; + bottom: 0; + width: 32px; + pointer-events: none; + z-index: 1; + } + + .app-tabs-nav-wrap.has-overflow:not(.at-start)::before { + left: 24px; + background: linear-gradient(to right, var(--app-card-background-color, var(--app-background-color)), transparent); + } + + .app-tabs-nav-wrap.has-overflow:not(.at-end)::after { + right: 24px; + background: linear-gradient(to left, var(--app-card-background-color, var(--app-background-color)), transparent); + } + /* ------------------------------------------------------------------ */ /* The scrollable tab strip */ /* ------------------------------------------------------------------ */ diff --git a/web/js/components/app-tabs.js b/web/js/components/app-tabs.js index 7adf017..1a89326 100644 --- a/web/js/components/app-tabs.js +++ b/web/js/components/app-tabs.js @@ -406,6 +406,8 @@ function initChevronScroll(nav) { const atEnd = nav.scrollLeft + nav.clientWidth >= nav.scrollWidth - SCROLL_THRESHOLD; btnStart.disabled = atStart; btnEnd.disabled = atEnd; + wrap.classList.toggle('at-start', atStart); + wrap.classList.toggle('at-end', atEnd); }; bind(nav, 'scroll', update, { passive: true });