style(TodoPanel): 优化待办面板动画和样式细节

- 新增待办卡片及条目动画效果,提升界面动感
- 添加待办分组折叠内容展开/收起过渡样式
- 优化按钮尺寸及颜色悬浮效果,提升交互体验
- 自动展开新待办项,空列表时自动收起
- 调整待办面板大小、圆角及阴影效果,增强视觉层次
- 改进待办标题栏排版及交互样式,支持拖动操作手感
- 为无待办状态提供占位展示及提示文字
- 优化待办分组标题样式及折叠箭头动画
- 改进待办条目列表项样式,支持悬浮高亮显示
- 移除消息气泡中的冗余子智能体标签显示内容
This commit is contained in:
oudecheng 2026-06-18 16:15:08 +08:00
parent 301506a3b1
commit 421714dfa3
3 changed files with 105 additions and 43 deletions

View File

@ -395,11 +395,6 @@ export function MessageBubble({ message, onNavigateToSubAgent, showThinking = tr
·{subagentType}
</span>
)}
{!isTaskTool && isSubAgent && (
<span className="text-xs px-1.5 py-0.5 rounded bg-violet-500/10 text-violet-400">
</span>
)}
<span className="text-xs text-[var(--text-muted)]">{formatTime(message.timestamp)}</span>
</div>
<div

View File

