feat(desktop): refine session tab preview popover (#34792)
This commit is contained in:
parent
3cf71808c4
commit
917eca4f55
3 changed files with 29 additions and 48 deletions
|
|
@ -71,11 +71,6 @@ export function TabNavItem(props: {
|
||||||
const home = serverCtx()?.sync.data.path.home
|
const home = serverCtx()?.sync.data.path.home
|
||||||
return home ? session.directory.replace(home, "~") : session.directory
|
return home ? session.directory.replace(home, "~") : session.directory
|
||||||
})
|
})
|
||||||
const branch = createMemo(() => {
|
|
||||||
const session = props.session()
|
|
||||||
if (!session) return
|
|
||||||
return serverCtx()?.sync.child(session.directory, { bootstrap: false })[0].vcs?.branch
|
|
||||||
})
|
|
||||||
// Only label the server when multiple servers are connected.
|
// Only label the server when multiple servers are connected.
|
||||||
const serverLabel = createMemo(() => {
|
const serverLabel = createMemo(() => {
|
||||||
if (global.servers.list().length <= 1) return
|
if (global.servers.list().length <= 1) return
|
||||||
|
|
@ -322,7 +317,6 @@ export function TabNavItem(props: {
|
||||||
projectName: projectName(),
|
projectName: projectName(),
|
||||||
title: props.session()?.title,
|
title: props.session()?.title,
|
||||||
path: previewPath(),
|
path: previewPath(),
|
||||||
branch: branch(),
|
|
||||||
serverName: serverLabel(),
|
serverName: serverLabel(),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
width: 256px;
|
width: 256px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
|
|
||||||
background: var(--v2-background-bg-base);
|
background: var(--v2-background-bg-base);
|
||||||
|
|
@ -23,30 +23,37 @@
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
transform-origin: var(--kb-hovercard-content-transform-origin);
|
transform-origin: var(--kb-hovercard-content-transform-origin);
|
||||||
/* Entrance: fade in while scaling up from 0.96 to 1.0. */
|
/* Entrance: instant (trying no animation — may revert to 120ms ease-out). */
|
||||||
animation: sessionTabPopoverIn 150ms cubic-bezier(0.16, 1, 0.3, 1);
|
animation: none;
|
||||||
|
|
||||||
&:focus-visible,
|
&:focus-visible,
|
||||||
&:focus-within {
|
&:focus-within {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Exit: subtle fade + scale down, using the same ease-out curve as entry. */
|
/* Exit: instant (trying no animation — may revert to 80ms ease-out). */
|
||||||
&[data-closed] {
|
&[data-closed] {
|
||||||
animation: sessionTabPopoverOut 100ms cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Warm streak (rapid tab hopping): appear/disappear instantly so the enter
|
||||||
|
and exit animations don't repeat on every tab. */
|
||||||
|
&[data-instant],
|
||||||
|
&[data-instant][data-closed] {
|
||||||
|
animation: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-slot="header"] {
|
[data-slot="header"] {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-slot="project"] {
|
[data-slot="project"] {
|
||||||
font-weight: 440;
|
font-weight: 440;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1;
|
line-height: 1.5;
|
||||||
letter-spacing: -0.04px;
|
letter-spacing: -0.04px;
|
||||||
color: var(--v2-text-text-muted);
|
color: var(--v2-text-text-muted);
|
||||||
}
|
}
|
||||||
|
|
@ -54,7 +61,7 @@
|
||||||
[data-slot="title"] {
|
[data-slot="title"] {
|
||||||
font-weight: 530;
|
font-weight: 530;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1.2;
|
line-height: 1.5;
|
||||||
letter-spacing: -0.04px;
|
letter-spacing: -0.04px;
|
||||||
color: var(--v2-text-text-base);
|
color: var(--v2-text-text-base);
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
|
|
@ -62,31 +69,18 @@
|
||||||
|
|
||||||
[data-slot="row"] {
|
[data-slot="row"] {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-slot="icon"] {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
color: var(--v2-icon-icon-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="detail"] {
|
[data-slot="detail"] {
|
||||||
flex: 1 0 0;
|
flex: 1 0 0;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
/* Wrap the full path onto the next line instead of clipping the end. */
|
||||||
text-overflow: ellipsis;
|
overflow-wrap: anywhere;
|
||||||
white-space: nowrap;
|
|
||||||
font-weight: 440;
|
font-weight: 440;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1;
|
line-height: 1.5;
|
||||||
letter-spacing: -0.04px;
|
letter-spacing: -0.04px;
|
||||||
color: var(--v2-text-text-muted);
|
color: var(--v2-text-text-muted);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { HoverCard as Kobalte } from "@kobalte/core/hover-card"
|
import { HoverCard as Kobalte } from "@kobalte/core/hover-card"
|
||||||
import { Show, type JSXElement } from "solid-js"
|
import { createSignal, Show, type JSXElement } from "solid-js"
|
||||||
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
|
||||||
import "./titlebar-tab-popover.css"
|
import "./titlebar-tab-popover.css"
|
||||||
|
|
||||||
// Initial hover delay before the preview appears, per design.
|
// Initial hover delay before the preview appears, per design.
|
||||||
|
|
@ -10,14 +9,13 @@ const CLOSE_DELAY = 0
|
||||||
// After a preview closes, hovering a neighbouring tab within this window skips
|
// After a preview closes, hovering a neighbouring tab within this window skips
|
||||||
// the open delay — mirrors the tooltip's skipDelayDuration so moving across
|
// the open delay — mirrors the tooltip's skipDelayDuration so moving across
|
||||||
// tabs doesn't re-wait the full delay each time.
|
// tabs doesn't re-wait the full delay each time.
|
||||||
const SKIP_WINDOW = 300
|
const SKIP_WINDOW = 500
|
||||||
let lastClosedAt = 0
|
let lastClosedAt = 0
|
||||||
|
|
||||||
export interface TabPreviewData {
|
export interface TabPreviewData {
|
||||||
projectName?: string
|
projectName?: string
|
||||||
title?: string
|
title?: string
|
||||||
path?: string
|
path?: string
|
||||||
branch?: string
|
|
||||||
serverName?: string
|
serverName?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -28,12 +26,18 @@ export function TabPreviewPopover(props: {
|
||||||
data: TabPreviewData
|
data: TabPreviewData
|
||||||
}) {
|
}) {
|
||||||
let triggerEl: HTMLDivElement | undefined
|
let triggerEl: HTMLDivElement | undefined
|
||||||
|
// When opened during a rapid tab-hopping streak, this preview appears and
|
||||||
|
// disappears instantly (no repeated enter/exit animation) — only the first,
|
||||||
|
// "cold" preview animates. Mirrors how browsers reuse one tab tooltip.
|
||||||
|
const [instant, setInstant] = createSignal(false)
|
||||||
|
|
||||||
|
const warm = () => Date.now() - lastClosedAt < SKIP_WINDOW
|
||||||
// Kobalte reads openDelay lazily when the pointer enters the trigger, so this
|
// Kobalte reads openDelay lazily when the pointer enters the trigger, so this
|
||||||
// resolves the skip window per-hover.
|
// resolves the skip window per-hover.
|
||||||
const resolveOpenDelay = () => (Date.now() - lastClosedAt < SKIP_WINDOW ? 0 : OPEN_DELAY)
|
const resolveOpenDelay = () => (warm() ? 0 : OPEN_DELAY)
|
||||||
const handleOpenChange = (open: boolean) => {
|
const handleOpenChange = (open: boolean) => {
|
||||||
if (!open) lastClosedAt = Date.now()
|
if (open) setInstant(warm())
|
||||||
|
else lastClosedAt = Date.now()
|
||||||
props.onOpenChange(open)
|
props.onOpenChange(open)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,6 +65,7 @@ export function TabPreviewPopover(props: {
|
||||||
if (theme) el.setAttribute("data-theme", theme)
|
if (theme) el.setAttribute("data-theme", theme)
|
||||||
}}
|
}}
|
||||||
data-component="session-tab-popover"
|
data-component="session-tab-popover"
|
||||||
|
data-instant={instant() || undefined}
|
||||||
>
|
>
|
||||||
<div data-slot="header">
|
<div data-slot="header">
|
||||||
<Show when={props.data.projectName}>
|
<Show when={props.data.projectName}>
|
||||||
|
|
@ -73,22 +78,10 @@ export function TabPreviewPopover(props: {
|
||||||
|
|
||||||
<Show when={props.data.path}>
|
<Show when={props.data.path}>
|
||||||
<div data-slot="row">
|
<div data-slot="row">
|
||||||
<span data-slot="icon">
|
|
||||||
<IconV2 name="folder" />
|
|
||||||
</span>
|
|
||||||
<span data-slot="detail">{props.data.path}</span>
|
<span data-slot="detail">{props.data.path}</span>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={props.data.branch}>
|
|
||||||
<div data-slot="row">
|
|
||||||
<span data-slot="icon">
|
|
||||||
<IconV2 name="branch" />
|
|
||||||
</span>
|
|
||||||
<span data-slot="detail">{props.data.branch}</span>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show when={props.data.serverName}>
|
<Show when={props.data.serverName}>
|
||||||
<div data-slot="server">{props.data.serverName}</div>
|
<div data-slot="server">{props.data.serverName}</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue