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 { interface SidebarConversationCardProps {
conversation: DbConversationSummary conversation: DbConversationSummary
isSelected: boolean isSelected: boolean
isOpenInTab?: boolean
timeLabel?: string timeLabel?: string
onSelect: (id: number, agentType: string) => void onSelect: (id: number, agentType: string) => void
onDoubleClick?: (id: number, agentType: string) => void onDoubleClick?: (id: number, agentType: string) => void
@@ -79,6 +80,7 @@ interface SidebarConversationCardProps {
export const SidebarConversationCard = memo(function SidebarConversationCard({ export const SidebarConversationCard = memo(function SidebarConversationCard({
conversation, conversation,
isSelected, isSelected,
isOpenInTab = false,
timeLabel, timeLabel,
onSelect, onSelect,
onDoubleClick, onDoubleClick,
@@ -146,14 +148,18 @@ export const SidebarConversationCard = memo(function SidebarConversationCard({
> >
<span <span
aria-hidden 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={{ style={{
left: "0.875rem", left: "calc(0.875rem - 0.5px)",
width: "1px", width: "1px",
transform: "translateX(-50%)",
}} }}
/> />
<SidebarStatusIcon status={beadStatus} /> <SidebarStatusIcon status={beadStatus} emphasized={isOpenInTab} />
<span <span
className={cn( className={cn(

View File

@@ -318,6 +318,16 @@ export function SidebarConversationList({
} }
}, [tabs, activeTabId]) }, [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 [importing, setImporting] = useState(false)
const [folderExpanded, setFolderExpanded] = useState<Record<number, boolean>>( const [folderExpanded, setFolderExpanded] = useState<Record<number, boolean>>(
{} {}
@@ -814,6 +824,9 @@ export function SidebarConversationList({
selectedConversation?.agentType === conv.agent_type && selectedConversation?.agentType === conv.agent_type &&
selectedConversation?.id === conv.id selectedConversation?.id === conv.id
} }
isOpenInTab={openTabConversationKeys.has(
`${conv.agent_type}:${conv.id}`
)}
timeLabel={formatRelative(conv.updated_at)} timeLabel={formatRelative(conv.updated_at)}
onSelect={handleSelect} onSelect={handleSelect}
onDoubleClick={handleDoubleClick} onDoubleClick={handleDoubleClick}

View File

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