feat: optimize dashboard/search UX and simplify extension architecture

Redesign Dashboard with compact tool grid and recently used tools
Improve TopBar search UX with Cmd/Ctrl+K shortcut and better history navigation
Reorganize project structure into src/
Migrate i18n from react-i18next to chrome.i18n
Remove runtime language switch and settings page
Remove HTML/Markdown conversion tools
Clean up unused code, dead animations, redundant comments, and imports
Improve component consistency with shadcn/ui patterns
Replace hardcoded strings/colors with i18n tokens and theme tokens
Add comprehensive project documentation and coding standards
Fix CI artifact upload workflow and multiple TypeScript/test issues

Includes various refactors, UI polish, i18n cleanup, CI improvements,
and maintenance updates across the codebase.
This commit is contained in:
LingandRX
2026-05-28 22:39:25 +08:00
committed by GitHub
parent d4c29a1bf4
commit 945780def8
200 changed files with 1557 additions and 4380 deletions
+107
View File
@@ -0,0 +1,107 @@
import type { ComponentType } from 'react';
import React from 'react';
import type { LucideProps } from 'lucide-react';
import { ChevronRight } from 'lucide-react';
import type { PaletteColorKey } from '@/config/features';
import { cn } from '@/lib/utils';
const PALETTE_COLORS: Record<PaletteColorKey, string> = {
primary: '13, 148, 136', // teal
success: '22, 163, 74', // green
warning: '217, 119, 6', // amber (存储清理的橙色轴)
error: '220, 38, 38', // red
secondary: '147, 51, 232',
info: '37, 99, 235', // blue
};
export interface ToolCardProps extends React.HTMLAttributes<HTMLDivElement> {
title: string;
description?: string;
snapshot?: React.ReactNode;
colorKey: PaletteColorKey;
icon: ComponentType<LucideProps>;
onNavigate: () => void;
}
export default function ToolCard({
title,
description,
snapshot,
colorKey,
icon: IconComponent,
onNavigate,
className,
...props
}: ToolCardProps) {
const rgbValues = PALETTE_COLORS[colorKey];
return (
<div
style={{
['--tool-color' as string]: rgbValues,
}}
/* 💡 核心修复点:
- 坚决不用 h-full 或固定高度,锁死 h-auto(高度自适应流),配合 py-4 px-4 牢牢把内容包裹在卡片体内。
- 废除原先会乱飘的内联 style 属性擦写,全权放权给 Tailwind 的声明式 hover 变体。
*/
className={cn(
'group relative rounded-xl border border-border/70 bg-card text-card-foreground p-4 h-auto flex flex-col items-stretch justify-start gap-3 shadow-sm select-none box-border',
'hover:bg-muted/30',
'hover:border-[rgba(var(--tool-color),0.45)]',
'hover:shadow-[0_8px_24px_-8px_rgba(var(--tool-color),0.14)] dark:hover:shadow-[0_8px_30px_-10px_rgba(var(--tool-color),0.25)]',
className,
)}
{...props}
>
{/* 上半部分:核心信息交互排版轴 */}
<div className="flex items-center justify-between w-full relative min-w-0 min-h-[44px]">
<div className="flex gap-3 items-center min-w-0 flex-1 pr-2">
{/* 左侧圆形图标容器 */}
<div
className={cn(
'flex items-center justify-center w-10 h-10 rounded-xl shrink-0',
'bg-[rgba(var(--tool-color),0.08)] dark:bg-[rgba(var(--tool-color),0.12)]',
'text-[rgb(var(--tool-color))]',
)}
>
<IconComponent className="h-5 w-5 shrink-0" />
</div>
{/* 中间文字描述区:利用 flex-1 min-w-0 防御文本过长发生恶性撑开 */}
<div className="flex-1 min-w-0 flex flex-col">
<h4 className="font-bold text-sm tracking-tight text-foreground leading-snug">
{title}
</h4>
{description && (
<p className="text-[11px] font-medium text-muted-foreground/90 mt-0.5 leading-normal w-full truncate">
{description}
</p>
)}
</div>
</div>
{/* 右侧指示小箭头 */}
<div className="text-muted-foreground/40 group-hover:text-[rgb(var(--tool-color))] p-1 shrink-0 group-hover:translate-x-0.5">
<ChevronRight className="h-4 w-4" />
</div>
{/* 覆盖整个上半部分的绝对定位隐形跳转层(A11y 无障碍标准合规) */}
<button
type="button"
onClick={onNavigate}
aria-label={`进入 ${title}`}
className="absolute inset-0 w-full h-full cursor-pointer bg-transparent border-none opacity-0 focus-visible:outline-none"
/>
</div>
{/* 下半部分:未来的动态预览沙箱独立承载区 */}
{snapshot != null && (
<div className="mt-1 pt-3 border-t border-dashed border-border/80 w-full relative z-10 select-text">
{snapshot}
</div>
)}
</div>
);
}
ToolCard.displayName = 'ToolCard';
+99
View File
@@ -0,0 +1,99 @@
import { useRouter } from '@/providers/RouterProvider';
import { getFeatureByKey } from '@/config/features';
import type { PageType } from '@/types/storage';
import { useMemo } from 'react';
import { useI18n } from '@/utils/chromeI18n';
import { cn } from '@/lib/utils';
export default function DashboardPage() {
const { navigateTo, visiblePages, pageOrder, recentlyUsedTools } = useRouter();
const { t } = useI18n(['features']);
const visibleSet = useMemo(() => new Set<string>(visiblePages), [visiblePages]);
const visibleFeatures = useMemo(() => {
return pageOrder
.filter((key) => visibleSet.has(key))
.map((key) => ({ key, feature: getFeatureByKey(key) }))
.filter((item) => item.feature?.themeColorKey && item.feature.icon != null);
}, [pageOrder, visibleSet]);
const recentFeatures = useMemo(() => {
return recentlyUsedTools
.filter((key) => visibleSet.has(key))
.map((key) => ({ key, feature: getFeatureByKey(key) }))
.filter((item) => item.feature?.themeColorKey && item.feature.icon != null);
}, [recentlyUsedTools, visibleSet]);
const showRecent = recentFeatures.length > 0;
return (
<div className={cn('flex flex-col gap-4 p-3.5 w-full h-auto select-none')}>
{/* 最近使用 */}
{showRecent && (
<div className="flex flex-col gap-2">
<h3 className="text-xs font-semibold text-muted-foreground/80 uppercase tracking-wider">
{t('dashboard_recentlyUsed')}
</h3>
<div className="flex flex-wrap gap-2">
{recentFeatures.map(({ key, feature }) => {
const IconComponent = feature!.icon!;
return (
<button
key={key}
type="button"
onClick={() => navigateTo(key as PageType)}
className={cn(
'inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium',
'border border-border/60 bg-card text-card-foreground',
'hover:bg-muted/40 hover:border-primary/30',
'transition-colors cursor-pointer',
)}
>
<IconComponent className="h-3.5 w-3.5 text-muted-foreground/70" />
{t(feature!.labelKey)}
</button>
);
})}
</div>
</div>
)}
{/* 全部工具 — 紧凑 Grid */}
<div className="flex flex-col gap-2">
<h3 className="text-xs font-semibold text-muted-foreground/80 uppercase tracking-wider">
{t('dashboard_allTools')}
</h3>
<div className={cn('grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 gap-2')}>
{visibleFeatures.map(({ key, feature }) => {
const IconComponent = feature!.icon!;
return (
<button
key={key}
type="button"
onClick={() => navigateTo(key as PageType)}
className={cn(
'group flex flex-col items-center justify-center gap-1.5',
'py-3 px-2 rounded-xl border border-border/50 bg-card',
'hover:bg-muted/40 hover:border-primary/30',
'transition-colors cursor-pointer',
)}
>
<IconComponent
className={cn(
'h-5 w-5 text-muted-foreground/70',
'group-hover:text-foreground',
'transition-colors',
)}
/>
<span className="text-[11px] font-medium text-muted-foreground/80 group-hover:text-foreground leading-tight text-center truncate w-full transition-colors">
{t(feature!.labelKey)}
</span>
</button>
);
})}
</div>
</div>
</div>
);
}