fix(app): fade overflowing titlebar tabs (#32082)
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,11 +34,13 @@ import { ProjectAvatar } from "@opencode-ai/ui/v2/project-avatar-v2"
|
|||||||
import { displayName, getProjectAvatarSource, projectForSession } from "@/pages/layout/helpers"
|
import { displayName, getProjectAvatarSource, projectForSession } from "@/pages/layout/helpers"
|
||||||
import { useSessionTabAvatarState } from "@/pages/layout/project-avatar-state"
|
import { useSessionTabAvatarState } from "@/pages/layout/project-avatar-state"
|
||||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
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 { readSessionTabsRemovedDetail, SESSION_TABS_REMOVED_EVENT } from "@/components/titlebar-session-events"
|
||||||
import { useGlobal } from "@/context/global"
|
import { useGlobal } from "@/context/global"
|
||||||
import { decode64 } from "@/utils/base64"
|
import { decode64 } from "@/utils/base64"
|
||||||
import { ServerConnection, useServer } from "@/context/server"
|
import { ServerConnection, useServer } from "@/context/server"
|
||||||
import { tabHref, useTabs, type Tab } from "@/context/tabs"
|
import { tabHref, useTabs, type Tab } from "@/context/tabs"
|
||||||
|
import "./titlebar.css"
|
||||||
|
|
||||||
type TauriDesktopWindow = {
|
type TauriDesktopWindow = {
|
||||||
startDragging?: () => Promise<void>
|
startDragging?: () => Promise<void>
|
||||||
@@ -435,18 +437,22 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
|||||||
state={!!homeMatch() ? "pressed" : undefined}
|
state={!!homeMatch() ? "pressed" : undefined}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div data-slot="titlebar-tabs" class="relative min-w-0">
|
||||||
class="flex min-w-0 flex-row items-center gap-1.5 overflow-x-auto no-scrollbar [app-region:no-drag]"
|
<div
|
||||||
ref={tabScrollRef}
|
data-slot="titlebar-tabs-scroll"
|
||||||
>
|
class="flex min-w-0 flex-row items-center gap-1.5 overflow-x-auto no-scrollbar [app-region:no-drag]"
|
||||||
<div class="flex min-w-0 flex-row items-center gap-1.5">
|
ref={(el) => {
|
||||||
<For each={tabsStore}>
|
tabScrollRef = el
|
||||||
{(tab, i) => {
|
createResizeObserver(el, refreshTabsAreOverflowing)
|
||||||
let ref!: HTMLDivElement
|
}}
|
||||||
|
>
|
||||||
onMount(() => {
|
<div
|
||||||
refreshTabsAreOverflowing()
|
class="flex min-w-0 flex-row items-center gap-1.5"
|
||||||
})
|
ref={(el) => createResizeObserver(el, refreshTabsAreOverflowing)}
|
||||||
|
>
|
||||||
|
<For each={tabsStore}>
|
||||||
|
{(tab, i) => {
|
||||||
|
let ref!: HTMLDivElement
|
||||||
|
|
||||||
const divider = () =>
|
const divider = () =>
|
||||||
i() !== 0 && (
|
i() !== 0 && (
|
||||||
@@ -520,7 +526,18 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
|||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</Show>
|
</Show>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
data-slot="titlebar-tabs-fade-left"
|
||||||
|
aria-hidden="true"
|
||||||
|
class="pointer-events-none absolute inset-y-0 left-0 z-10 w-6 bg-[linear-gradient(to_right,var(--v2-background-bg-deep),transparent)]"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
data-slot="titlebar-tabs-fade-right"
|
||||||
|
aria-hidden="true"
|
||||||
|
class="pointer-events-none absolute inset-y-0 right-0 z-10 w-6 bg-[linear-gradient(to_left,var(--v2-background-bg-deep),transparent)]"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Show when={!(creating() && params.dir)}>
|
<Show when={!(creating() && params.dir)}>
|
||||||
<IconButtonV2
|
<IconButtonV2
|
||||||
|
|||||||
Reference in New Issue
Block a user