perf: optimize app startup and background lifecycles

This commit is contained in:
inman
2026-08-19 00:56:15 +08:00
parent 31332f6d76
commit 927e13349b
121 changed files with 4359 additions and 1400 deletions

View File

@@ -42,8 +42,8 @@ type ProjectDialogState =
const VISIBLE_CONVERSATION_LIMIT = 5;
const CONVERSATION_PREVIEW_LIMIT = 28;
const PROJECT_ACTION_BUTTON_CLASS = 'flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-muted-foreground pointer-events-none opacity-0 transition-[opacity,background-color,color,transform] group-hover/project:pointer-events-auto group-hover/project:opacity-100 focus-visible:pointer-events-auto focus-visible:opacity-100';
const IMAGE_SIDEBAR_ACTION_CLASS = 'motion-press flex min-h-9 w-full items-center justify-start gap-2 rounded-lg border-0 bg-transparent px-2 py-1.5 text-left text-sm font-medium text-foreground shadow-none transition-[background-color,color,transform] hover:bg-surface-subtle hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/35 focus-visible:ring-offset-1 active:scale-[0.985]';
const PROJECT_ACTION_BUTTON_CLASS = 'relative z-10 flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-muted-foreground pointer-events-none opacity-0 transition-colors duration-100 group-hover/project:pointer-events-auto group-hover/project:opacity-100 focus-visible:pointer-events-auto focus-visible:opacity-100';
const IMAGE_SIDEBAR_ACTION_CLASS = 'motion-press flex min-h-9 w-full items-center justify-start gap-2 rounded-lg border-0 bg-transparent px-2 py-1.5 text-left text-sm font-medium text-foreground shadow-none transition-colors duration-100 hover:bg-surface-subtle hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/35 focus-visible:ring-offset-1';
function conversationTimestamp(conversation: DesignConversationSummary): number {
const timestamp = Date.parse(conversation.updatedAt);
@@ -364,7 +364,7 @@ export function ImageWorkspaceSidebar({ sidebarCollapsed }: ImageWorkspaceSideba
aria-current={selected ? 'page' : undefined}
title={`${conversation.title}${fullPreview}`}
className={cn(
'group/image-conversation flex w-full min-w-0 items-center gap-1.5 rounded-lg px-1.5 py-1.5 text-left transition-[background-color,color,transform] active:scale-[0.985]',
'group/image-conversation flex w-full min-w-0 items-center gap-1.5 rounded-lg px-1.5 py-1.5 text-left transition-colors duration-100',
selected
? 'bg-background text-foreground shadow-soft'
: 'text-muted-foreground hover:bg-background/70 hover:text-foreground',
@@ -386,7 +386,7 @@ export function ImageWorkspaceSidebar({ sidebarCollapsed }: ImageWorkspaceSideba
key={project.workspaceId}
data-testid={`sidebar-image-project-${project.workspaceId}`}
className={cn(
'motion-press group/project relative overflow-hidden rounded-lg border px-2 py-1.5 text-left transition-[background-color,border-color,color]',
'motion-press group/project relative overflow-hidden rounded-lg border px-2 py-1.5 text-left transition-colors duration-100',
active
? 'border-brand/35 bg-brand-soft shadow-soft before:absolute before:inset-y-2 before:left-0 before:w-0.5 before:rounded-full before:bg-brand'
: 'border-transparent bg-transparent hover:border-border/70 hover:bg-surface-subtle',
@@ -399,7 +399,7 @@ export function ImageWorkspaceSidebar({ sidebarCollapsed }: ImageWorkspaceSideba
<button
type="button"
aria-current={active ? 'page' : undefined}
className="flex min-w-0 flex-1 items-center gap-2 text-left"
className="flex min-w-0 flex-1 items-center gap-2 pr-28 text-left"
onClick={() => handleSelectProject(project.workspaceId)}
>
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-surface-input text-foreground">
@@ -407,24 +407,24 @@ export function ImageWorkspaceSidebar({ sidebarCollapsed }: ImageWorkspaceSideba
</span>
<span className="min-w-0 flex-1 truncate text-sm font-semibold">{project.title}</span>
</button>
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center gap-1">
<div className="pointer-events-none absolute inset-y-0 right-0 z-10 flex items-center gap-1">
<button
type="button"
aria-label={projectExpanded ? `收起 ${project.title} 的会话` : `展开 ${project.title} 的会话`}
title={projectExpanded ? `收起 ${project.title} 的会话` : `展开 ${project.title} 的会话`}
aria-expanded={active ? projectExpanded : undefined}
className={cn(PROJECT_ACTION_BUTTON_CLASS, 'hover:bg-background hover:text-foreground active:scale-[0.96]')}
className={cn(PROJECT_ACTION_BUTTON_CLASS, 'hover:bg-background hover:text-foreground')}
onClick={() => handleToggleProject(project.workspaceId)}
>
<ChevronDown
data-testid={`sidebar-image-project-toggle-icon-${project.workspaceId}`}
className={cn('h-3.5 w-3.5 transition-transform duration-200 ease-out', projectExpanded && 'rotate-180')}
className={cn('h-3.5 w-3.5', projectExpanded && 'rotate-180')}
/>
</button>
<button
type="button"
aria-label={`重命名 ${project.title}`}
className={cn(PROJECT_ACTION_BUTTON_CLASS, 'hover:bg-background hover:text-foreground active:scale-[0.96]')}
className={cn(PROJECT_ACTION_BUTTON_CLASS, 'hover:bg-background hover:text-foreground')}
onClick={() => openRenameDialog(project)}
>
<Pencil className="h-3.5 w-3.5" />
@@ -434,7 +434,7 @@ export function ImageWorkspaceSidebar({ sidebarCollapsed }: ImageWorkspaceSideba
aria-label="新建设计会话"
title="新建设计会话"
aria-busy={creatingConversation}
className={cn(PROJECT_ACTION_BUTTON_CLASS, 'hover:bg-background hover:text-brand active:scale-[0.96]')}
className={cn(PROJECT_ACTION_BUTTON_CLASS, 'hover:bg-background hover:text-brand')}
disabled={creatingConversation || !workspace || workspace.workspaceId !== project.workspaceId}
onClick={() => void handleCreateConversation()}
>
@@ -447,7 +447,7 @@ export function ImageWorkspaceSidebar({ sidebarCollapsed }: ImageWorkspaceSideba
data-testid={`sidebar-delete-image-project-${project.workspaceId}`}
aria-label={`删除 ${project.title}`}
title={`删除 ${project.title}`}
className={cn(PROJECT_ACTION_BUTTON_CLASS, 'hover:bg-destructive/10 hover:text-destructive active:scale-[0.96]')}
className={cn(PROJECT_ACTION_BUTTON_CLASS, 'hover:bg-destructive/10 hover:text-destructive')}
onClick={() => openDeleteDialog(project)}
>
<Trash2 className="h-3.5 w-3.5" />
@@ -489,7 +489,7 @@ export function ImageWorkspaceSidebar({ sidebarCollapsed }: ImageWorkspaceSideba
}))}
>
<span>{hiddenConversations.length}</span>
<ChevronRight className={cn('h-3.5 w-3.5 transition-transform duration-200 ease-out', moreConversationsOpen && 'rotate-90')} />
<ChevronRight className={cn('h-3.5 w-3.5', moreConversationsOpen && 'rotate-90')} />
</button>
<DisclosureContent
open={moreConversationsOpen}

View File

@@ -82,7 +82,7 @@ export function LearningSidebar({ sidebarCollapsed }: LearningSidebarProps) {
aria-current={catalogActive ? 'page' : undefined}
onClick={() => navigate('/learning')}
className={cn(
'motion-press flex min-h-9 w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-sm font-semibold transition-colors active:scale-[0.985]',
'motion-press flex min-h-9 w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-sm font-semibold transition-colors duration-100',
catalogActive ? 'bg-brand-soft text-foreground' : 'text-foreground hover:bg-surface-subtle',
sidebarCollapsed && 'justify-center px-0',
)}
@@ -96,7 +96,7 @@ export function LearningSidebar({ sidebarCollapsed }: LearningSidebarProps) {
aria-label="生成课程"
onClick={() => navigate('/learning?generate=1')}
className={cn(
'motion-press mt-1 flex min-h-9 w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-sm font-medium text-foreground transition-colors hover:bg-surface-subtle active:scale-[0.985]',
'motion-press mt-1 flex min-h-9 w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-sm font-medium text-foreground transition-colors duration-100 hover:bg-surface-subtle',
sidebarCollapsed && 'justify-center px-0',
)}
>
@@ -146,7 +146,7 @@ export function LearningSidebar({ sidebarCollapsed }: LearningSidebarProps) {
disabled={downloading === course.id}
onClick={() => void openCourse(course)}
className={cn(
'motion-press group/course flex w-full items-center gap-2 rounded-xl px-2.5 py-2 text-left transition-[background-color,color,transform] active:scale-[0.985]',
'motion-press group/course flex w-full items-center gap-2 rounded-xl px-2.5 py-2 text-left transition-colors duration-100',
isActive ? 'bg-brand-selected text-foreground ring-1 ring-brand/20' : 'hover:bg-surface-tertiary',
)}
>

View File

@@ -21,7 +21,7 @@ export function ModuleSwitcher({ sidebarCollapsed, compact = false }: { sidebarC
title="返回首页"
onClick={() => navigate(AI_MODULE_SELECTION_PATH)}
className={cn(
'motion-press items-center bg-transparent text-left font-semibold text-foreground transition-[background-color,color,transform] hover:bg-surface-subtle active:scale-[0.985]',
'motion-press items-center bg-transparent text-left font-semibold text-foreground transition-colors duration-100 hover:bg-surface-subtle',
compactTrigger
? 'inline-flex min-h-9 w-fit translate-y-2 justify-start rounded-xl p-2 text-xs'
: 'flex min-h-11 w-full justify-between gap-2 rounded-2xl p-3 text-sm',

View File

@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
import { useLocation, useNavigate } from 'react-router-dom';
import {
BarChart3,
Bot,
Cpu,
ChevronRight,
Crown,
@@ -19,6 +20,13 @@ import { ConfirmDialog } from '@/components/ui/confirm-dialog';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { DisclosureContent } from '@/components/ui/disclosure';
import { getAgentAvatarSrc } from '@/lib/agent-avatars';
import { getAiHardwareOverview, type AiHardwareOverview } from '@/lib/ai-hardware';
import {
AI_HARDWARE_NAVIGATION_EVENT,
AI_HARDWARE_OVERVIEW_EVENT,
announceAiHardwareNavigation,
readAiHardwareAgentId,
} from '@/lib/ai-hardware-navigation';
import { getAiModuleForPath } from '@/lib/ai-modules';
import { invokeIpc } from '@/lib/api-client';
import { getAuthUserDisplayName } from '@/lib/auth-user-display';
@@ -159,6 +167,9 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
const [creatingProject, setCreatingProject] = useState(false);
const [projectEntryError, setProjectEntryError] = useState<ProjectEntryError | null>(null);
const [removeProjectCandidate, setRemoveProjectCandidate] = useState<OpencodeProject | null>(null);
const [robotOverview, setRobotOverview] = useState<AiHardwareOverview | null>(null);
const [robotOverviewLoading, setRobotOverviewLoading] = useState(false);
const [robotSelectedAgentId, setRobotSelectedAgentId] = useState<string | null>(() => readAiHardwareAgentId());
const accountButtonRef = useRef<HTMLButtonElement | null>(null);
const accountMenuRef = useRef<HTMLDivElement | null>(null);
const tokenUsageRequestIdRef = useRef(0);
@@ -168,6 +179,7 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
const isProgrammingModule = activeModule === 'programming';
const isPaintingModule = activeModule === 'painting';
const isLearningModule = activeModule === 'learning';
const isRobotModule = activeModule === 'robot';
const isPromptMuseum = location.pathname === '/image-prompts' || location.pathname.startsWith('/image-prompts/');
const projectConfigPath = '/project-config';
const visibleProjects = opencodeProjects;
@@ -182,6 +194,16 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
const tokenUsageExhausted = isTokenUsageExhausted(tokenUsageState);
const fiveHourRefreshLabel = getRefreshTimeLabel(tokenUsageState.usage?.five_hour_refresh_at);
const weeklyRefreshLabel = getRefreshTimeLabel(tokenUsageState.usage?.weekly_refresh_at);
const activeRobotAgentId = isRobotModule ? robotSelectedAgentId : null;
const openRobotRoute = useCallback((agentId: string | null = null, openCreate = false) => {
const params = new URLSearchParams();
if (agentId) params.set('agent', agentId);
if (openCreate) params.set('create', '1');
const search = params.toString();
navigate({ pathname: '/ai-hardware', search: search ? `?${search}` : '' });
announceAiHardwareNavigation({ agentId, openCreate });
}, [navigate]);
useEffect(() => {
if (!isProgrammingModule) return;
@@ -200,6 +222,54 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
void refreshProviderSnapshot().catch(() => undefined);
}, [isProgrammingModule, refreshProviderSnapshot]);
useEffect(() => {
if (!isRobotModule) {
setRobotOverview(null);
setRobotOverviewLoading(false);
return undefined;
}
let active = true;
const handleOverview = (event: Event) => {
const next = (event as CustomEvent<AiHardwareOverview>).detail;
if (!next || !Array.isArray(next.agents) || !Array.isArray(next.devices)) return;
setRobotOverview(next);
setRobotOverviewLoading(false);
};
window.addEventListener(AI_HARDWARE_OVERVIEW_EVENT, handleOverview);
setRobotOverviewLoading(true);
void getAiHardwareOverview()
.then((next) => {
if (!active) return;
setRobotOverview(next);
setRobotOverviewLoading(false);
})
.catch(() => {
if (!active) return;
setRobotOverviewLoading(false);
});
return () => {
active = false;
window.removeEventListener(AI_HARDWARE_OVERVIEW_EVENT, handleOverview);
};
}, [isRobotModule]);
useEffect(() => {
if (!isRobotModule) {
setRobotSelectedAgentId(null);
return undefined;
}
setRobotSelectedAgentId(readAiHardwareAgentId(location.search));
const handleNavigation = (event: Event) => {
const detail = (event as CustomEvent<{ agentId?: string | null; openCreate?: boolean }>).detail;
if (!detail?.openCreate && detail?.agentId !== undefined) setRobotSelectedAgentId(detail.agentId);
};
window.addEventListener(AI_HARDWARE_NAVIGATION_EVENT, handleNavigation);
return () => window.removeEventListener(AI_HARDWARE_NAVIGATION_EVENT, handleNavigation);
}, [isRobotModule, location.search]);
useEffect(() => {
if (profileRequired || profileSyncError) setProfileDialogOpen(true);
}, [profileRequired, profileSyncError]);
@@ -455,7 +525,7 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
onPointerEnter={() => onSidebarPeekChange?.(true, 'sidebar')}
onPointerLeave={() => onSidebarPeekChange?.(false, 'sidebar')}
className={cn(
'sidebar-glass-surface flex min-h-0 shrink-0 flex-col overflow-hidden border-r border-border/80 text-foreground transition-[width,opacity,box-shadow] duration-300 ease-spring motion-reduce:transition-none',
'sidebar-glass-surface flex min-h-0 shrink-0 flex-col overflow-hidden border-r border-border/80 text-foreground',
sidebarCollapsed
? 'pointer-events-none w-0 border-r-0 opacity-0'
: workspaceLayout
@@ -489,7 +559,7 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
type="button"
data-testid="sidebar-create-project"
className={cn(
'motion-press mb-1 flex min-h-9 w-full items-center gap-2 rounded-lg border-0 bg-transparent px-2 py-1.5 text-left text-sm font-medium text-foreground shadow-none transition-colors hover:bg-surface-subtle active:scale-[0.985]',
'motion-press mb-1 flex min-h-9 w-full items-center gap-2 rounded-lg border-0 bg-transparent px-2 py-1.5 text-left text-sm font-medium text-foreground shadow-none transition-colors duration-100 hover:bg-surface-subtle',
sidebarCollapsed ? 'justify-center px-0' : '',
)}
aria-label="新建项目"
@@ -536,7 +606,7 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
aria-current={active ? 'page' : undefined}
onClick={() => void openProject(project)}
className={cn(
'motion-press group/project flex w-full items-center gap-2 rounded-xl px-3 py-2 text-left transition-[background-color,color,box-shadow,transform] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/35 focus-visible:ring-offset-1 active:scale-[0.985]',
'motion-press group/project flex w-full items-center gap-2 rounded-xl px-3 py-2 text-left transition-colors duration-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/35 focus-visible:ring-offset-1',
active
? 'bg-brand-selected text-foreground shadow-soft ring-1 ring-brand/25'
: 'bg-transparent text-foreground hover:bg-surface-tertiary',
@@ -574,8 +644,8 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
<button
type="button"
aria-label="机器人工作台"
aria-current="page"
onClick={() => navigate('/ai-hardware')}
aria-current={location.pathname === '/ai-hardware' ? 'page' : undefined}
onClick={() => openRobotRoute()}
className={cn(
'flex min-h-10 w-full items-center gap-2 rounded-lg bg-brand-soft px-2 py-2 text-left text-sm font-semibold text-foreground',
sidebarCollapsed && 'justify-center px-0',
@@ -585,9 +655,55 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
{!sidebarCollapsed ? <span></span> : null}
</button>
{!sidebarCollapsed ? (
<p className="mt-3 px-2 text-xs font-medium leading-5 text-muted-foreground">
</p>
<div className="mt-4 border-t border-border/60 pt-4">
<div className="flex items-center justify-between gap-2 px-2">
<span className="text-xs font-semibold tracking-wide text-muted-foreground"></span>
<button
type="button"
data-testid="sidebar-robot-create-agent"
aria-label="创建智能体"
title="创建智能体"
onClick={() => openRobotRoute(null, true)}
className="motion-press flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition-colors duration-100 hover:bg-surface-subtle hover:text-brand focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/35"
>
<Plus className="h-4 w-4" />
</button>
</div>
<div data-testid="sidebar-robot-agents" className="mt-2 space-y-1">
{robotOverviewLoading ? (
<p role="status" className="px-2 py-2 text-xs text-muted-foreground"></p>
) : robotOverview ? (
robotOverview.agents.length > 0 ? robotOverview.agents.map((agent) => {
const active = activeRobotAgentId === agent.id;
const deviceCount = robotOverview.devices.filter((device) => device.agent_id === agent.id).length;
return (
<button
key={agent.id}
type="button"
data-testid={`sidebar-robot-agent-${agent.id}`}
aria-label={`进入智能体 ${agent.name}`}
aria-current={active ? 'page' : undefined}
onClick={() => openRobotRoute(agent.id)}
className={cn(
'motion-press flex min-h-10 w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left transition-colors duration-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/35',
active ? 'bg-brand-selected text-foreground' : 'text-foreground hover:bg-surface-tertiary',
)}
>
<Bot className={cn('h-4 w-4 shrink-0', active ? 'text-brand' : 'text-muted-foreground')} />
<span className="min-w-0 flex-1">
<span className="block truncate text-sm font-medium">{agent.name}</span>
<span className="block text-[11px] tabular-nums text-muted-foreground">{deviceCount} </span>
</span>
</button>
);
}) : (
<p className="px-2 py-2 text-xs leading-5 text-muted-foreground"> + </p>
)
) : (
<p className="px-2 py-2 text-xs leading-5 text-muted-foreground"></p>
)}
</div>
</div>
) : null}
</div>
)}
@@ -639,7 +755,7 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
<label
key={value}
className={cn(
'min-h-28 cursor-pointer rounded-xl bg-white p-3 shadow-soft transition-[box-shadow,transform] active:scale-[0.96]',
'min-h-28 cursor-pointer rounded-xl bg-white p-3 shadow-soft',
newProjectType === value
? 'ring-2 ring-brand/35 shadow-float'
: 'ring-1 ring-foreground/10 hover:shadow-float',
@@ -796,7 +912,7 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
? createPortal(
<div
data-testid="project-config-error-overlay"
className="fixed inset-0 z-[1000] flex min-h-screen items-center justify-center bg-foreground/15 p-4 backdrop-blur-md sm:p-6"
className="fixed inset-0 z-[1000] flex min-h-screen items-center justify-center bg-foreground/15 p-4 sm:p-6"
>
<div
role="alertdialog"
@@ -886,7 +1002,7 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
<span className="min-w-0 flex-1">
<span className="block truncate">{accountName}</span>
</span>
<ChevronRight className={cn('h-3.5 w-3.5 shrink-0 transition-transform duration-200 ease-out', accountMenuOpen && 'rotate-90')} />
<ChevronRight className={cn('h-3.5 w-3.5 shrink-0', accountMenuOpen && 'rotate-90')} />
</>
)}
</button>
@@ -928,12 +1044,12 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
data-testid="sidebar-account-usage-menuitem"
aria-expanded={usageDrawerOpen}
aria-controls="sidebar-account-usage-drawer"
className="flex min-h-8 w-full items-center gap-2 rounded-md px-1 py-1 text-left text-sm font-medium transition-colors hover:bg-surface-subtle active:scale-[0.985]"
className="flex min-h-8 w-full items-center gap-2 rounded-md px-1 py-1 text-left text-sm font-medium transition-colors duration-100 hover:bg-surface-subtle"
onClick={() => setUsageDrawerOpen((open) => !open)}
>
<BarChart3 className="h-3.5 w-3.5 shrink-0" />
<span className="min-w-0 flex-1 truncate"></span>
<ChevronRight className={cn('h-3.5 w-3.5 shrink-0 text-muted-foreground transition-transform duration-200 ease-out', usageDrawerOpen && 'rotate-90')} />
<ChevronRight className={cn('h-3.5 w-3.5 shrink-0 text-muted-foreground', usageDrawerOpen && 'rotate-90')} />
</button>
<DisclosureContent
open={usageDrawerOpen}
@@ -974,7 +1090,7 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
<button
type="button"
data-testid="sidebar-account-upgrade-button"
className="mt-1.5 flex min-h-8 w-full items-center justify-center gap-1.5 rounded-md border border-foreground/15 bg-accent-soft px-2 py-1.5 text-xs font-semibold text-foreground shadow-soft transition-transform hover:bg-background active:scale-[0.985]"
className="mt-1.5 flex min-h-8 w-full items-center justify-center gap-1.5 rounded-md border border-foreground/15 bg-accent-soft px-2 py-1.5 text-xs font-semibold text-foreground shadow-soft transition-colors duration-100 hover:bg-background"
onClick={openSubscriptionUpgrade}
>
<Crown className="h-3.5 w-3.5" />
@@ -987,7 +1103,7 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
<button
type="button"
role="menuitem"
className="flex min-h-8 w-full items-center gap-2 rounded-md px-1 py-1 text-left text-sm font-medium transition-colors hover:bg-surface-subtle active:scale-[0.985]"
className="flex min-h-8 w-full items-center gap-2 rounded-md px-1 py-1 text-left text-sm font-medium transition-colors duration-100 hover:bg-surface-subtle"
onClick={openProfileDialog}
>
<UserCircle2 className="h-3.5 w-3.5 shrink-0" />
@@ -997,7 +1113,7 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
type="button"
role="menuitem"
data-testid="sidebar-nav-settings"
className="flex min-h-8 w-full items-center gap-2 rounded-md px-1 py-1 text-left text-sm font-medium transition-colors hover:bg-surface-subtle active:scale-[0.985]"
className="flex min-h-8 w-full items-center gap-2 rounded-md px-1 py-1 text-left text-sm font-medium transition-colors duration-100 hover:bg-surface-subtle"
onClick={() => navigateFromAccountMenu('/settings')}
>
<SettingsIcon className="h-3.5 w-3.5 shrink-0" />
@@ -1009,7 +1125,7 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
<button
type="button"
role="menuitem"
className="flex min-h-8 w-full items-center gap-2 rounded-md px-1 py-1 text-left text-sm font-medium text-accent-strong transition-colors hover:bg-accent-soft hover:text-foreground active:scale-[0.985]"
className="flex min-h-8 w-full items-center gap-2 rounded-md px-1 py-1 text-left text-sm font-medium text-accent-strong transition-colors duration-100 hover:bg-accent-soft hover:text-foreground"
onClick={() => void handleAccountLogout()}
>
<LogOut className="h-3.5 w-3.5 shrink-0" />

View File

@@ -115,12 +115,11 @@ function ProductTitleBar({
<div
data-testid="titlebar-logo"
className={cn(
'pointer-events-none absolute top-1/2 z-10 h-6 w-24 -translate-y-1/2 overflow-hidden transition-[left,right] ease-out motion-reduce:transition-none',
'pointer-events-none absolute top-1/2 z-10 h-6 w-24 -translate-y-1/2 overflow-hidden',
overlay ? 'bg-transparent' : 'bg-background',
)}
style={{
right: overlay ? '0px' : 'var(--agent-browser-titlebar-logo-gap, 48px)',
transitionDuration: 'var(--agent-browser-panel-transition-duration, 180ms)',
}}
>
<img
@@ -151,7 +150,7 @@ function ProductTitleBar({
<div
data-testid="titlebar-sidebar-surface"
className={cn(
'flex h-full shrink-0 items-center gap-1 px-2 transition-[width] duration-300 ease-spring motion-reduce:transition-none',
'flex h-full shrink-0 items-center gap-1 px-2',
sidebarTitlebarHidden
? nativeTrafficLights
? 'w-[132px]'
@@ -179,20 +178,19 @@ function ProductTitleBar({
<div
data-testid="titlebar-main-surface"
className={cn(
'relative flex min-w-0 flex-1 bg-background transition-[margin-right] ease-out motion-reduce:transition-none',
'relative flex min-w-0 flex-1 bg-background',
overlay && 'pointer-events-none border-b-0 bg-transparent',
nativeTrafficLights && 'drag-region',
!overlay && (workspaceLayout || sidebarTitlebarHidden ? 'border-b-0' : 'border-b border-border/80'),
)}
style={{
marginRight: 'var(--agent-browser-panel-width, 0px)',
transitionDuration: 'var(--agent-browser-panel-transition-duration, 180ms)',
}}
>
{workspaceLayout ? (
<div
data-testid="titlebar-conversation-surface"
className="pointer-events-none absolute inset-y-0 z-0 w-[256px] border-r border-border/80 bg-surface-tertiary transition-[left] duration-300 ease-spring motion-reduce:transition-none"
className="pointer-events-none absolute inset-y-0 z-0 w-[256px] border-r border-border/80 bg-surface-tertiary"
style={{ left: conversationSurfaceOffset }}
/>
) : null}

View File

@@ -363,7 +363,7 @@ export function AgentConversationSidebar({
<div className="pt-1">
<button type="button" className="flex w-full items-center justify-between px-2.5 py-1 text-left text-[10px] font-semibold text-muted-foreground" aria-expanded={moreSessionsOpen} onClick={() => setExpandedSessionLists((current) => ({ ...current, [agent.id]: !moreSessionsOpen }))}>
<span>{hiddenSessions.length}</span>
<ChevronRight className={cn('h-3.5 w-3.5 transition-transform duration-200 ease-out', moreSessionsOpen && 'rotate-90')} />
<ChevronRight className={cn('h-3.5 w-3.5', moreSessionsOpen && 'rotate-90')} />
</button>
<DisclosureContent open={moreSessionsOpen} className="mt-0.5" innerClassName="space-y-0.5">
{hiddenSessions.map(renderSession)}
@@ -374,7 +374,7 @@ export function AgentConversationSidebar({
<div className="pt-2">
<button type="button" className="flex w-full items-center justify-between px-2.5 py-1 text-left text-[10px] font-semibold text-muted-foreground" aria-expanded={archivedSessionsOpen} onClick={() => setExpandedArchivedSessionLists((current) => ({ ...current, [agent.id]: !archivedSessionsOpen }))}>
<span>{archivedSessions.length}</span>
<ChevronRight className={cn('h-3.5 w-3.5 transition-transform duration-200 ease-out', archivedSessionsOpen && 'rotate-90')} />
<ChevronRight className={cn('h-3.5 w-3.5', archivedSessionsOpen && 'rotate-90')} />
</button>
<DisclosureContent open={archivedSessionsOpen} className="mt-1" innerClassName="space-y-1">
{archivedSessions.map((metadata) => {
@@ -412,11 +412,11 @@ export function AgentConversationSidebar({
>
<div className="no-drag pointer-events-auto flex shrink-0 items-center gap-1">
{onOpenProjectSettings ? (
<Button type="button" size="icon" variant="ghost" className="no-drag h-8 w-8 rounded-full text-muted-foreground transition-[background-color,color,transform] duration-300 ease-spring hover:bg-surface-subtle hover:text-foreground active:scale-[0.96]" aria-label="项目设置" title="项目设置" onClick={onOpenProjectSettings}>
<Button type="button" size="icon" variant="ghost" className="no-drag h-8 w-8 rounded-full text-muted-foreground transition-colors duration-100 hover:bg-surface-subtle hover:text-foreground" aria-label="项目设置" title="项目设置" onClick={onOpenProjectSettings}>
<SettingsIcon className="h-4 w-4" />
</Button>
) : null}
<Button type="button" size="icon" className={cn('group no-drag h-8 w-8 rounded-full border border-brand/20 transition-[background-color,color,transform,box-shadow] duration-300 ease-spring', createOpen ? 'bg-brand text-white hover:bg-brand-hover' : 'bg-brand-soft text-brand hover:bg-brand-hover hover:text-white')} aria-label="创建伙伴" aria-expanded={createOpen} onClick={() => { if (modelOptions.length === 0) { toast.error('请先配置至少一个可用模型。'); return; } setCreateOpen(true); }}>
<Button type="button" size="icon" className={cn('group no-drag h-8 w-8 rounded-full border border-brand/20 transition-colors duration-100', createOpen ? 'bg-brand text-white hover:bg-brand-hover' : 'bg-brand-soft text-brand hover:bg-brand-hover hover:text-white')} aria-label="创建伙伴" aria-expanded={createOpen} onClick={() => { if (modelOptions.length === 0) { toast.error('请先配置至少一个可用模型。'); return; } setCreateOpen(true); }}>
<Plus className={cn('h-4 w-4', createOpen ? 'text-white' : 'text-brand group-hover:text-white')} />
</Button>
</div>
@@ -463,7 +463,7 @@ export function AgentConversationSidebar({
)}
>
<div className="relative">
<button type="button" className="flex w-full min-w-0 items-center gap-2.5 px-2.5 py-2.5 pr-12 text-left transition-transform duration-150 ease-out active:scale-[0.985]" aria-pressed={selected} aria-expanded={selected} aria-busy={startingAgentId === agent.id} title={selected ? '收起最近会话' : '展开最近会话'} data-testid={`project-agent-chat-${agent.id}`} disabled={startingAgentId === agent.id} onClick={() => handleAgentSelection(agent.id)}>
<button type="button" className="flex w-full min-w-0 items-center gap-2.5 px-2.5 py-2.5 pr-12 text-left" aria-pressed={selected} aria-expanded={selected} aria-busy={startingAgentId === agent.id} title={selected ? '收起最近会话' : '展开最近会话'} data-testid={`project-agent-chat-${agent.id}`} disabled={startingAgentId === agent.id} onClick={() => handleAgentSelection(agent.id)}>
<span className="relative shrink-0"><img src={getAgentAvatarSrc(agent.avatarId, agent.avatarDataUrl)} alt="" className="h-11 w-11 rounded-xl border border-border object-cover [image-rendering:pixelated]" /></span>
<span className="min-w-0 flex-1"><span className="flex items-center gap-1.5"><span className="truncate text-sm font-semibold">{agent.name || '未命名伙伴'}</span>{agent.pinned ? <Pin className="h-3 w-3 shrink-0 text-brand" /> : null}</span><span className="mt-0.5 block truncate text-[11px] text-muted-foreground">{agent.responsibility.mission || '还没有填写职责'}</span></span>
<span className="flex shrink-0 flex-col items-end gap-1">
@@ -486,7 +486,7 @@ export function AgentConversationSidebar({
{activeAgents.length === 0 ? <div className="rounded-xl border border-dashed border-border/80 bg-background/60 p-5 text-center text-xs leading-5 text-muted-foreground"><br /> + </div> : null}
</div>
{archivedAgents.length > 0 ? <section className="mt-3 border-t border-border/70 pt-3"><button type="button" className="flex w-full items-center justify-between px-1 text-xs font-semibold text-muted-foreground" aria-expanded={showArchived} onClick={() => setShowArchived((current) => !current)}><span>{archivedAgents.length}</span><ChevronRight className={cn('h-3.5 w-3.5 transition-transform duration-200 ease-out', showArchived && 'rotate-90')} /></button><DisclosureContent open={showArchived} className="mt-1" innerClassName="space-y-1">{archivedAgents.map((agent) => <div key={agent.id} className="flex items-center gap-2 rounded-lg px-2.5 py-2 text-xs text-muted-foreground"><img src={getAgentAvatarSrc(agent.avatarId, agent.avatarDataUrl)} alt="" className="h-7 w-7 rounded-lg object-cover [image-rendering:pixelated]" /><span className="min-w-0 flex-1 truncate">{agent.name || '未命名伙伴'}</span><button type="button" className="rounded p-1.5 hover:bg-background hover:text-brand" aria-label={`恢复伙伴 ${agent.name || '未命名伙伴'}`} onClick={() => void onRestoreAgent(agent)}><RotateCcw className="h-3.5 w-3.5" /></button>{!agent.builtIn ? <button type="button" className="rounded p-1.5 text-muted-foreground hover:bg-accent-soft hover:text-destructive" aria-label={`永久删除伙伴 ${agent.name || '未命名伙伴'}`} title="永久删除伙伴" onClick={() => setDeleteTarget({ kind: 'agent', agent, sessionCount: getAgentSessions(agent.id).length })}><Trash2 className="h-3.5 w-3.5" /></button> : null}</div>)}</DisclosureContent></section> : null}
{archivedAgents.length > 0 ? <section className="mt-3 border-t border-border/70 pt-3"><button type="button" className="flex w-full items-center justify-between px-1 text-xs font-semibold text-muted-foreground" aria-expanded={showArchived} onClick={() => setShowArchived((current) => !current)}><span>{archivedAgents.length}</span><ChevronRight className={cn('h-3.5 w-3.5', showArchived && 'rotate-90')} /></button><DisclosureContent open={showArchived} className="mt-1" innerClassName="space-y-1">{archivedAgents.map((agent) => <div key={agent.id} className="flex items-center gap-2 rounded-lg px-2.5 py-2 text-xs text-muted-foreground"><img src={getAgentAvatarSrc(agent.avatarId, agent.avatarDataUrl)} alt="" className="h-7 w-7 rounded-lg object-cover [image-rendering:pixelated]" /><span className="min-w-0 flex-1 truncate">{agent.name || '未命名伙伴'}</span><button type="button" className="rounded p-1.5 hover:bg-background hover:text-brand" aria-label={`恢复伙伴 ${agent.name || '未命名伙伴'}`} onClick={() => void onRestoreAgent(agent)}><RotateCcw className="h-3.5 w-3.5" /></button>{!agent.builtIn ? <button type="button" className="rounded p-1.5 text-muted-foreground hover:bg-accent-soft hover:text-destructive" aria-label={`永久删除伙伴 ${agent.name || '未命名伙伴'}`} title="永久删除伙伴" onClick={() => setDeleteTarget({ kind: 'agent', agent, sessionCount: getAgentSessions(agent.id).length })}><Trash2 className="h-3.5 w-3.5" /></button> : null}</div>)}</DisclosureContent></section> : null}
</div>
<AgentCreationDialog
@@ -528,11 +528,10 @@ export function AgentConversationSidebar({
<>
{typeof document !== 'undefined' ? createPortal(
<div
className="pointer-events-none fixed top-0 z-[80] w-[256px] border-r border-border/80 transition-[left] ease-spring motion-reduce:transition-none"
className="pointer-events-none fixed top-0 z-[80] w-[256px] border-r border-border/80"
data-testid="agent-conversation-sidebar-titlebar-portal"
style={{
left: `${sidebarPinnedCollapsed ? 0 : MIN_LAYOUT_COLUMN_WIDTH}px`,
transitionDuration: '300ms',
}}
>
{conversationHeader}
@@ -541,11 +540,10 @@ export function AgentConversationSidebar({
) : null}
{typeof document !== 'undefined' ? createPortal(
<div
className="pointer-events-none fixed top-0 z-[80] transition-[left] ease-spring motion-reduce:transition-none"
className="pointer-events-none fixed top-0 z-[80]"
data-testid="agent-conversation-dialog-context-portal"
style={{
left: `${sidebarPinnedCollapsed ? MIN_LAYOUT_COLUMN_WIDTH : MIN_LAYOUT_COLUMN_WIDTH * 2}px`,
transitionDuration: '300ms',
}}
>
<div className="pointer-events-auto">{projectContextHeader}</div>

View File

@@ -187,7 +187,7 @@ export function ProjectSessionTree({
key={project.id}
data-testid={`opencode-project-node-${project.id}`}
className={cn(
'rounded-lg border p-1 transition-[background-color,border-color,box-shadow]',
'rounded-lg border p-1 transition-colors duration-100',
active
? 'border-foreground/15 bg-brand-soft text-foreground shadow-soft'
: 'border-border/70 bg-surface-subtle text-foreground',
@@ -207,7 +207,7 @@ export function ProjectSessionTree({
aria-label={expanded ? `Collapse project ${project.name}` : `Expand project ${project.name}`}
onClick={() => toggleProjectExpanded(project.id)}
>
{expanded ? <ChevronDown className="h-4 w-4 transition-transform duration-200 ease-out" /> : <ChevronRight className="h-4 w-4 transition-transform duration-200 ease-out" />}
{expanded ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
</Button>
<button

View File

@@ -192,7 +192,7 @@ export function UserProfileDialog({
<button
type="button"
aria-label="关闭个人资料"
className="flex h-9 w-9 items-center justify-center rounded-md border border-foreground/15 bg-white text-foreground transition-transform active:scale-[0.985]"
className="flex h-9 w-9 items-center justify-center rounded-md border border-foreground/15 bg-white text-foreground"
onClick={() => onOpenChange(false)}
disabled={profileAvatarProcessing || profileSaving}
>
@@ -223,7 +223,7 @@ export function UserProfileDialog({
<label
htmlFor="profile-avatar-upload"
className={cn(
'inline-flex min-h-8 cursor-pointer items-center justify-center gap-1.5 rounded-md border border-foreground/15 bg-white px-2.5 py-1.5 text-xs font-semibold text-foreground shadow-soft transition-transform active:scale-[0.985]',
'inline-flex min-h-8 cursor-pointer items-center justify-center gap-1.5 rounded-md border border-foreground/15 bg-white px-2.5 py-1.5 text-xs font-semibold text-foreground shadow-soft',
(profileAvatarProcessing || profileSaving) && 'pointer-events-none opacity-60',
)}
>

View File

@@ -57,7 +57,7 @@ import { useAuthStore } from '@/stores/auth';
import { hostApiFetch } from '@/lib/host-api';
import { subscribeHostEvent } from '@/lib/host-events';
const inputClasses = 'h-[44px] rounded-xl font-mono text-meta bg-surface-input border-border/70 focus-visible:ring-2 focus-visible:ring-brand/35 focus-visible:border-brand shadow-sm transition-all text-foreground placeholder:text-foreground/40';
const inputClasses = 'h-[44px] rounded-xl font-mono text-meta bg-surface-input border-border/70 focus-visible:ring-2 focus-visible:ring-brand/35 focus-visible:border-brand shadow-sm transition-colors duration-100 text-foreground placeholder:text-foreground/40';
const labelClasses = 'text-sm text-foreground/80 font-medium';
type ArkMode = 'apikey' | 'codeplan';
@@ -623,7 +623,7 @@ function ProviderCard({
<div
data-testid={`provider-card-${account.id}`}
className={cn(
"group flex flex-col p-4 rounded-2xl transition-all relative overflow-hidden hover:bg-surface-subtle",
"group flex flex-col p-4 rounded-2xl transition-colors duration-100 relative overflow-hidden hover:bg-surface-subtle",
isDefault
? "bg-surface-subtle border border-transparent"
: "bg-transparent border border-transparent"
@@ -631,7 +631,7 @@ function ProviderCard({
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="h-[42px] w-[42px] shrink-0 flex items-center justify-center text-foreground border border-border/70 border-border/70 rounded-full bg-surface-subtle shadow-sm group-hover:scale-105 transition-transform">
<div className="h-[42px] w-[42px] shrink-0 flex items-center justify-center text-foreground border border-border/70 border-border/70 rounded-full bg-surface-subtle shadow-sm">
{getProviderIconUrl(account.vendorId) ? (
<img src={getProviderIconUrl(account.vendorId)} alt={typeInfo?.name || account.vendorId} className="h-5 w-5" />
) : (
@@ -860,7 +860,7 @@ function ProviderCard({
className="flex items-center justify-between w-full text-sm font-medium text-foreground/80 hover:text-foreground transition-colors"
>
<span>{t('aiProviders.sections.fallback')}</span>
<ChevronDown className={cn("h-4 w-4 transition-transform duration-200 ease-out", showFallback && "rotate-180")} />
<ChevronDown className={cn("h-4 w-4", showFallback && "rotate-180")} />
</button>
<DisclosureContent open={showFallback} className="mt-2" innerClassName="space-y-3 pt-2">
<div className="space-y-1.5">
@@ -871,7 +871,7 @@ function ProviderCard({
placeholder={t('aiProviders.dialog.fallbackModelIdsPlaceholder')}
className={isDefault
? "min-h-24 w-full rounded-xl border border-border/70 bg-background px-3 py-2 text-meta font-mono outline-none focus-visible:ring-2 focus-visible:ring-brand/35 shadow-sm"
: "min-h-24 w-full rounded-xl border border-border/70 bg-surface-input px-3 py-2 text-meta font-mono outline-none focus-visible:ring-2 focus-visible:ring-brand/35 focus-visible:border-brand shadow-sm transition-all text-foreground placeholder:text-foreground/40"}
: "min-h-24 w-full rounded-xl border border-border/70 bg-surface-input px-3 py-2 text-meta font-mono outline-none focus-visible:ring-2 focus-visible:ring-brand/35 focus-visible:border-brand shadow-sm transition-colors duration-100 text-foreground placeholder:text-foreground/40"}
/>
<p className="text-xs text-muted-foreground">
{t('aiProviders.dialog.fallbackModelIdsHelp')}
@@ -1381,7 +1381,7 @@ function AddProviderDialog({
}}
className="p-4 rounded-2xl border border-border/70 hover:bg-surface-subtle transition-colors text-center group"
>
<div className="h-12 w-12 mx-auto mb-3 flex items-center justify-center bg-surface-subtle rounded-xl shadow-sm border border-border/70 group-hover:scale-105 transition-transform">
<div className="h-12 w-12 mx-auto mb-3 flex items-center justify-center bg-surface-subtle rounded-xl shadow-sm border border-border/70">
{getProviderIconUrl(type.id) ? (
<img src={getProviderIconUrl(type.id)} alt={type.name} className="h-6 w-6" />
) : (
@@ -1637,7 +1637,7 @@ function AddProviderDialog({
className="flex items-center justify-between w-full text-sm font-medium text-foreground/80 hover:text-foreground transition-colors"
>
<span>{t('aiProviders.dialog.advancedConfig')}</span>
<ChevronDown className={cn("h-4 w-4 transition-transform duration-200 ease-out", showAdvancedConfig && "rotate-180")} />
<ChevronDown className={cn("h-4 w-4", showAdvancedConfig && "rotate-180")} />
</button>
<DisclosureContent open={showAdvancedConfig} className="mt-1" innerClassName="space-y-2.5 pt-1">
<Label htmlFor="userAgent" className={labelClasses}>{t('aiProviders.dialog.userAgent')}</Label>

View File

@@ -17,7 +17,7 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-[100] bg-foreground/15 backdrop-blur-[3px] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
"fixed inset-0 z-[100] bg-foreground/15 transition-opacity duration-150 data-[state=open]:opacity-100 data-[state=closed]:opacity-0",
className,
)}
{...props}
@@ -35,7 +35,7 @@ const DialogContent = React.forwardRef<
ref={ref}
aria-modal="true"
className={cn(
"glass-surface fixed left-1/2 top-1/2 z-[110] grid w-[calc(100%-2rem)] max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 rounded-2xl border border-border/80 bg-background p-6 text-foreground shadow-float duration-300 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
"glass-surface fixed left-1/2 top-1/2 z-[110] grid w-[calc(100%-2rem)] max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 rounded-2xl border border-border/80 bg-background p-6 text-foreground shadow-float transition-opacity duration-150 data-[state=open]:opacity-100 data-[state=closed]:opacity-0",
className,
)}
{...props}

View File

@@ -16,7 +16,7 @@ const Progress = React.forwardRef<
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
className="h-full w-full flex-1 bg-primary transition-transform duration-100"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>

View File

@@ -21,7 +21,7 @@ const SheetOverlay = React.forwardRef<
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
"fixed inset-0 z-[100] bg-foreground/15 backdrop-blur-[2px] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
"fixed inset-0 z-[100] bg-foreground/15 transition-opacity duration-150 data-[state=open]:opacity-100 data-[state=closed]:opacity-0",
className
)}
{...props}
@@ -31,16 +31,14 @@ const SheetOverlay = React.forwardRef<
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
const sheetVariants = cva(
"glass-surface fixed z-[110] gap-4 border-border/80 p-6 shadow-float transition ease-spring data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-300",
"glass-surface fixed z-[110] gap-4 border-border/80 bg-background p-6 shadow-float transition-opacity duration-150 data-[state=open]:opacity-100 data-[state=closed]:opacity-0",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
top: "inset-x-0 top-0 border-b",
bottom: "inset-x-0 bottom-0 border-t",
left: "inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
right: "inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
},
},
defaultVariants: {

View File

@@ -20,7 +20,7 @@ const Switch = React.forwardRef<
>
<SwitchPrimitives.Thumb
className={cn(
'pointer-events-none block h-5 w-5 rounded-full bg-background shadow-sm ring-0 transition-transform duration-200 data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0'
'pointer-events-none block h-5 w-5 rounded-full bg-background shadow-sm ring-0 data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0'
)}
/>
</SwitchPrimitives.Root>

View File

@@ -17,7 +17,7 @@ const TooltipContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md transition-opacity duration-100 data-[state=open]:opacity-100 data-[state=closed]:opacity-0",
className
)}
{...props}