From 2630f457b9db40363fe96237298c8f8079aeb1f2 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Sat, 13 Jun 2026 17:33:31 +1000 Subject: [PATCH] fix(app): fade overflowing titlebar tabs (#32082) --- packages/app/src/components/titlebar.css | 46 ++++++++++++++++++++++++ packages/app/src/components/titlebar.tsx | 41 ++++++++++++++------- 2 files changed, 75 insertions(+), 12 deletions(-) create mode 100644 packages/app/src/components/titlebar.css diff --git a/packages/app/src/components/titlebar.css b/packages/app/src/components/titlebar.css new file mode 100644 index 0000000000..edce93c310 --- /dev/null +++ b/packages/app/src/components/titlebar.css @@ -0,0 +1,46 @@ +@keyframes titlebar-tab-fade-left { + from { + visibility: hidden; + } + to { + visibility: visible; + } +} + +@keyframes titlebar-tab-fade-right { + from { + visibility: visible; + } + to { + visibility: hidden; + } +} + +/* The narrow ranges preserve the binary thresholds of scrollLeft > 0 and + remaining scroll distance > 1px. Hidden defaults make an inactive timeline + (no overflow) a no-op. Unsupported browsers also degrade to no fades. */ +[data-slot="titlebar-tabs"] { + timeline-scope: --titlebar-tabs-scroll; + + [data-slot^="titlebar-tabs-fade-"] { + visibility: hidden; + } +} + +@supports (animation-timeline: --titlebar-tabs-scroll) and (timeline-scope: --titlebar-tabs-scroll) { + [data-slot="titlebar-tabs-scroll"] { + scroll-timeline: --titlebar-tabs-scroll x; + } + + [data-slot="titlebar-tabs-fade-left"] { + animation: titlebar-tab-fade-left linear both; + animation-timeline: --titlebar-tabs-scroll; + animation-range: 0 0.1px; + } + + [data-slot="titlebar-tabs-fade-right"] { + animation: titlebar-tab-fade-right linear both; + animation-timeline: --titlebar-tabs-scroll; + animation-range: calc(100% - 1.1px) calc(100% - 1px); + } +} diff --git a/packages/app/src/components/titlebar.tsx b/packages/app/src/components/titlebar.tsx index a4fcde5aac..ef12b827c0 100644 --- a/packages/app/src/components/titlebar.tsx +++ b/packages/app/src/components/titlebar.tsx @@ -34,11 +34,13 @@ import { ProjectAvatar } from "@opencode-ai/ui/v2/project-avatar-v2" import { displayName, getProjectAvatarSource, projectForSession } from "@/pages/layout/helpers" import { useSessionTabAvatarState } from "@/pages/layout/project-avatar-state" import { makeEventListener } from "@solid-primitives/event-listener" +import { createResizeObserver } from "@solid-primitives/resize-observer" import { readSessionTabsRemovedDetail, SESSION_TABS_REMOVED_EVENT } from "@/components/titlebar-session-events" import { useGlobal } from "@/context/global" import { decode64 } from "@/utils/base64" import { ServerConnection, useServer } from "@/context/server" import { tabHref, useTabs, type Tab } from "@/context/tabs" +import "./titlebar.css" type TauriDesktopWindow = { startDragging?: () => Promise @@ -435,18 +437,22 @@ export function Titlebar(props: { update?: TitlebarUpdate }) { state={!!homeMatch() ? "pressed" : undefined} /> -
-
- - {(tab, i) => { - let ref!: HTMLDivElement - - onMount(() => { - refreshTabsAreOverflowing() - }) +
+
{ + tabScrollRef = el + createResizeObserver(el, refreshTabsAreOverflowing) + }} + > +
createResizeObserver(el, refreshTabsAreOverflowing)} + > + + {(tab, i) => { + let ref!: HTMLDivElement const divider = () => i() !== 0 && ( @@ -520,7 +526,18 @@ export function Titlebar(props: { update?: TitlebarUpdate }) { ) }} +
+