Files
testing-tool/components/ToolCard.tsx
T
LingandRX 84ffd2a132 Develop fastapi (#6)
* feat: add side panel with navigation and storage cleaner functionality

* feat: add OpenUrlPage and integrate into app navigation

* feat: 添加 GlobalSnackbar 组件并在多个页面中集成,替换原有 Snackbar 实现

* feat: fix OpenUrl sidebar issue with architecture refactor

- 修复原问题:不再直接替换侧边栏 URL,保持插件导航可见
- 采用配置页 + 查看页分离架构:OpenUrlPage (配置) + OpenUrlViewerPage (查看)
- 支持多个 URL 快捷方式管理(添加/删除)
- 每个 URL 提供两种打开方式:在侧边栏打开 / 在新标签页打开
- 侧边栏查看页使用 iframe 占满全部剩余空间
- 更新 TypeScript 类型定义
- 保留原有混合内容警告检查
- 数据持久化到 Chrome Storage

* refactor: centralized route management - consolidate routing config into single source

* feat: 更换logo

* refactor: code review fixes - security and race condition improvements

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Changes:

- wxt.config.ts: Remove unused `debugger` permission (no code uses it)
- OpenUrlPage.tsx: Fix unreachable showMessage after window.close()
- OpenUrlPage.tsx: Replace unnecessary div wrapper with Fragment to reduce DOM nesting
- OpenUrlViewerPage.tsx: Add URL validation to prevent XSS via javascript:/data: URLs
- OpenUrlViewerPage.tsx: Add sandbox attribute to iframe for security isolation
- OpenUrlViewerPage.tsx: Add error handling for invalid URLs
- StorageCleanerPage.tsx: Fix race condition in handleOptionChange preference saving
- StorageCleanerPage.tsx: Remove unnecessary storage reads when saving preferences (use state directly)
- StorageCleanerPage.tsx: Add timeout cleanup for setTimeout to follow React best practices

* feat: implement drill-down navigation with master-detail dashboard

* feat: enhance dashboard dynamism and refine options UI

* feat: overhaul storage cleaner UI with real-time size estimation and modern aesthetics

* feat: overhaul TimestampPage UI/UX and fix GlobalSnackbar positioning

* fix: decouple popup routing from storage sync and enhance OpenUrlPage UI

* fix: avoid closing sidepanel when opening URL preview from within sidepanel
2026-04-16 08:54:23 +08:00

115 lines
3.1 KiB
TypeScript

import { Box, Typography, Stack } from '@mui/material';
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome'; // Sparkles for AI
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
import React from 'react';
interface ToolCardProps {
title: string;
description?: string;
snapshot?: React.ReactNode;
colorCode: string;
icon: React.ReactNode;
onClick: () => void;
hasAI?: boolean;
}
export default function ToolCard({ title, description, snapshot, colorCode, icon, onClick, hasAI }: ToolCardProps) {
return (
<Box
onClick={onClick}
sx={{
position: 'relative',
bgcolor: 'background.paper',
borderRadius: 4,
p: 2.5,
cursor: 'pointer',
border: '1px solid',
borderColor: 'grey.100',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
display: 'flex',
flexDirection: 'column',
gap: 1.5,
'&:hover': {
borderColor: colorCode,
transform: 'translateY(-4px)',
boxShadow: `0 12px 24px -10px ${colorCode}33`, // 20% opacity of colorCode
'& .arrow-icon': {
transform: 'translateX(4px)',
color: colorCode
}
}
}}
>
<Stack direction="row" justifyContent="space-between" alignItems="flex-start">
<Stack direction="row" spacing={1.5} alignItems="center">
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: 40,
height: 40,
borderRadius: 3,
bgcolor: `${colorCode}11`, // 7% opacity
color: colorCode
}}
>
{icon}
</Box>
<Box>
<Typography
variant="subtitle1"
sx={{
fontWeight: 700,
lineHeight: 1.2,
color: 'text.primary',
display: 'flex',
alignItems: 'center',
gap: 0.5
}}
>
{title}
{hasAI && <AutoAwesomeIcon sx={{ fontSize: 14, color: '#f5b041' }} />}
</Typography>
{description && (
<Typography
variant="caption"
sx={{
color: 'text.secondary',
fontWeight: 500,
display: 'block',
mt: 0.5
}}
>
{description}
</Typography>
)}
</Box>
</Stack>
<ArrowForwardIosIcon
className="arrow-icon"
sx={{
fontSize: 12,
color: 'grey.300',
mt: 0.5,
transition: 'all 0.3s ease'
}}
/>
</Stack>
{snapshot && (
<Box
sx={{
mt: 'auto',
pt: 1.5,
borderTop: '1px dashed',
borderColor: 'grey.100'
}}
>
{snapshot}
</Box>
)}
</Box>
);
}