fix(app): handle tab overflow and scrolling in titlebar (#30886)
This commit is contained in:
@@ -1,4 +1,16 @@
|
|||||||
import { createEffect, createMemo, createResource, For, Match, Show, startTransition, Switch, untrack } from "solid-js"
|
import {
|
||||||
|
createEffect,
|
||||||
|
createMemo,
|
||||||
|
createResource,
|
||||||
|
createSignal,
|
||||||
|
For,
|
||||||
|
Match,
|
||||||
|
onMount,
|
||||||
|
Show,
|
||||||
|
startTransition,
|
||||||
|
Switch,
|
||||||
|
untrack,
|
||||||
|
} from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { useLocation, useMatch, useNavigate, useParams } from "@solidjs/router"
|
import { useLocation, useMatch, useNavigate, useParams } from "@solidjs/router"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
@@ -230,7 +242,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
|||||||
: undefined,
|
: undefined,
|
||||||
"align-self": electronWindows() ? "flex-start" : undefined,
|
"align-self": electronWindows() ? "flex-start" : undefined,
|
||||||
}}
|
}}
|
||||||
data-tauri-drag-region
|
// data-tauri-drag-region
|
||||||
onMouseDown={drag}
|
onMouseDown={drag}
|
||||||
onDblClick={maximize}
|
onDblClick={maximize}
|
||||||
>
|
>
|
||||||
@@ -393,9 +405,16 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
|||||||
].filter((v) => v !== undefined)
|
].filter((v) => v !== undefined)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const [tabsAreOverflowing, setTabsAreOverflowing] = createSignal(false)
|
||||||
|
let tabScrollRef!: HTMLDivElement
|
||||||
|
|
||||||
|
function refreshTabsAreOverflowing() {
|
||||||
|
setTabsAreOverflowing(tabScrollRef.scrollWidth > tabScrollRef.clientWidth)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class="h-full flex-1 flex flex-row items-center gap-1.5 pr-3 pt-2"
|
class="h-full flex-1 overflow-hidden flex flex-row items-center gap-1.5 pr-3 pt-2"
|
||||||
classList={{
|
classList={{
|
||||||
"pl-2": mac(),
|
"pl-2": mac(),
|
||||||
"pl-4": !mac(),
|
"pl-4": !mac(),
|
||||||
@@ -410,60 +429,86 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
|||||||
size="large"
|
size="large"
|
||||||
as="a"
|
as="a"
|
||||||
href="/"
|
href="/"
|
||||||
class="!w-9"
|
class="!w-9 shrink-0"
|
||||||
icon={<IconV2 name="grid-plus" />}
|
icon={<IconV2 name="grid-plus" />}
|
||||||
state={!!homeMatch() ? "pressed" : undefined}
|
state={!!homeMatch() ? "pressed" : undefined}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="flex min-w-0 flex-1 flex-row items-center gap-1.5 overflow-hidden">
|
<div class="flex min-w-0 flex-row items-center gap-1.5 overflow-x-auto no-scrollbar" ref={tabScrollRef}>
|
||||||
<div class="flex min-w-0 flex-row items-center gap-1.5 overflow-hidden">
|
<div class="flex min-w-0 flex-row items-center gap-1.5">
|
||||||
<For each={tabsStore}>
|
<For each={[...tabsStore, ...tabsStore, ...tabsStore]}>
|
||||||
{(tab, i) => (
|
{(tab, i) => {
|
||||||
<>
|
let ref!: HTMLDivElement
|
||||||
{i() !== 0 && (
|
|
||||||
<div class="w-[1.5px] h-3 shrink-0 rounded-full bg-[var(--v2-background-bg-layer-02)]" />
|
onMount(() => {
|
||||||
)}
|
refreshTabsAreOverflowing()
|
||||||
<TabNavItem
|
})
|
||||||
href={tabHref(tab)}
|
|
||||||
server={tab.server}
|
return (
|
||||||
directory={decode64(tab.dirBase64)!}
|
<>
|
||||||
sessionId={tab.sessionId}
|
{i() !== 0 && (
|
||||||
onNavigate={() => navigateTab(tab)}
|
<div class="w-[1.5px] h-3 shrink-0 rounded-full bg-[var(--v2-background-bg-layer-02)]" />
|
||||||
onClose={() => tabsStoreActions.removeTab(i())}
|
)}
|
||||||
active={currentTab() === tab}
|
<TabNavItem
|
||||||
activeServer={tab.server === server.key}
|
ref={ref}
|
||||||
/>
|
href={tabHref(tab)}
|
||||||
</>
|
server={tab.server}
|
||||||
)}
|
directory={decode64(tab.dirBase64)!}
|
||||||
</For>
|
sessionId={tab.sessionId}
|
||||||
</div>
|
onNavigate={() => {
|
||||||
<Show
|
navigateTab(tab)
|
||||||
when={creating() && params.dir}
|
|
||||||
fallback={
|
ref.scrollIntoView({ behavior: "instant" })
|
||||||
<IconButtonV2
|
}}
|
||||||
type="button"
|
onClose={() => tabsStoreActions.removeTab(i())}
|
||||||
variant="ghost-muted"
|
active={currentTab() === tab}
|
||||||
size="large"
|
activeServer={tab.server === server.key}
|
||||||
class="shrink-0"
|
forceTruncate={tabsAreOverflowing()}
|
||||||
icon={<IconV2 name="plus" />}
|
/>
|
||||||
as="a"
|
</>
|
||||||
href={newSessionHref()}
|
)
|
||||||
aria-label={language.t("command.session.new")}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<NewSessionTabItem
|
|
||||||
href={`/${params.dir}/session`}
|
|
||||||
title={language.t("command.session.new")}
|
|
||||||
onClose={() => {
|
|
||||||
const tab = tabsStore.at(-1)
|
|
||||||
if (tab) navigateTab(tab)
|
|
||||||
else navigate("/")
|
|
||||||
}}
|
}}
|
||||||
/>
|
</For>
|
||||||
</Show>
|
<Show when={creating() && params.dir}>
|
||||||
<div class="min-w-0 flex-1" />
|
{(_) => {
|
||||||
|
let ref!: HTMLDivElement
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
ref.scrollIntoView({ behavior: "instant" })
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div class="w-[1.5px] h-3 shrink-0 rounded-full bg-[var(--v2-background-bg-layer-02)]" />
|
||||||
|
<NewSessionTabItem
|
||||||
|
ref={ref}
|
||||||
|
href={`/${params.dir}/session`}
|
||||||
|
title={language.t("command.session.new")}
|
||||||
|
onClose={() => {
|
||||||
|
const tab = tabsStore.at(-1)
|
||||||
|
if (tab) navigateTab(tab)
|
||||||
|
else navigate("/")
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Show when={!(creating() && params.dir)}>
|
||||||
|
<IconButtonV2
|
||||||
|
type="button"
|
||||||
|
variant="ghost-muted"
|
||||||
|
size="large"
|
||||||
|
class="shrink-0"
|
||||||
|
icon={<IconV2 name="plus" />}
|
||||||
|
as="a"
|
||||||
|
href={newSessionHref()}
|
||||||
|
aria-label={language.t("command.session.new")}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
<div class="flex-1" />
|
||||||
<TitlebarV2Right state={v2RightState()} />
|
<TitlebarV2Right state={v2RightState()} />
|
||||||
<Show when={windows() && !electronWindows()}>
|
<Show when={windows() && !electronWindows()}>
|
||||||
<div data-tauri-decorum-tb class="flex flex-row" />
|
<div data-tauri-decorum-tb class="flex flex-row" />
|
||||||
@@ -615,7 +660,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
|||||||
"flex items-center min-w-0 justify-end": true,
|
"flex items-center min-w-0 justify-end": true,
|
||||||
"pr-2": !windows(),
|
"pr-2": !windows(),
|
||||||
}}
|
}}
|
||||||
data-tauri-drag-region
|
// data-tauri-drag-region
|
||||||
onMouseDown={drag}
|
onMouseDown={drag}
|
||||||
>
|
>
|
||||||
<div id="opencode-titlebar-right" class="flex items-center gap-1 shrink-0 justify-end" />
|
<div id="opencode-titlebar-right" class="flex items-center gap-1 shrink-0 justify-end" />
|
||||||
@@ -685,6 +730,7 @@ function TitlebarUpdateIconButton(props: { state: TitlebarUpdatePillState }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function TabNavItem(props: {
|
function TabNavItem(props: {
|
||||||
|
ref?: HTMLDivElement
|
||||||
href: string
|
href: string
|
||||||
server: ServerConnection.Key
|
server: ServerConnection.Key
|
||||||
directory: string
|
directory: string
|
||||||
@@ -694,6 +740,7 @@ function TabNavItem(props: {
|
|||||||
onNavigate: () => void
|
onNavigate: () => void
|
||||||
active?: boolean
|
active?: boolean
|
||||||
activeServer: boolean
|
activeServer: boolean
|
||||||
|
forceTruncate?: boolean
|
||||||
}) {
|
}) {
|
||||||
const closeTab = (event: MouseEvent) => {
|
const closeTab = (event: MouseEvent) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
@@ -710,6 +757,7 @@ function TabNavItem(props: {
|
|||||||
const [session] = createResource(
|
const [session] = createResource(
|
||||||
() => {
|
() => {
|
||||||
const ctx = dirSyncCtx()
|
const ctx = dirSyncCtx()
|
||||||
|
console.log({ ctx, sessionId: props.sessionId })
|
||||||
if (!ctx || !props.sessionId) return
|
if (!ctx || !props.sessionId) return
|
||||||
return [props.sessionId, ctx] as const
|
return [props.sessionId, ctx] as const
|
||||||
},
|
},
|
||||||
@@ -722,6 +770,7 @@ function TabNavItem(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
ref={props.ref}
|
||||||
class="group relative flex h-7 min-w-24 max-w-60 flex-row items-center gap-1.5 overflow-hidden whitespace-nowrap rounded-[6px] bg-[var(--tab-bg)] px-1.5 [--tab-bg:var(--v2-background-bg-deep)] hover:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:[--tab-bg:var(--v2-background-bg-layer-02)]"
|
class="group relative flex h-7 min-w-24 max-w-60 flex-row items-center gap-1.5 overflow-hidden whitespace-nowrap rounded-[6px] bg-[var(--tab-bg)] px-1.5 [--tab-bg:var(--v2-background-bg-deep)] hover:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:[--tab-bg:var(--v2-background-bg-layer-02)]"
|
||||||
data-active={props.active}
|
data-active={props.active}
|
||||||
onMouseDown={(event) => {
|
onMouseDown={(event) => {
|
||||||
@@ -731,6 +780,7 @@ function TabNavItem(props: {
|
|||||||
>
|
>
|
||||||
<Show when={session.latest}>
|
<Show when={session.latest}>
|
||||||
{(session) => {
|
{(session) => {
|
||||||
|
console.log({ session: session() })
|
||||||
const project = createMemo(() => projectForSession(session(), serverCtx()?.projects.list() ?? []))
|
const project = createMemo(() => projectForSession(session(), serverCtx()?.projects.list() ?? []))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -756,7 +806,10 @@ function TabNavItem(props: {
|
|||||||
}}
|
}}
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<div class="absolute not-group-hover:not-group-data-[active=true]:left-52 group-hover:right-0 group-data-[active=true]:right-0 inset-y-0 flex flex-row items-center pr-1 py-1 w-8 pl-2">
|
<div
|
||||||
|
class="absolute not-group-hover:not-group-data-[active=true]:not-data-[truncate=true]:left-52 group-hover:right-0 group-data-[active=true]:right-0 data-[truncate=true]:right-0 inset-y-0 flex flex-row items-center pr-1 py-1 w-8 pl-2"
|
||||||
|
data-truncate={props.forceTruncate}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 rounded-r-[6px] bg-(image:--inactive-bg) group-hover:bg-(image:--active-bg) group-data-[active=true]:bg-(image:--active-bg)"
|
class="absolute inset-0 rounded-r-[6px] bg-(image:--inactive-bg) group-hover:bg-(image:--active-bg) group-data-[active=true]:bg-(image:--active-bg)"
|
||||||
style={{
|
style={{
|
||||||
@@ -796,7 +849,7 @@ function ProjectTabAvatar(props: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function NewSessionTabItem(props: { href: string; title: string; onClose: () => void }) {
|
function NewSessionTabItem(props: { ref?: HTMLDivElement; href: string; title: string; onClose: () => void }) {
|
||||||
const closeTab = (event: MouseEvent) => {
|
const closeTab = (event: MouseEvent) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
@@ -804,7 +857,8 @@ function NewSessionTabItem(props: { href: string; title: string; onClose: () =>
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class="group relative flex h-7 max-w-60 flex-row items-center gap-1.5 overflow-hidden rounded-[6px] bg-[var(--v2-overlay-simple-overlay-pressed)] pl-1.5 pr-8 whitespace-nowrap focus-within:outline focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-[var(--v2-border-border-focus)]"
|
ref={props.ref}
|
||||||
|
class="group relative shrink-0 flex h-7 max-w-60 flex-row items-center gap-1.5 overflow-hidden rounded-[6px] bg-[var(--v2-overlay-simple-overlay-pressed)] pl-1.5 pr-8 whitespace-nowrap focus-within:outline focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-[var(--v2-border-border-focus)]"
|
||||||
onMouseDown={(event) => {
|
onMouseDown={(event) => {
|
||||||
if (event.button !== 1) return
|
if (event.button !== 1) return
|
||||||
closeTab(event)
|
closeTab(event)
|
||||||
|
|||||||
Reference in New Issue
Block a user