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