@ -77,11 +77,15 @@ export function TodoPanel({ todos, requestTodoList, sendCommand }: TodoPanelProp
localStorage.setItem('picobot-todo-expanded', String(expanded))
}, [expanded])
// auto-expand on new items
// auto-expand on new items, auto-collapse when empty
useEffect(() => {
const newIds = new Set(todos.map(t => t.id))
const hasNewItems = todos.some(t => !prevTodoIdsRef.current.has(t.id))
if (hasNewItems && todos.length > 0) setExpanded(true)
if (todos.length === 0) {
setExpanded(false)
} else {
const hasNewItems = todos.some(t => !prevTodoIdsRef.current.has(t.id))
if (hasNewItems) setExpanded(true)
}
prevTodoIdsRef.current = newIds
}, [todos])
@ -152,26 +156,23 @@ export function TodoPanel({ todos, requestTodoList, sendCommand }: TodoPanelProp
>
<button
onClick={() => { if (totalCount > 0) setExpanded(true); else handleRefresh() }}
className="relative w-12 h-12 rounded-full bg-[var(--bg-tertiary)]/90 backdrop-blur-md border border-[var(--border-color)] hover:border-[var(--accent-cyan)]/40 shadow-lg hover:shadow-[0_0_20px_var(--shadow-glow-sm)] transition-all duration-300 group"
className="relative w-14 h-14 rounded-full bg-[var(--bg-tertiary)]/90 backdrop-blur-xl border border-[var(--border-color)] hover:border-[var(--accent-cyan)]/50 shadow-[0_4px_24px_rgba(0,240,255,0.08)] hover:shadow-[0_4px_32px_var(--shadow-glow-sm)] transition-all duration-300 group"
title="待办"
>
<div className="flex flex-col items-center justify-center">
<ClipboardList className="h-3.5 w-3.5 text-[var(--text-muted)] group-hover:text-[var(--accent-cyan)] transition-colors" />
<span className="text-[8px] font-bold text-[var(--text-muted)] group-hover:text-[var(--accent-cyan)] transition-colors mt-0.5">
ToDo
</span>
<div className="flex items-center justify-center">
<ClipboardList className="h-5 w-5 text-[var(--text-muted)] group-hover:text-[var(--accent-cyan)] transition-colors" />
</div>
{totalCount > 0 && (
<span className={`absolute -top-1 -right-1 min-w-[18px] h-[18px] rounded-full flex items-center justify-center text-[10px] font-bold shadow-md ${
<span className={`absolute -top-1.5 -right-1.5 min-w-[20px] h-5 px-1 rounded-full flex items-center justify-center text-[11px] font-bold leading-tight ${
inProgressCount > 0
? 'bg-amber-400 text-black'
? 'bg-amber-400 text-black shadow-[0_0_12px_rgba(245,158,11,0.4)]'
: 'bg-[var(--bg-secondary)] text-[var(--text-secondary)] border border-[var(--border-color)]'
}`}>
{totalCount}
</span>
)}
{inProgressCount > 0 && (
<span className="absolute inset-0 rounded-full border-2 border-amber-400/30 animate-ping" />
<span className="absolute -inset-[3px] rounded-full animate-todo-ring-pulse" />
)}
</button>
</div>
@ -186,29 +187,44 @@ export function TodoPanel({ todos, requestTodoList, sendCommand }: TodoPanelProp
className="absolute z-30"
style={{ top: `${16 + pos.y}px`, right: `${16 + pos.x}px` }}
>
<div className="w-72 max-h-[50vh] flex flex-col rounded-xl bg-[var(--bg-tertiary)]/95 backdrop-blur-md border border-[var(--border-color)] shadow-2xl overflow-hidden">
<div className="w-80 max-h-[55vh] flex flex-col rounded-2xl bg-[var(--bg-tertiary)]/95 backdrop-blur-md border border-[var(--border-color)] shadow-2xl hover:shadow-[0_8px_40px_var(--shadow-glow-sm)] overflow-hidden animate-todo-card-in">
{/* title bar (drag handle) */}
<div
className="shrink-0 flex items-center gap-2 px-3 py-2 border-b border-[var(--border-color)]/50 cursor-grab active:cursor-grabbing select-none"
className="shrink-0 flex items-center gap-2 px-4 py-2.5 border-b border-[var(--border-color)]/50 cursor-grab active:cursor-grabbing select-none rounded-t-2xl"
onMouseDown={handleDragStart}
>
<span className="text-[var(--text-muted)]/30 text-[10px] leading-none select-none"></span>
<ClipboardList className="h-3 w-3 text-[var(--accent-cyan)]" />
<span className="text-[11px] font-semibold text-[var(--text-secondary)]"></span>
<span className="text-[10px] text-[var(--text-muted)]/70 tabular-nums">{totalCount}</span>
<span className="text-[var(--text-muted)]/40 text-sm tracking-[0.15em] leading-none select-none group-hover:text-[var(--accent-cyan)]/60 transition-colors"></span>
<ClipboardList className="h-4 w-4 text-[var(--accent-cyan)]/80" />
<span className="text-[13px] font-semibold text-[var(--text-primary)] tracking-tight"></span>
<span className="text-[11px] text-[var(--text-muted)]/80 tabular-nums">{totalCount}</span>
{inProgressCount > 0 && <PulseDot />}
<div className="ml-auto flex items-center gap-0.5">
<button onClick={handleRefresh} className="p-1 rounded text-[var(--text-muted)]/50 hover:text-[var(--accent-cyan)] transition-colors" title="刷新">
<RefreshCw className="h-3 w-3" />
<button onClick={handleRefresh} className="p-1.5 rounded-lg text-[var(--text-muted)]/50 hover:text-[var(--accent-cyan)] hover:bg-[var(--overlay-hover)] transition-colors" title="刷新">
<RefreshCw className="h-3.5 w-3.5" />
</button>
<button onClick={() => setExpanded(false)} className="p-1 rounded text-[var(--text-muted)]/50 hover:text-[var(--text-secondary)] transition-colors" title="缩小">
<ChevronDown className="h-3 w-3" />
<button onClick={() => setExpanded(false)} className="p-1.5 rounded-lg text-[var(--text-muted)]/50 hover:text-[var(--text-secondary)] hover:bg-[var(--overlay-hover)] transition-colors" title="缩小">
<ChevronDown className="h-4 w-4" />
</button>
</div>
</div>
{/* list */}
<div className="flex-1 overflow-y-auto scrollbar-hide px-3 py-2">
<div className="flex-1 overflow-y-auto scrollbar-hide px-4 py-3 space-y-2">
{totalCount === 0 && (
<div className="flex flex-col items-center justify-center py-8 px-4 text-center select-none">
<div className="relative mb-4">
<div className="absolute inset-0 rounded-full bg-[var(--accent-cyan)]/10 blur-xl animate-pulse" />
<ClipboardList className="relative h-10 w-10 text-[var(--accent-cyan)]/25" />
</div>
<p className="text-[12px] text-[var(--text-muted)] leading-relaxed mb-1">
</p>
<p className="text-[10px] text-[var(--text-muted)]/60 leading-relaxed">
AI
</p>
</div>
)}
{GROUP_ORDER.map(status => {
const items = grouped.get(status)
if (!items || items.length === 0) return null
@ -220,31 +236,28 @@ export function TodoPanel({ todos, requestTodoList, sendCommand }: TodoPanelProp
<div key={status} className="mt-1 first:mt-0">
<button
onClick={() => toggleGroup(status)}
className="flex items-center gap-1.5 w-full py-0.5 group"
className="sticky top-0 z-10 flex items-center gap-2 w-full py-1.5 rounded-lg transition-colors hover:bg-[var(--overlay-hover)] bg-[var(--bg-tertiary)]/95 backdrop-blur-sm"
>
<span className={`h-1.5 w-1.5 rounded-full ${cfg.dot} shrink-0`} />
<span className={`text-[10px] font-medium ${cfg.color}`}>{cfg.label}</span>
<span className={`text-[10px] ${cfg.color} opacity-60 tabular-nums`}>{items.length}</span>
<span className="ml-auto text-[var(--text-muted)]/40">
{isCollapsed
? <ChevronDown className="h-2.5 w-2.5 rotate-[-90deg]" />
: <ChevronDown className="h-2.5 w-2.5" />
}
<span className={`h-2 w-2 rounded-full ${cfg.dot} shrink-0`} />
<span className={`text-[12px] font-semibold ${cfg.color}`}>{cfg.label}</span>
<span className={`text-[10px] ${cfg.color} opacity-50 tabular-nums ml-0.5`}>{items.length}</span>
<span className="ml-auto text-[var(--text-muted)]/50">
<ChevronDown className={`h-3.5 w-3.5 transition-transform duration-200 ${isCollapsed ? '-rotate-90' : 'rotate-0'}`} />
</span>
</button>
{!isCollapsed && (
<div className="ml-3 border-l border-[var(--border-color)]/30 pl-2 mt-0.5 space-y-px">
<div className={`todo-group-body ${isCollapsed ? 'todo-group-body-closed' : 'todo-group-body-open'}`}>
<div className="ml-[7px] border-l-2 border-[var(--border-color)]/60 pl-3 mt-1.5 space-y-0.5">
{items.map(item => (
<div key={item.id} className="py-[3px] flex items-start gap-1.5">
<span className={`h-1.5 w-1.5 rounded-full ${cfg.dot} shrink-0 mt-[5px]`} />
<span className="text-[12px] leading-snug text-[var(--text-primary)]/90 break-words">
<div key={item.id} className="group/item py-1.5 px-2 -mx-2 rounded-md transition-colors duration-150 hover:bg-[var(--overlay-hover)] flex items-start gap-1.5">
<span className={`h-2 w-2 rounded-full ${cfg.dot} shrink-0 mt-1.5`} />
<span className="text-[13px] leading-relaxed text-[var(--text-primary)]/85 group-hover/item:text-[var(--text-primary)] transition-colors break-words">
{item.content}
</span>
</div>
))}
</div>
)}
</div>
</div>
)
})}

View File

@ -258,6 +258,60 @@ body {
.animate-fade-in { animation: fade-in 0.2s ease-out; }
.animate-scale-in { animation: scale-in 0.3s ease-out; }
/* ============================================
TodoPanel animations
============================================ */
@keyframes todo-card-in {
from {
opacity: 0;
transform: scale(0.92) translateY(-8px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
@keyframes todo-item-in {
from {
opacity: 0;
transform: translateX(-6px);
max-height: 0;
}
to {
opacity: 1;
transform: translateX(0);
max-height: 40px;
}
}
@keyframes todo-ring-pulse {
0%, 100% {
box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.4);
}
50% {
box-shadow: 0 0 0 6px rgba(245, 158, 11, 0);
}
}
.animate-todo-card-in { animation: todo-card-in 0.25s cubic-bezier(0.34, 1.56, 0.64, 1); }
.animate-todo-item-in { animation: todo-item-in 0.2s ease-out forwards; }
.animate-todo-ring-pulse { animation: todo-ring-pulse 2s ease-in-out infinite; }
/* 分组折叠内容展开/收起 */
.todo-group-body {
overflow: hidden;
transition: max-height 0.25s ease, opacity 0.2s ease;
}
.todo-group-body-open {
max-height: 600px;
opacity: 1;
}
.todo-group-body-closed {
max-height: 0;
opacity: 0;
}
@keyframes thinking-reveal {
from { max-height: 0; opacity: 0; }
to { max-height: 300px; opacity: 1; }