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 (