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:
@@ -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(
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user