refactor(sidebar): emphasize active-tab conversations via rail color and status icon

- Highlight the vertical rail on conversations open in tabs with a darker primary tint and bleed 1px at both ends to stay continuous across cards
- Add emphasized state to SidebarStatusIcon mirroring the rail, deepening color when the conversation is open in a tab
- Redesign done icon as outlined circle + check, unify all status icons to sidebar-primary with consistent 0.75rem size and sidebar-tinted backdrop so they mask the rail
This commit is contained in:
xintaofei
2026-04-22 18:37:01 +08:00
parent 1d40308f52
commit 1904013dac
3 changed files with 84 additions and 31 deletions

View File

@@ -67,6 +67,7 @@ const STATUS_ICON_COLORS: Record<ConversationStatus, string> = {
interface SidebarConversationCardProps {
conversation: DbConversationSummary
isSelected: boolean
isOpenInTab?: boolean
timeLabel?: string
onSelect: (id: number, agentType: string) => void
onDoubleClick?: (id: number, agentType: string) => void
@@ -79,6 +80,7 @@ interface SidebarConversationCardProps {
export const SidebarConversationCard = memo(function SidebarConversationCard({
conversation,
isSelected,
isOpenInTab = false,
timeLabel,
onSelect,
onDoubleClick,
@@ -146,14 +148,18 @@ export const SidebarConversationCard = memo(function SidebarConversationCard({
>
<span
aria-hidden
className="pointer-events-none absolute top-0 bottom-0 rounded-[0.125rem] bg-sidebar-primary/15"
className={cn(
"pointer-events-none absolute -top-px -bottom-px z-0",
isOpenInTab
? "bg-sidebar-primary/85"
: "bg-sidebar-primary/30"
)}
style={{
left: "0.875rem",
left: "calc(0.875rem - 0.5px)",
width: "1px",
transform: "translateX(-50%)",
}}
/>
<SidebarStatusIcon status={beadStatus} />
<SidebarStatusIcon status={beadStatus} emphasized={isOpenInTab} />
<span
className={cn(

View File

@@ -318,6 +318,16 @@ export function SidebarConversationList({
}
}, [tabs, activeTabId])
const openTabConversationKeys = useMemo(() => {
const set = new Set<string>()
for (const tab of tabs) {
if (tab.conversationId != null) {
set.add(`${tab.agentType}:${tab.conversationId}`)
}
}
return set
}, [tabs])
const [importing, setImporting] = useState(false)
const [folderExpanded, setFolderExpanded] = useState<Record<number, boolean>>(
{}
@@ -814,6 +824,9 @@ export function SidebarConversationList({
selectedConversation?.agentType === conv.agent_type &&
selectedConversation?.id === conv.id
}
isOpenInTab={openTabConversationKeys.has(
`${conv.agent_type}:${conv.id}`
)}
timeLabel={formatRelative(conv.updated_at)}
onSelect={handleSelect}
onDoubleClick={handleDoubleClick}

View File

@@ -6,6 +6,7 @@ export type SidebarBeadStatus = "done" | "active" | "running" | "failed"
interface SidebarStatusIconProps {
status: SidebarBeadStatus
emphasized?: boolean
className?: string
}
@@ -21,15 +22,15 @@ function IconFrame({
return (
<div
className={cn(
"pointer-events-none absolute top-1/2",
"flex items-center justify-center",
"pointer-events-none absolute top-1/2 z-10",
"flex items-center justify-center rounded-full bg-sidebar",
colorClass,
className
)}
style={{
left: "0.875rem",
width: "0.625rem",
height: "0.625rem",
width: "0.75rem",
height: "0.75rem",
transform: "translate(-50%, -50%)",
}}
aria-hidden
@@ -41,34 +42,37 @@ function IconFrame({
export function SidebarStatusIcon({
status,
emphasized = false,
className,
}: SidebarStatusIconProps) {
if (status === "running") {
return (
<IconFrame
colorClass="text-amber-600 dark:text-amber-400"
colorClass={
emphasized ? "text-sidebar-primary" : "text-sidebar-primary/65"
}
className={className}
>
<svg
width="0.625rem"
height="0.625rem"
width="0.75rem"
height="0.75rem"
viewBox="0 0 10 10"
preserveAspectRatio="xMidYMid meet"
>
<circle
cx="5"
cy="5"
r="3.6"
r="3.8"
fill="none"
stroke="currentColor"
strokeWidth="1.2"
strokeWidth="1.1"
opacity="0.28"
/>
<path
d="M5 1.4 A 3.6 3.6 0 1 1 1.4 5"
d="M5 1.2 A 3.8 3.8 0 1 1 1.2 5"
fill="none"
stroke="currentColor"
strokeWidth="1.4"
strokeWidth="1.3"
strokeLinecap="round"
>
<animateTransform
@@ -87,26 +91,31 @@ export function SidebarStatusIcon({
if (status === "failed") {
return (
<IconFrame colorClass="text-destructive" className={className}>
<IconFrame
colorClass={
emphasized ? "text-sidebar-primary" : "text-sidebar-primary/65"
}
className={className}
>
<svg
width="0.625rem"
height="0.625rem"
width="0.75rem"
height="0.75rem"
viewBox="0 0 10 10"
preserveAspectRatio="xMidYMid meet"
>
<circle
cx="5"
cy="5"
r="3.8"
r="3.9"
fill="none"
stroke="currentColor"
strokeWidth="1.2"
strokeWidth="1.1"
/>
<path
d="M3.5 3.5L6.5 6.5M6.5 3.5L3.5 6.5"
d="M3.4 3.4L6.6 6.6M6.6 3.4L3.4 6.6"
fill="none"
stroke="currentColor"
strokeWidth="1.4"
strokeWidth="1.3"
strokeLinecap="round"
/>
</svg>
@@ -116,20 +125,25 @@ export function SidebarStatusIcon({
if (status === "active") {
return (
<IconFrame colorClass="text-sidebar-primary" className={className}>
<IconFrame
colorClass={
emphasized ? "text-sidebar-primary" : "text-sidebar-primary/65"
}
className={className}
>
<svg
width="0.625rem"
height="0.625rem"
width="0.75rem"
height="0.75rem"
viewBox="0 0 10 10"
preserveAspectRatio="xMidYMid meet"
>
<circle
cx="5"
cy="5"
r="3.8"
r="3.9"
fill="none"
stroke="currentColor"
strokeWidth="1.2"
strokeWidth="1.1"
opacity="0.35"
/>
<circle cx="5" cy="5" r="2" fill="currentColor" />
@@ -139,14 +153,34 @@ export function SidebarStatusIcon({
}
return (
<IconFrame colorClass="text-sidebar-primary/40" className={className}>
<IconFrame
colorClass={
emphasized ? "text-sidebar-primary/75" : "text-sidebar-primary/40"
}
className={className}
>
<svg
width="0.625rem"
height="0.625rem"
width="0.75rem"
height="0.75rem"
viewBox="0 0 10 10"
preserveAspectRatio="xMidYMid meet"
>
<circle cx="5" cy="5" r="3" fill="currentColor" />
<circle
cx="5"
cy="5"
r="3.9"
fill="none"
stroke="currentColor"
strokeWidth="1.1"
/>
<path
d="M3.2 5.1 L4.4 6.3 L6.9 3.6"
fill="none"
stroke="currentColor"
strokeWidth="1.3"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</IconFrame>
)