diff --git a/AGENTS.md b/AGENTS.md
index d2f240d..f28e8bd 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,6 +1,6 @@
# AGENTS.md
-WXT 浏览器扩展项目 (React 19 + TypeScript)。
+WXT 浏览器扩展项目 (React 19 + TypeScript)。提供时间戳转换、存储清理、JWT 解析、JSON 工具、二维码、Base64、Markdown 等测试效率工具。
## 核心命令
@@ -22,14 +22,17 @@ npm run test:coverage # 带覆盖率的测试
## 验证流程
-CI 执行顺序: `setup → lint/typecheck/test(并行) → build` (build 依赖前三者)。
+CI 步骤(严格顺序,任一步骤失败则停止并标记 CI 失败):
-Pre-commit hook (`.husky/pre-commit` 调用 `lint-staged`):
+1. `setup`(安装依赖、`wxt prepare`)
+2. 并行运行 `lint`、`typecheck`、`test`(三者全部通过才继续)
+3. `build`(仅当步骤 2 全部成功时执行)
-- 代码文件 (`*.{ts,tsx,js,jsx,mjs}`): `eslint --fix --max-warnings=0 --no-warn-ignored` → `prettier --write`
-- 其他文件 (`*.{json,css,scss,md}`): `prettier --write`
+Pre-commit hook(`.husky/pre-commit` 调用 `lint-staged`,任一步骤返回非零则终止提交):
-提交前确保 `lint` 和 `typecheck` 通过。
+1. 代码文件 (`*.{ts,tsx,js,jsx,mjs}`):运行 `eslint --fix --max-warnings=0 --no-warn-ignored`;若失败则终止并报告错误
+2. 同一代码文件:运行 `prettier --write`
+3. 其他文件 (`*.{json,css,scss,md}`):运行 `prettier --write`
## WXT 生成文件
@@ -54,10 +57,12 @@ i18n/locales/{zh,en}/ # 国际化资源 (common.json, features.json 及各功
## 关键架构决策
**路由**: 不使用 React Router。通过 `config/features.tsx` 的 `FEATURES` 数组管理,`RouterProvider` 根据 `PageType`
-渲染对应组件。支持三种渲染模式: popup / sidepanel / tab。
+渲染对应组件。支持三种渲染模式:popup(弹窗)、sidepanel(侧边栏)和 browser-tab(浏览器新标签页,通过 `open_in_tab` 打开)。
+每种模式有独立的路由和可见页面配置。
-**存储**: 所有 Chrome Storage 键必须在 `types/storage.d.ts` 的 `StorageSchema` 中定义。使用 `utils/chromeStorage.ts` 及其
-Hook。Router 同时使用 `chrome.storage.local` 和 `localStorage` 做快照以消除首屏闪烁。
+**存储**: 所有 Chrome Storage 键必须在 `types/storage.d.ts` 的 `StorageSchema` 中定义,键名使用 kebab-case 格式(如 `app/currentRoute`)。
+使用 `utils/chromeStorage.ts` 及其 Hook。Router 同时使用 `chrome.storage.local` 和 `localStorage` 做快照以消除首屏闪烁。
+修改 StorageSchema 时,必须在 `utils/chromeStorage.ts` 添加版本迁移函数,并在测试中覆盖迁移场景。
**通信**: 使用 `@webext-core/messaging`,协议定义在 `utils/messages.ts`。
@@ -75,13 +80,21 @@ Hook。Router 同时使用 `chrome.storage.local` 和 `localStorage` 做快照
- `@/utils/useLazyTranslation` (返回 `ns:key` 格式翻译)
- `window.matchMedia`
- 测试文件命名: `__tests__/*.test.{ts,tsx}` 或 `*.test.{ts,tsx}`
+- Mock 模式: 使用 `vi.mock()` 进行模块级 mock,避免在测试文件中重复 mock 代码
+- 测试工具: `@testing-library/react` + `@testing-library/user-event` 进行组件测试
## i18n
- 命名空间: `common` (默认), `features`
- 翻译键格式: `namespace:key` (如 `features:timestamp.title`)
- 语言: `zh` (默认), `en`
+- 翻译文件结构:
+ - `i18n/locales/{zh,en}/common.json` - 全局通用翻译
+ - `i18n/locales/{zh,en}/features.json` - 功能模块标题和描述
+ - `i18n/locales/{zh,en}/{功能名}.json` - 各功能独立翻译(如 timestamp.json, storageCleaner.json 等)
- 添加新翻译: 编辑 `i18n/locales/{zh,en}/{common,features}.json` 及对应功能独立 JSON
+- 使用 `useLazyTranslation` hook 加载功能独立翻译,返回 `ns:key` 格式
+- 回退策略: 当翻译 key 在目标语言缺失时,回退到默认语言 `zh`;若默认语言也缺失,返回占位格式 `namespace:key` 并在开发模式下记录 warning
## 新功能开发清单
@@ -97,6 +110,8 @@ Hook。Router 同时使用 `chrome.storage.local` 和 `localStorage` 做快照
- 禁止使用 `any` (测试文件除外)
- 未使用变量/参数: 使用 `_` 前缀 (如 `_unused`)
- 样式: 使用 Tailwind CSS + shadcn/ui (通过 `className` 和 `cn()` 工具)
+- UI 组件: 优先使用 `components/ui/` 下的 shadcn/ui 组件 (button, dialog, select 等)
+- 图标: 使用 `lucide-react` 图标库
- 格式: Prettier (`.prettierrc`: 100 字符宽, 单引号, 尾逗号 all, LF 换行)
- ESLint 使用 `typescript-eslint` 的 `projectService: true`(无需手动维护 project 路径)
@@ -109,3 +124,6 @@ Hook。Router 同时使用 `chrome.storage.local` 和 `localStorage` 做快照
- TypeScript: ^5.9.3
- Vitest: ^4.1.7
- i18next: ^26.2.0
+- @dnd-kit (拖拽排序)
+- marked (Markdown 解析)
+- qrious + qr-scanner (二维码生成与解析)
diff --git a/pages/Base64Converter/Base64ConverterSection.tsx b/pages/Base64Converter/Base64ConverterSection.tsx
index 3cc0d4e..8639af9 100644
--- a/pages/Base64Converter/Base64ConverterSection.tsx
+++ b/pages/Base64Converter/Base64ConverterSection.tsx
@@ -57,7 +57,7 @@ export default function Base64ConverterSection({ mode }: Base64ConverterSectionP
};
return (
-
+
fileInputRef.current?.click()}
className={cn(
- 'flex flex-col items-center justify-center min-h-[190px] border-2 border-dashed rounded-2xl p-8 cursor-pointer transition-all duration-300',
+ 'flex flex-col items-center justify-center min-h-[190px] border-2 border-dashed rounded-2xl p-8 cursor-pointer',
isDragging
? 'border-primary bg-primary/10'
: info
@@ -107,7 +107,7 @@ export default function Base64ConverterSection({ mode }: Base64ConverterSectionP
{isLoading ? (
) : info ? (
-
+
{mode === 'image' && result && (
)}
-
+
{info.name}
diff --git a/pages/Base64Converter/FileMode.tsx b/pages/Base64Converter/FileMode.tsx
index c331fac..d40fd41 100644
--- a/pages/Base64Converter/FileMode.tsx
+++ b/pages/Base64Converter/FileMode.tsx
@@ -45,7 +45,7 @@ export default function FileMode() {
};
return (
-
+
fileInputRef.current?.click()}
className={cn(
- 'flex flex-col items-center justify-center min-h-[190px] border-2 border-dashed rounded-2xl p-8 cursor-pointer transition-all duration-300',
+ 'flex flex-col items-center justify-center min-h-[190px] border-2 border-dashed rounded-2xl p-8 cursor-pointer',
isDragging
? 'border-primary bg-primary/10'
: info
@@ -100,7 +100,7 @@ export default function FileMode() {
) : info ? (
-
+
{info.name}
diff --git a/pages/Base64Converter/ImageMode.tsx b/pages/Base64Converter/ImageMode.tsx
index a61285a..eafb0c3 100644
--- a/pages/Base64Converter/ImageMode.tsx
+++ b/pages/Base64Converter/ImageMode.tsx
@@ -52,7 +52,7 @@ export default function ImageMode() {
};
return (
-
+
fileInputRef.current?.click()}
className={cn(
- 'flex flex-col items-center justify-center min-h-[190px] border-2 border-dashed rounded-2xl p-8 cursor-pointer transition-all duration-300',
+ 'flex flex-col items-center justify-center min-h-[190px] border-2 border-dashed rounded-2xl p-8 cursor-pointer',
isDragging
? 'border-primary bg-primary/10'
: info
@@ -103,7 +103,7 @@ export default function ImageMode() {
{isLoading ? (
) : info ? (
-
+
{result && (
![]()
{decoded && (
-
+
+
{/* 受控方向切流中枢 */}
+
{t('imageDataUriHint')}
@@ -134,7 +134,7 @@ export default function TextMode({ onSwitchToImageMode }: TextModeProps = {}) {
variant="ghost"
size="sm"
onClick={onSwitchToImageMode}
- className="h-7 rounded-md text-xs font-bold text-primary hover:text-primary hover:bg-primary/20 dark:hover:bg-primary/10 transition-colors px-2.5"
+ className="h-7 rounded-md text-xs font-bold text-primary hover:text-primary hover:bg-primary/20 dark:hover:bg-primary/10 px-2.5"
>
{t('switchToImageMode')}
@@ -143,7 +143,7 @@ export default function TextMode({ onSwitchToImageMode }: TextModeProps = {}) {
{/* 5. 编码/解码核心数据承载流卡片 */}
{output && (
-
+
{outputLabel}
diff --git a/pages/Base64Converter/index.tsx b/pages/Base64Converter/index.tsx
index 5b53d44..dadb075 100644
--- a/pages/Base64Converter/index.tsx
+++ b/pages/Base64Converter/index.tsx
@@ -20,7 +20,7 @@ export default function Index() {
);
return (
-
+
{/* 右侧指示小箭头 */}
-
+
diff --git a/pages/Dashboard/index.tsx b/pages/Dashboard/index.tsx
index 2a413ab..8479dda 100644
--- a/pages/Dashboard/index.tsx
+++ b/pages/Dashboard/index.tsx
@@ -16,7 +16,7 @@ export default function DashboardPage() {
{pageOrder.map((key) => {
diff --git a/pages/HtmlToMarkdown/index.tsx b/pages/HtmlToMarkdown/index.tsx
index aab869b..f860989 100644
--- a/pages/HtmlToMarkdown/index.tsx
+++ b/pages/HtmlToMarkdown/index.tsx
@@ -49,7 +49,7 @@ export default function HtmlToMarkdownPage() {
- 彻底清除多余的 container max-w-7xl 这种网页大边距,
- 统一收拢为我们先前在 Dashboard 页、JSON 工具箱制定的 p-4 space-y-4 标准极客桌面规格。
*/
-
+
{/* 工具栏集成区 */}
@@ -72,7 +72,7 @@ export default function HtmlToMarkdownPage() {
size="sm"
onClick={handleDownload}
disabled={!result.markdown}
- className="h-8 rounded-md font-medium text-xs gap-1.5 shadow-sm active:scale-95 transition-all"
+ className="h-8 rounded-md font-medium text-xs gap-1.5 shadow-sm"
>
{t('download')}
@@ -83,7 +83,7 @@ export default function HtmlToMarkdownPage() {
variant="outline"
size="sm"
onClick={handleClear}
- className="h-8 rounded-md font-medium text-xs gap-1.5 shadow-sm active:scale-95 text-destructive hover:text-destructive hover:bg-destructive/5 dark:hover:bg-destructive/10 border-input/60 transition-all"
+ className="h-8 rounded-md font-medium text-xs gap-1.5 shadow-sm text-destructive hover:text-destructive hover:bg-destructive/5 dark:hover:bg-destructive/10 border-input/60"
>
{t('clear')}
@@ -95,7 +95,7 @@ export default function HtmlToMarkdownPage() {
- 💡 核心修复点:将硬编码的 bg-red-50 实色,完美超进化为系统的全自适应透明色变体
*/}
{error && (
-
+
{error}
)}
@@ -114,7 +114,7 @@ export default function HtmlToMarkdownPage() {
- 只要用户用鼠标点击了内部的 textarea,外层整块精巧的圆角大边框会一帧内亮起 primary 系统的深色呼吸发光环,
- 这种“全外包裹层框聚焦”的体验极大模仿了本地原生 IDE 的硬核专业体验!
*/
-
+
{/* 卡片头部:改用标准的灰色 bg-muted/50 */}
diff --git a/pages/JsonTools/DiffNavigator.tsx b/pages/JsonTools/DiffNavigator.tsx
index 353b3f4..2ee84f3 100644
--- a/pages/JsonTools/DiffNavigator.tsx
+++ b/pages/JsonTools/DiffNavigator.tsx
@@ -30,7 +30,7 @@ export default function DiffNavigator({
return (
@@ -81,9 +81,9 @@ export default function DiffNavigator({
aria-label={t('jsonDiff:nextDiff')}
onClick={onNext}
className={cn(
- 'p-1 rounded-md text-muted-foreground transition-all hover:bg-accent hover:text-foreground',
+ 'p-1 rounded-md text-muted-foreground hover:bg-accent hover:text-foreground',
'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring',
- 'disabled:pointer-events-none disabled:opacity-30 active:scale-95', // 4. 边界拦截:触底时优雅淡化并锁死点击
+ 'disabled:pointer-events-none disabled:opacity-30', // 4. 边界拦截:触底时优雅淡化并锁死点击
)}
>
diff --git a/pages/JsonTools/DiffResult.tsx b/pages/JsonTools/DiffResult.tsx
index e254446..08ec54b 100644
--- a/pages/JsonTools/DiffResult.tsx
+++ b/pages/JsonTools/DiffResult.tsx
@@ -197,7 +197,7 @@ const UnifiedRow = ({ depth, type, text, active, multiline }: UnifiedRowProps) =
return (
+
{/* 输入区 */}
+
{/* 结果栏精致头部 */}
diff --git a/pages/JsonTools/JsonFormatSection.tsx b/pages/JsonTools/JsonFormatSection.tsx
index e1f3aa0..bc4b091 100644
--- a/pages/JsonTools/JsonFormatSection.tsx
+++ b/pages/JsonTools/JsonFormatSection.tsx
@@ -62,7 +62,7 @@ export default function JsonFormatSection() {
: null;
return (
-
+
{/* 工具控制栏 */}
@@ -95,7 +95,7 @@ export default function JsonFormatSection() {
/>
@@ -118,7 +118,7 @@ export default function JsonFormatSection() {
{/* 格式化结果流面板展示 */}
{result && result.formatted ? (
-
+
{/* 结果栏头部 */}
diff --git a/pages/JsonTools/JsonTree.tsx b/pages/JsonTools/JsonTree.tsx
index fcd3f0d..597791d 100644
--- a/pages/JsonTools/JsonTree.tsx
+++ b/pages/JsonTools/JsonTree.tsx
@@ -135,7 +135,7 @@ const NodeRow = React.memo(
setOverride(expanded ? 'closed' : 'open')}
className={cn(
- 'group flex items-center gap-1 py-0.5 pr-2 text-xs font-mono select-none cursor-pointer rounded-sm transition-colors w-full h-[22px] leading-relaxed',
+ 'group flex items-center gap-1 py-0.5 pr-2 text-xs font-mono select-none cursor-pointer rounded-sm w-full h-[22px] leading-relaxed',
theme.bg,
isActive &&
'bg-primary/10 relative before:absolute before:left-0 before:top-0 before:bottom-0 before:w-0.5 before:bg-blue-500 rounded-none ring-0',
@@ -207,7 +207,7 @@ const NodeRow = React.memo(
+
setPageMode(v)}
@@ -129,7 +129,7 @@ export default function Index() {
/>
{pageMode === 'diff' ? (
-
+
{
const { t } = useLazyTranslation('jwt');
return (
-
+
{title}
@@ -70,7 +68,7 @@ export default function Index() {
}, [debouncedInput]);
return (
-
+
{/* 输入终端 */}
+
{/* Header 分区:完美致敬 JWT.io 的鲜艳色彩,同时实现黑夜暗化自适应 */}
{/* Signature 签名区:完全对齐标准的 shadcn 骨架阶度 */}
-
+
{t('jwt:signatureTitle')}
diff --git a/pages/MarkdownToHtml/index.tsx b/pages/MarkdownToHtml/index.tsx
index b27cfa3..a43eda3 100644
--- a/pages/MarkdownToHtml/index.tsx
+++ b/pages/MarkdownToHtml/index.tsx
@@ -175,7 +175,7 @@ export default function MarkdownToHtmlPage() {
const showPreview = previewMode !== 'html';
return (
-
+
{/* 工具集成控制中枢 */}
@@ -196,7 +196,7 @@ export default function MarkdownToHtmlPage() {
variant="outline"
size="sm"
onClick={handleClear}
- className="h-8 rounded-md font-medium text-xs gap-1.5 shadow-sm active:scale-95 text-destructive hover:text-destructive hover:bg-destructive/5 dark:hover:bg-destructive/10 border-input/60 transition-all"
+ className="h-8 rounded-md font-medium text-xs gap-1.5 shadow-sm text-destructive hover:text-destructive hover:bg-destructive/5 dark:hover:bg-destructive/10 border-input/60"
>
{t('clear')}
@@ -206,7 +206,7 @@ export default function MarkdownToHtmlPage() {
size="sm"
onClick={handlePrint}
disabled={!result.html}
- className="h-8 rounded-md font-medium text-xs gap-1.5 shadow-sm active:scale-95 transition-all"
+ className="h-8 rounded-md font-medium text-xs gap-1.5 shadow-sm"
>
{t('print')}
@@ -216,7 +216,7 @@ export default function MarkdownToHtmlPage() {
size="sm"
onClick={handleDownload}
disabled={!result.html}
- className="h-8 rounded-md font-medium text-xs gap-1.5 shadow-sm active:scale-95 transition-all"
+ className="h-8 rounded-md font-medium text-xs gap-1.5 shadow-sm"
>
{t('download')}
@@ -228,7 +228,7 @@ export default function MarkdownToHtmlPage() {
{error && (
{error}
@@ -243,7 +243,7 @@ export default function MarkdownToHtmlPage() {
>
{/* Markdown 输入翼终端 */}
{showInput && (
-
+
{t('inputLabel')}
@@ -263,7 +263,7 @@ export default function MarkdownToHtmlPage() {
{/* 实时 HTML/Iframe 预览翼终端 */}
{showPreview && (
-
+
{(previewMode as string) === 'html' ? t('htmlOutputLabel') : t('previewLabel')}
diff --git a/pages/QrCode/components/GeneratePanel.tsx b/pages/QrCode/components/GeneratePanel.tsx
index f3eceec..e9f5243 100644
--- a/pages/QrCode/components/GeneratePanel.tsx
+++ b/pages/QrCode/components/GeneratePanel.tsx
@@ -10,11 +10,11 @@ export default function GeneratePanel() {
const { generatorState, setTextToEncode, downloadQrCode, copyQrCode } = useQrCodeContext();
return (
-
+
{/* 左翼:高性能受控输入翼终端 */}
diff --git a/pages/QrCode/components/ParsePanel.tsx b/pages/QrCode/components/ParsePanel.tsx
index 80a138f..cd97651 100644
--- a/pages/QrCode/components/ParsePanel.tsx
+++ b/pages/QrCode/components/ParsePanel.tsx
@@ -61,7 +61,7 @@ export default function ParsePanel() {
/* 💡 统一大视觉轴:
- 追加 p-0.5 微隔离,配合 gap-6 建立与生成面板(GeneratePanel)绝对像素对齐的网格天平。
*/
-
+
{/* 左翼:图片接收/拖拽/剪贴板上传终端 */}
+
{/* 流式中央控制切流卡:注入 sm 断点防御,防范单栏状态下发生变形 */}
{qrCode.mode === 'generate' ? (
-
+
) : (
-
+
)}
diff --git a/pages/RightClickRestorer/index.tsx b/pages/RightClickRestorer/index.tsx
index 31194c2..bc1cbae 100644
--- a/pages/RightClickRestorer/index.tsx
+++ b/pages/RightClickRestorer/index.tsx
@@ -11,7 +11,7 @@ export default function RightClickRestorerPage() {
if (isLoading) {
return (
-
+
{t('rightClickRestorer:loading')}
@@ -20,9 +20,9 @@ export default function RightClickRestorerPage() {
}
return (
-
+
{/* Current Domain */}
-
+
@@ -50,7 +50,7 @@ export default function RightClickRestorerPage() {
{/* Unlock Action */}
-
+
{isUnsupported ? (
<>
diff --git a/pages/StorageCleaner/AutoRefreshToggle.tsx b/pages/StorageCleaner/AutoRefreshToggle.tsx
index a062723..dec8af4 100644
--- a/pages/StorageCleaner/AutoRefreshToggle.tsx
+++ b/pages/StorageCleaner/AutoRefreshToggle.tsx
@@ -48,7 +48,7 @@ export default function AutoRefreshToggle({
return (
+
{/* 2. 彻底重构容器类名结构:
- 成功状态:采用 Tailwind 官方推荐的 emerald 体系,利用 /10 (10% 透明度) 和 /20 (边框)。
- 失败状态:完全放权给标准的 border-destructive/20 和 bg-destructive/5。
diff --git a/pages/StorageCleaner/ErrorDisplay.tsx b/pages/StorageCleaner/ErrorDisplay.tsx
index 4bf067d..e653ada 100644
--- a/pages/StorageCleaner/ErrorDisplay.tsx
+++ b/pages/StorageCleaner/ErrorDisplay.tsx
@@ -13,7 +13,7 @@ export default function ErrorDisplay({ error, className, ...props }: ErrorDispla
// 1. 精简层级:单层外壳直接搞定居中、响应式高度与外部类名扩展
{/* 3. 图标与主要错误信息全面对接 text-destructive 语义色 */}
-
+
diff --git a/pages/StorageCleaner/OptionItem.tsx b/pages/StorageCleaner/OptionItem.tsx
index ba82ff5..db4b12d 100644
--- a/pages/StorageCleaner/OptionItem.tsx
+++ b/pages/StorageCleaner/OptionItem.tsx
@@ -29,7 +29,7 @@ export default function OptionItem({
// 3. 跨越级交互升级:将外部容器升级为一个高度敏感的可点击 Tab 热区
onClick={onChange}
className={cn(
- 'flex justify-between items-center py-2.5 px-3.5 rounded-xl border cursor-pointer select-none transition-all duration-200',
+ 'flex justify-between items-center py-2.5 px-3.5 rounded-xl border cursor-pointer select-none',
// 4. 彻底抛弃硬编码黄底:
// - 选中时:使用 bg-primary/5 (系统主色超淡叠加) 配合标准 border-primary/30。
// - 未选中时:保持透明 border-transparent,悬停呈现 bg-muted。
@@ -45,7 +45,7 @@ export default function OptionItem({
diff --git a/pages/StorageCleaner/StorageCleanerConfirm.tsx b/pages/StorageCleaner/StorageCleanerConfirm.tsx
index 1d736a4..190b405 100644
--- a/pages/StorageCleaner/StorageCleanerConfirm.tsx
+++ b/pages/StorageCleaner/StorageCleanerConfirm.tsx
@@ -42,7 +42,6 @@ export function StorageCleanerConfirm({
{/* 头部标题区域 */}
@@ -72,7 +71,7 @@ export function StorageCleanerConfirm({
{/* 风险警告横幅 */}
-
+
{t('storageCleaner:irreversible')}
diff --git a/pages/StorageCleaner/StorageOptionsGrid.tsx b/pages/StorageCleaner/StorageOptionsGrid.tsx
index 4e84d6f..699913d 100644
--- a/pages/StorageCleaner/StorageOptionsGrid.tsx
+++ b/pages/StorageCleaner/StorageOptionsGrid.tsx
@@ -46,7 +46,7 @@ export default function StorageOptionsGrid({
return (