develop → main: 合并开发分支 (#51)
* fix(ci): fix release workflow artifact upload issue - Split artifact upload into 3 separate steps (Chrome, Firefox, Source) - Update download steps to match new artifact names - Add if-no-files-found: error for better error handling - Fix Node.js 20 compatibility issue with upload-artifact@v4 glob pattern * docs: 更新 AGENTS.md,添加测试工具和翻译文件结构说明 * docs: 更新 AGENTS.md,完善 CI 步骤和存储键名格式说明 * fix: remove unnecessary animations from StorageCleaner page - Remove entrance animations (animate-in, fade-in, zoom-in) from all components - Remove scale effect (active:scale-[0.99]) from clean button - Remove transition effects (transition-all, transition-colors) from OptionItem, StorageOptionsGrid, AutoRefreshToggle - Remove bouncing animation from error icon - Remove pulsing animation from warning banner - Keep animate-spin on button loading spinner as functional indicator * fix: remove unnecessary animations from RightClickRestorer page * fix: remove all unnecessary animations from all pages - Remove dead code animations (animate-in, fade-in, slide-in, zoom-in, shake) from tailwindcss-animate plugin (not installed) - Remove decorative transition effects (transition-all, transition-colors) from all page components - Remove scale effects (active:scale-95) from buttons - Remove bounce animations (animate-bounce) from icons - Keep functional animate-spin on loading spinners as they provide essential loading feedback Affected pages: Dashboard, Timestamp, Jwt, JsonTools, Base64Converter, HtmlToMarkdown, MarkdownToHtml, QrCode, TextStatistics --------- Co-authored-by: Ubuntu <ubuntu@localhost.localdomain>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# AGENTS.md
|
# 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`
|
Pre-commit hook(`.husky/pre-commit` 调用 `lint-staged`,任一步骤返回非零则终止提交):
|
||||||
- 其他文件 (`*.{json,css,scss,md}`): `prettier --write`
|
|
||||||
|
|
||||||
提交前确保 `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 生成文件
|
## WXT 生成文件
|
||||||
|
|
||||||
@@ -54,10 +57,12 @@ i18n/locales/{zh,en}/ # 国际化资源 (common.json, features.json 及各功
|
|||||||
## 关键架构决策
|
## 关键架构决策
|
||||||
|
|
||||||
**路由**: 不使用 React Router。通过 `config/features.tsx` 的 `FEATURES` 数组管理,`RouterProvider` 根据 `PageType`
|
**路由**: 不使用 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` 及其
|
**存储**: 所有 Chrome Storage 键必须在 `types/storage.d.ts` 的 `StorageSchema` 中定义,键名使用 kebab-case 格式(如 `app/currentRoute`)。
|
||||||
Hook。Router 同时使用 `chrome.storage.local` 和 `localStorage` 做快照以消除首屏闪烁。
|
使用 `utils/chromeStorage.ts` 及其 Hook。Router 同时使用 `chrome.storage.local` 和 `localStorage` 做快照以消除首屏闪烁。
|
||||||
|
修改 StorageSchema 时,必须在 `utils/chromeStorage.ts` 添加版本迁移函数,并在测试中覆盖迁移场景。
|
||||||
|
|
||||||
**通信**: 使用 `@webext-core/messaging`,协议定义在 `utils/messages.ts`。
|
**通信**: 使用 `@webext-core/messaging`,协议定义在 `utils/messages.ts`。
|
||||||
|
|
||||||
@@ -75,13 +80,21 @@ Hook。Router 同时使用 `chrome.storage.local` 和 `localStorage` 做快照
|
|||||||
- `@/utils/useLazyTranslation` (返回 `ns:key` 格式翻译)
|
- `@/utils/useLazyTranslation` (返回 `ns:key` 格式翻译)
|
||||||
- `window.matchMedia`
|
- `window.matchMedia`
|
||||||
- 测试文件命名: `__tests__/*.test.{ts,tsx}` 或 `*.test.{ts,tsx}`
|
- 测试文件命名: `__tests__/*.test.{ts,tsx}` 或 `*.test.{ts,tsx}`
|
||||||
|
- Mock 模式: 使用 `vi.mock()` 进行模块级 mock,避免在测试文件中重复 mock 代码
|
||||||
|
- 测试工具: `@testing-library/react` + `@testing-library/user-event` 进行组件测试
|
||||||
|
|
||||||
## i18n
|
## i18n
|
||||||
|
|
||||||
- 命名空间: `common` (默认), `features`
|
- 命名空间: `common` (默认), `features`
|
||||||
- 翻译键格式: `namespace:key` (如 `features:timestamp.title`)
|
- 翻译键格式: `namespace:key` (如 `features:timestamp.title`)
|
||||||
- 语言: `zh` (默认), `en`
|
- 语言: `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
|
- 添加新翻译: 编辑 `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` (测试文件除外)
|
- 禁止使用 `any` (测试文件除外)
|
||||||
- 未使用变量/参数: 使用 `_` 前缀 (如 `_unused`)
|
- 未使用变量/参数: 使用 `_` 前缀 (如 `_unused`)
|
||||||
- 样式: 使用 Tailwind CSS + shadcn/ui (通过 `className` 和 `cn()` 工具)
|
- 样式: 使用 Tailwind CSS + shadcn/ui (通过 `className` 和 `cn()` 工具)
|
||||||
|
- UI 组件: 优先使用 `components/ui/` 下的 shadcn/ui 组件 (button, dialog, select 等)
|
||||||
|
- 图标: 使用 `lucide-react` 图标库
|
||||||
- 格式: Prettier (`.prettierrc`: 100 字符宽, 单引号, 尾逗号 all, LF 换行)
|
- 格式: Prettier (`.prettierrc`: 100 字符宽, 单引号, 尾逗号 all, LF 换行)
|
||||||
- ESLint 使用 `typescript-eslint` 的 `projectService: true`(无需手动维护 project 路径)
|
- ESLint 使用 `typescript-eslint` 的 `projectService: true`(无需手动维护 project 路径)
|
||||||
|
|
||||||
@@ -109,3 +124,6 @@ Hook。Router 同时使用 `chrome.storage.local` 和 `localStorage` 做快照
|
|||||||
- TypeScript: ^5.9.3
|
- TypeScript: ^5.9.3
|
||||||
- Vitest: ^4.1.7
|
- Vitest: ^4.1.7
|
||||||
- i18next: ^26.2.0
|
- i18next: ^26.2.0
|
||||||
|
- @dnd-kit (拖拽排序)
|
||||||
|
- marked (Markdown 解析)
|
||||||
|
- qrious + qr-scanner (二维码生成与解析)
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export default function Base64ConverterSection({ mode }: Base64ConverterSectionP
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex flex-col space-y-4 animate-in fade-in duration-300">
|
<div className="w-full flex flex-col space-y-4">
|
||||||
<div className="flex h-11 items-center px-1.5 bg-secondary/40 rounded-xl border border-border/60 w-fit">
|
<div className="flex h-11 items-center px-1.5 bg-secondary/40 rounded-xl border border-border/60 w-fit">
|
||||||
<SwitchButtonGroup
|
<SwitchButtonGroup
|
||||||
value={direction}
|
value={direction}
|
||||||
@@ -86,7 +86,7 @@ export default function Base64ConverterSection({ mode }: Base64ConverterSectionP
|
|||||||
}}
|
}}
|
||||||
onClick={() => fileInputRef.current?.click()}
|
onClick={() => fileInputRef.current?.click()}
|
||||||
className={cn(
|
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
|
isDragging
|
||||||
? 'border-primary bg-primary/10'
|
? 'border-primary bg-primary/10'
|
||||||
: info
|
: info
|
||||||
@@ -107,7 +107,7 @@ export default function Base64ConverterSection({ mode }: Base64ConverterSectionP
|
|||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="w-9 h-9 border-3 border-primary/20 border-t-primary rounded-full animate-spin" />
|
<div className="w-9 h-9 border-3 border-primary/20 border-t-primary rounded-full animate-spin" />
|
||||||
) : info ? (
|
) : info ? (
|
||||||
<div className="flex flex-col items-center gap-1.5 text-center w-full animate-in fade-in duration-200">
|
<div className="flex flex-col items-center gap-1.5 text-center w-full">
|
||||||
{mode === 'image' && result && (
|
{mode === 'image' && result && (
|
||||||
<div className="relative p-1 border border-border bg-background rounded-lg shadow-sm mb-1 max-w-[180px] overflow-hidden">
|
<div className="relative p-1 border border-border bg-background rounded-lg shadow-sm mb-1 max-w-[180px] overflow-hidden">
|
||||||
<img
|
<img
|
||||||
@@ -117,9 +117,7 @@ export default function Base64ConverterSection({ mode }: Base64ConverterSectionP
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Upload
|
<Upload className={cn('w-8 h-8 text-primary')} />
|
||||||
className={cn('w-8 h-8 text-primary', mode === 'file' && 'animate-bounce')}
|
|
||||||
/>
|
|
||||||
<span className="text-sm font-bold text-foreground/90 max-w-[280px] truncate">
|
<span className="text-sm font-bold text-foreground/90 max-w-[280px] truncate">
|
||||||
{info.name}
|
{info.name}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export default function FileMode() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex flex-col space-y-4 animate-in fade-in duration-300">
|
<div className="w-full flex flex-col space-y-4">
|
||||||
<div className="flex h-11 items-center px-1.5 bg-secondary/40 rounded-xl border border-border/60 w-fit">
|
<div className="flex h-11 items-center px-1.5 bg-secondary/40 rounded-xl border border-border/60 w-fit">
|
||||||
<SwitchButtonGroup
|
<SwitchButtonGroup
|
||||||
value={direction}
|
value={direction}
|
||||||
@@ -79,7 +79,7 @@ export default function FileMode() {
|
|||||||
}}
|
}}
|
||||||
onClick={() => fileInputRef.current?.click()}
|
onClick={() => fileInputRef.current?.click()}
|
||||||
className={cn(
|
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
|
isDragging
|
||||||
? 'border-primary bg-primary/10'
|
? 'border-primary bg-primary/10'
|
||||||
: info
|
: info
|
||||||
@@ -100,7 +100,7 @@ export default function FileMode() {
|
|||||||
<div className="w-9 h-9 border-3 border-primary/20 border-t-primary rounded-full animate-spin" />
|
<div className="w-9 h-9 border-3 border-primary/20 border-t-primary rounded-full animate-spin" />
|
||||||
) : info ? (
|
) : info ? (
|
||||||
<div className="flex flex-col items-center gap-1.5 text-center">
|
<div className="flex flex-col items-center gap-1.5 text-center">
|
||||||
<Upload className="w-8 h-8 text-primary animate-bounce" />
|
<Upload className="w-8 h-8 text-primary" />
|
||||||
<span className="text-sm font-bold text-foreground/90 max-w-[280px] truncate">
|
<span className="text-sm font-bold text-foreground/90 max-w-[280px] truncate">
|
||||||
{info.name}
|
{info.name}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export default function ImageMode() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex flex-col space-y-4 animate-in fade-in duration-300">
|
<div className="w-full flex flex-col space-y-4">
|
||||||
<div className="flex h-11 items-center px-1.5 bg-secondary/40 rounded-xl border border-border/60 w-fit">
|
<div className="flex h-11 items-center px-1.5 bg-secondary/40 rounded-xl border border-border/60 w-fit">
|
||||||
<SwitchButtonGroup
|
<SwitchButtonGroup
|
||||||
value={direction}
|
value={direction}
|
||||||
@@ -82,7 +82,7 @@ export default function ImageMode() {
|
|||||||
}}
|
}}
|
||||||
onClick={() => fileInputRef.current?.click()}
|
onClick={() => fileInputRef.current?.click()}
|
||||||
className={cn(
|
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
|
isDragging
|
||||||
? 'border-primary bg-primary/10'
|
? 'border-primary bg-primary/10'
|
||||||
: info
|
: info
|
||||||
@@ -103,7 +103,7 @@ export default function ImageMode() {
|
|||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="w-9 h-9 border-3 border-primary/20 border-t-primary rounded-full animate-spin" />
|
<div className="w-9 h-9 border-3 border-primary/20 border-t-primary rounded-full animate-spin" />
|
||||||
) : info ? (
|
) : info ? (
|
||||||
<div className="flex flex-col items-center gap-1.5 text-center animate-in fade-in duration-200 w-full">
|
<div className="flex flex-col items-center gap-1.5 text-center w-full">
|
||||||
{result && (
|
{result && (
|
||||||
<div className="relative p-1 border border-border bg-background rounded-lg shadow-sm mb-1 max-w-[180px] overflow-hidden">
|
<div className="relative p-1 border border-border bg-background rounded-lg shadow-sm mb-1 max-w-[180px] overflow-hidden">
|
||||||
<img
|
<img
|
||||||
@@ -233,7 +233,7 @@ export default function ImageMode() {
|
|||||||
onClear={resetAll}
|
onClear={resetAll}
|
||||||
/>
|
/>
|
||||||
{decoded && (
|
{decoded && (
|
||||||
<div className="animate-in slide-in-from-bottom-2 duration-300">
|
<div>
|
||||||
<DecodeResultPaper
|
<DecodeResultPaper
|
||||||
title={t('decodedImageOutput')}
|
title={t('decodedImageOutput')}
|
||||||
mimeType={decoded.mimeType}
|
mimeType={decoded.mimeType}
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export default function TextMode({ onSwitchToImageMode }: TextModeProps = {}) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex flex-col space-y-4 animate-in fade-in duration-300">
|
<div className="w-full flex flex-col space-y-4">
|
||||||
{/* 受控方向切流中枢 */}
|
{/* 受控方向切流中枢 */}
|
||||||
<div className="flex h-11 items-center px-1.5 bg-secondary/40 rounded-xl border border-border/60 w-fit">
|
<div className="flex h-11 items-center px-1.5 bg-secondary/40 rounded-xl border border-border/60 w-fit">
|
||||||
<SwitchButtonGroup
|
<SwitchButtonGroup
|
||||||
@@ -125,7 +125,7 @@ export default function TextMode({ onSwitchToImageMode }: TextModeProps = {}) {
|
|||||||
- 完美向全站 shadcn 暗黑生态看齐,采用标准的 bg-primary/10 混合变体。
|
- 完美向全站 shadcn 暗黑生态看齐,采用标准的 bg-primary/10 混合变体。
|
||||||
*/}
|
*/}
|
||||||
{showImageHint && (
|
{showImageHint && (
|
||||||
<div className="flex items-center justify-between p-3.5 rounded-xl bg-primary/10 border border-primary/20 animate-in slide-in-from-top-1 duration-200">
|
<div className="flex items-center justify-between p-3.5 rounded-xl bg-primary/10 border border-primary/20">
|
||||||
<span className="text-xs font-semibold text-primary tracking-tight">
|
<span className="text-xs font-semibold text-primary tracking-tight">
|
||||||
{t('imageDataUriHint')}
|
{t('imageDataUriHint')}
|
||||||
</span>
|
</span>
|
||||||
@@ -134,7 +134,7 @@ export default function TextMode({ onSwitchToImageMode }: TextModeProps = {}) {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={onSwitchToImageMode}
|
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')}
|
{t('switchToImageMode')}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -143,7 +143,7 @@ export default function TextMode({ onSwitchToImageMode }: TextModeProps = {}) {
|
|||||||
|
|
||||||
{/* 5. 编码/解码核心数据承载流卡片 */}
|
{/* 5. 编码/解码核心数据承载流卡片 */}
|
||||||
{output && (
|
{output && (
|
||||||
<div className="p-4 rounded-2xl bg-card border border-border shadow-sm flex flex-col space-y-3 animate-in slide-in-from-bottom-2 duration-300">
|
<div className="p-4 rounded-2xl bg-card border border-border shadow-sm flex flex-col space-y-3">
|
||||||
<div className="flex justify-between items-center select-none">
|
<div className="flex justify-between items-center select-none">
|
||||||
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground/90">
|
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground/90">
|
||||||
{outputLabel}
|
{outputLabel}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export default function Index() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 w-full flex flex-col space-y-4 min-h-[520px] select-none animate-in fade-in duration-300">
|
<div className="p-4 w-full flex flex-col space-y-4 min-h-[520px] select-none">
|
||||||
<SwitchButtonGroup
|
<SwitchButtonGroup
|
||||||
value={pageMode}
|
value={pageMode}
|
||||||
options={[
|
options={[
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ export default function ToolCard({
|
|||||||
*/
|
*/
|
||||||
className={cn(
|
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',
|
'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',
|
||||||
'transition-all duration-300 ease-[cubic-bezier(0.4,0,0.2,1)]',
|
|
||||||
'hover:bg-muted/30',
|
'hover:bg-muted/30',
|
||||||
'hover:border-[rgba(var(--tool-color),0.45)]',
|
'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)]',
|
'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)]',
|
||||||
@@ -60,7 +59,7 @@ export default function ToolCard({
|
|||||||
{/* 左侧圆形图标容器 */}
|
{/* 左侧圆形图标容器 */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center justify-center w-10 h-10 rounded-xl shrink-0 transition-colors duration-300',
|
'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)]',
|
'bg-[rgba(var(--tool-color),0.08)] dark:bg-[rgba(var(--tool-color),0.12)]',
|
||||||
'text-[rgb(var(--tool-color))]',
|
'text-[rgb(var(--tool-color))]',
|
||||||
)}
|
)}
|
||||||
@@ -82,7 +81,7 @@ export default function ToolCard({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 右侧指示小箭头 */}
|
{/* 右侧指示小箭头 */}
|
||||||
<div className="text-muted-foreground/40 group-hover:text-[rgb(var(--tool-color))] p-1 shrink-0 transition-all duration-300 ease-in-out group-hover:translate-x-0.5">
|
<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" />
|
<ChevronRight className="h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export default function DashboardPage() {
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'grid grid-cols-1 sm:grid-cols-[repeat(auto-fill,minmax(290px,1fr))] auto-rows-auto gap-3.5 p-3.5 w-full h-auto',
|
'grid grid-cols-1 sm:grid-cols-[repeat(auto-fill,minmax(290px,1fr))] auto-rows-auto gap-3.5 p-3.5 w-full h-auto',
|
||||||
'animate-in fade-in duration-300 select-none',
|
'select-none',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{pageOrder.map((key) => {
|
{pageOrder.map((key) => {
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export default function HtmlToMarkdownPage() {
|
|||||||
- 彻底清除多余的 container max-w-7xl 这种网页大边距,
|
- 彻底清除多余的 container max-w-7xl 这种网页大边距,
|
||||||
- 统一收拢为我们先前在 Dashboard 页、JSON 工具箱制定的 p-4 space-y-4 标准极客桌面规格。
|
- 统一收拢为我们先前在 Dashboard 页、JSON 工具箱制定的 p-4 space-y-4 标准极客桌面规格。
|
||||||
*/
|
*/
|
||||||
<div className="p-4 w-full flex flex-col space-y-4 select-none animate-in fade-in duration-300">
|
<div className="p-4 w-full flex flex-col space-y-4 select-none">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{/* 工具栏集成区 */}
|
{/* 工具栏集成区 */}
|
||||||
<div className="flex flex-col sm:flex-row gap-3 justify-between items-stretch sm:items-center bg-secondary/40 rounded-xl border border-border/60 px-1.5 py-1.5 sm:h-12">
|
<div className="flex flex-col sm:flex-row gap-3 justify-between items-stretch sm:items-center bg-secondary/40 rounded-xl border border-border/60 px-1.5 py-1.5 sm:h-12">
|
||||||
@@ -72,7 +72,7 @@ export default function HtmlToMarkdownPage() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleDownload}
|
onClick={handleDownload}
|
||||||
disabled={!result.markdown}
|
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"
|
||||||
>
|
>
|
||||||
<Download className="h-3.5 w-3.5" />
|
<Download className="h-3.5 w-3.5" />
|
||||||
{t('download')}
|
{t('download')}
|
||||||
@@ -83,7 +83,7 @@ export default function HtmlToMarkdownPage() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleClear}
|
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"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-3.5 w-3.5" />
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
{t('clear')}
|
{t('clear')}
|
||||||
@@ -95,7 +95,7 @@ export default function HtmlToMarkdownPage() {
|
|||||||
- 💡 核心修复点:将硬编码的 bg-red-50 实色,完美超进化为系统的全自适应透明色变体
|
- 💡 核心修复点:将硬编码的 bg-red-50 实色,完美超进化为系统的全自适应透明色变体
|
||||||
*/}
|
*/}
|
||||||
{error && (
|
{error && (
|
||||||
<div className="p-3.5 bg-destructive/10 border border-destructive/20 rounded-xl text-destructive text-xs font-semibold tracking-wide animate-in shake duration-300">
|
<div className="p-3.5 bg-destructive/10 border border-destructive/20 rounded-xl text-destructive text-xs font-semibold tracking-wide">
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -114,7 +114,7 @@ export default function HtmlToMarkdownPage() {
|
|||||||
- 只要用户用鼠标点击了内部的 textarea,外层整块精巧的圆角大边框会一帧内亮起 primary 系统的深色呼吸发光环,
|
- 只要用户用鼠标点击了内部的 textarea,外层整块精巧的圆角大边框会一帧内亮起 primary 系统的深色呼吸发光环,
|
||||||
- 这种“全外包裹层框聚焦”的体验极大模仿了本地原生 IDE 的硬核专业体验!
|
- 这种“全外包裹层框聚焦”的体验极大模仿了本地原生 IDE 的硬核专业体验!
|
||||||
*/
|
*/
|
||||||
<div className="border border-border rounded-xl bg-card text-card-foreground shadow-sm overflow-hidden flex flex-col transition-all duration-200 focus-within:ring-1 focus-within:ring-ring focus-within:border-ring">
|
<div className="border border-border rounded-xl bg-card text-card-foreground shadow-sm overflow-hidden flex flex-col focus-within:ring-1 focus-within:ring-ring focus-within:border-ring">
|
||||||
{/* 卡片头部:改用标准的灰色 bg-muted/50 */}
|
{/* 卡片头部:改用标准的灰色 bg-muted/50 */}
|
||||||
<div className="flex h-9 items-center justify-between px-4 bg-muted/50 border-b border-border select-none">
|
<div className="flex h-9 items-center justify-between px-4 bg-muted/50 border-b border-border select-none">
|
||||||
<span className="text-[10px] font-bold text-muted-foreground/90 uppercase tracking-wider">
|
<span className="text-[10px] font-bold text-muted-foreground/90 uppercase tracking-wider">
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export default function DiffNavigator({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center justify-center gap-3 px-4 py-2 rounded-lg border border-border bg-muted/30 select-none animate-in fade-in duration-200',
|
'flex items-center justify-center gap-3 px-4 py-2 rounded-lg border border-border bg-muted/30 select-none',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -60,9 +60,9 @@ export default function DiffNavigator({
|
|||||||
aria-label={t('jsonDiff:previousDiff')}
|
aria-label={t('jsonDiff:previousDiff')}
|
||||||
onClick={onPrev}
|
onClick={onPrev}
|
||||||
className={cn(
|
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',
|
'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. 边界拦截:触顶时优雅淡化并锁死点击
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ChevronLeft className="h-4 w-4" />
|
<ChevronLeft className="h-4 w-4" />
|
||||||
@@ -81,9 +81,9 @@ export default function DiffNavigator({
|
|||||||
aria-label={t('jsonDiff:nextDiff')}
|
aria-label={t('jsonDiff:nextDiff')}
|
||||||
onClick={onNext}
|
onClick={onNext}
|
||||||
className={cn(
|
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',
|
'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. 边界拦截:触底时优雅淡化并锁死点击
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ChevronRight className="h-4 w-4" />
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ const UnifiedRow = ({ depth, type, text, active, multiline }: UnifiedRowProps) =
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-start w-full font-mono py-0.5 select-text group transition-colors',
|
'flex items-start w-full font-mono py-0.5 select-text group',
|
||||||
currentTheme.bg,
|
currentTheme.bg,
|
||||||
currentTheme.text,
|
currentTheme.text,
|
||||||
// 4. 高亮定位条:不再使用生硬的蓝圆环,改为现代编辑器的“侧边左高亮带”设计,质感直接拉满
|
// 4. 高亮定位条:不再使用生硬的蓝圆环,改为现代编辑器的“侧边左高亮带”设计,质感直接拉满
|
||||||
|
|||||||
@@ -74,10 +74,7 @@ export default function JsonConvertSection({
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={cn('w-full flex flex-col gap-4', className)} {...props}>
|
||||||
className={cn('w-full flex flex-col gap-4 animate-in fade-in duration-300', className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{/* 输入区 */}
|
{/* 输入区 */}
|
||||||
<TextInputArea
|
<TextInputArea
|
||||||
placeholder={t(`jsonFormat:${pk}InputPlaceholder`)}
|
placeholder={t(`jsonFormat:${pk}InputPlaceholder`)}
|
||||||
@@ -93,7 +90,7 @@ export default function JsonConvertSection({
|
|||||||
|
|
||||||
{/* 4. 结果展示或状态引导卡片区 */}
|
{/* 4. 结果展示或状态引导卡片区 */}
|
||||||
{result && result.output ? (
|
{result && result.output ? (
|
||||||
<div className="relative rounded-xl border border-border bg-card text-card-foreground shadow-sm overflow-hidden animate-in slide-in-from-bottom-2 duration-300">
|
<div className="relative rounded-xl border border-border bg-card text-card-foreground shadow-sm overflow-hidden">
|
||||||
{/* 结果栏精致头部 */}
|
{/* 结果栏精致头部 */}
|
||||||
<div className="flex h-9 items-center justify-between px-4 border-b border-border bg-muted/50 select-none">
|
<div className="flex h-9 items-center justify-between px-4 border-b border-border bg-muted/50 select-none">
|
||||||
<div className="flex gap-4 items-center">
|
<div className="flex gap-4 items-center">
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export default function JsonFormatSection() {
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex flex-col gap-4 animate-in fade-in duration-300">
|
<div className="w-full flex flex-col gap-4">
|
||||||
{/* 工具控制栏 */}
|
{/* 工具控制栏 */}
|
||||||
<div className="flex h-10 items-center justify-between px-1.5 bg-secondary/40 rounded-xl border border-border/60">
|
<div className="flex h-10 items-center justify-between px-1.5 bg-secondary/40 rounded-xl border border-border/60">
|
||||||
<div className="flex gap-4 items-center w-full">
|
<div className="flex gap-4 items-center w-full">
|
||||||
@@ -95,7 +95,7 @@ export default function JsonFormatSection() {
|
|||||||
/>
|
/>
|
||||||
<Label
|
<Label
|
||||||
htmlFor="sort-keys-checkbox"
|
htmlFor="sort-keys-checkbox"
|
||||||
className="text-xs font-bold text-foreground/80 cursor-pointer tracking-tight group-hover:text-foreground transition-colors"
|
className="text-xs font-bold text-foreground/80 cursor-pointer tracking-tight group-hover:text-foreground"
|
||||||
>
|
>
|
||||||
{t('jsonFormat:sortKeys')}
|
{t('jsonFormat:sortKeys')}
|
||||||
</Label>
|
</Label>
|
||||||
@@ -118,7 +118,7 @@ export default function JsonFormatSection() {
|
|||||||
|
|
||||||
{/* 格式化结果流面板展示 */}
|
{/* 格式化结果流面板展示 */}
|
||||||
{result && result.formatted ? (
|
{result && result.formatted ? (
|
||||||
<div className="relative rounded-xl border border-border bg-card text-card-foreground shadow-sm overflow-hidden animate-in slide-in-from-bottom-2 duration-300">
|
<div className="relative rounded-xl border border-border bg-card text-card-foreground shadow-sm overflow-hidden">
|
||||||
{/* 结果栏头部 */}
|
{/* 结果栏头部 */}
|
||||||
<div className="flex h-9 items-center justify-between px-4 border-b border-border bg-muted/50 select-none">
|
<div className="flex h-9 items-center justify-between px-4 border-b border-border bg-muted/50 select-none">
|
||||||
<div className="flex gap-4 items-center">
|
<div className="flex gap-4 items-center">
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ const NodeRow = React.memo(
|
|||||||
<div
|
<div
|
||||||
onClick={() => setOverride(expanded ? 'closed' : 'open')}
|
onClick={() => setOverride(expanded ? 'closed' : 'open')}
|
||||||
className={cn(
|
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,
|
theme.bg,
|
||||||
isActive &&
|
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',
|
'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(
|
|||||||
<div
|
<div
|
||||||
ref={rowRef}
|
ref={rowRef}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-1 py-0.5 pr-2 font-mono text-xs w-full h-[22px] leading-relaxed rounded-sm transition-colors',
|
'flex items-center gap-1 py-0.5 pr-2 font-mono text-xs w-full h-[22px] leading-relaxed rounded-sm',
|
||||||
theme.bg,
|
theme.bg,
|
||||||
isActive &&
|
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',
|
'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',
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ export default function Index() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 w-full flex flex-col space-y-4 min-h-[500px] select-none animate-in fade-in duration-300">
|
<div className="p-4 w-full flex flex-col space-y-4 min-h-[500px] select-none">
|
||||||
<SwitchButtonGroup
|
<SwitchButtonGroup
|
||||||
value={pageMode}
|
value={pageMode}
|
||||||
onChange={(v: PageMode) => setPageMode(v)}
|
onChange={(v: PageMode) => setPageMode(v)}
|
||||||
@@ -129,7 +129,7 @@ export default function Index() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{pageMode === 'diff' ? (
|
{pageMode === 'diff' ? (
|
||||||
<div className="flex flex-col space-y-4 animate-in fade-in duration-200">
|
<div className="flex flex-col space-y-4">
|
||||||
<div className="flex h-10 items-center justify-between px-1.5 bg-secondary/40 rounded-xl border border-border/60">
|
<div className="flex h-10 items-center justify-between px-1.5 bg-secondary/40 rounded-xl border border-border/60">
|
||||||
<SwitchButtonGroup
|
<SwitchButtonGroup
|
||||||
value={viewMode}
|
value={viewMode}
|
||||||
|
|||||||
+4
-6
@@ -17,9 +17,7 @@ interface SectionProps {
|
|||||||
const Section = ({ title, content, colorClass, bgClass, borderClass }: SectionProps) => {
|
const Section = ({ title, content, colorClass, bgClass, borderClass }: SectionProps) => {
|
||||||
const { t } = useLazyTranslation('jwt');
|
const { t } = useLazyTranslation('jwt');
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={cn('p-4 rounded-xl border border-solid', bgClass, borderClass)}>
|
||||||
className={cn('p-4 rounded-xl border border-solid transition-colors', bgClass, borderClass)}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between items-center mb-2 select-none">
|
<div className="flex justify-between items-center mb-2 select-none">
|
||||||
<span className={cn('text-xs font-bold tracking-wider uppercase', colorClass)}>
|
<span className={cn('text-xs font-bold tracking-wider uppercase', colorClass)}>
|
||||||
{title}
|
{title}
|
||||||
@@ -70,7 +68,7 @@ export default function Index() {
|
|||||||
}, [debouncedInput]);
|
}, [debouncedInput]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 w-full flex flex-col space-y-4 animate-in fade-in duration-300 select-none">
|
<div className="p-4 w-full flex flex-col space-y-4 select-none">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{/* 输入终端 */}
|
{/* 输入终端 */}
|
||||||
<TextInputArea
|
<TextInputArea
|
||||||
@@ -90,7 +88,7 @@ export default function Index() {
|
|||||||
|
|
||||||
{/* 解码看板结果展现 */}
|
{/* 解码看板结果展现 */}
|
||||||
{result && !result.error && (
|
{result && !result.error && (
|
||||||
<div className="flex flex-col gap-4 animate-in slide-in-from-bottom-2 duration-300">
|
<div className="flex flex-col gap-4">
|
||||||
{/* Header 分区:完美致敬 JWT.io 的鲜艳色彩,同时实现黑夜暗化自适应 */}
|
{/* Header 分区:完美致敬 JWT.io 的鲜艳色彩,同时实现黑夜暗化自适应 */}
|
||||||
<Section
|
<Section
|
||||||
title={t('jwt:headerTitle')}
|
title={t('jwt:headerTitle')}
|
||||||
@@ -110,7 +108,7 @@ export default function Index() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Signature 签名区:完全对齐标准的 shadcn 骨架阶度 */}
|
{/* Signature 签名区:完全对齐标准的 shadcn 骨架阶度 */}
|
||||||
<div className="p-4 rounded-xl border border-border bg-secondary/40 shadow-sm transition-colors">
|
<div className="p-4 rounded-xl border border-border bg-secondary/40 shadow-sm">
|
||||||
<div className="flex justify-between items-center mb-2">
|
<div className="flex justify-between items-center mb-2">
|
||||||
<span className="text-xs font-bold tracking-wider text-muted-foreground/90 uppercase">
|
<span className="text-xs font-bold tracking-wider text-muted-foreground/90 uppercase">
|
||||||
{t('jwt:signatureTitle')}
|
{t('jwt:signatureTitle')}
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ export default function MarkdownToHtmlPage() {
|
|||||||
const showPreview = previewMode !== 'html';
|
const showPreview = previewMode !== 'html';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 w-full flex flex-col space-y-4 select-none animate-in fade-in duration-300">
|
<div className="p-4 w-full flex flex-col space-y-4 select-none">
|
||||||
<div className="flex flex-col space-y-4">
|
<div className="flex flex-col space-y-4">
|
||||||
{/* 工具集成控制中枢 */}
|
{/* 工具集成控制中枢 */}
|
||||||
<div className="flex flex-col sm:flex-row gap-3 justify-between items-stretch sm:items-center bg-secondary/40 rounded-xl border border-border/60 px-1.5 py-1.5 sm:h-12">
|
<div className="flex flex-col sm:flex-row gap-3 justify-between items-stretch sm:items-center bg-secondary/40 rounded-xl border border-border/60 px-1.5 py-1.5 sm:h-12">
|
||||||
@@ -196,7 +196,7 @@ export default function MarkdownToHtmlPage() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleClear}
|
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"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-3.5 w-3.5" />
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
{t('clear')}
|
{t('clear')}
|
||||||
@@ -206,7 +206,7 @@ export default function MarkdownToHtmlPage() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={handlePrint}
|
onClick={handlePrint}
|
||||||
disabled={!result.html}
|
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"
|
||||||
>
|
>
|
||||||
<Printer className="h-3.5 w-3.5" />
|
<Printer className="h-3.5 w-3.5" />
|
||||||
{t('print')}
|
{t('print')}
|
||||||
@@ -216,7 +216,7 @@ export default function MarkdownToHtmlPage() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleDownload}
|
onClick={handleDownload}
|
||||||
disabled={!result.html}
|
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"
|
||||||
>
|
>
|
||||||
<Download className="h-3.5 w-3.5" />
|
<Download className="h-3.5 w-3.5" />
|
||||||
{t('download')}
|
{t('download')}
|
||||||
@@ -228,7 +228,7 @@ export default function MarkdownToHtmlPage() {
|
|||||||
{error && (
|
{error && (
|
||||||
<div
|
<div
|
||||||
role="alert"
|
role="alert"
|
||||||
className="p-3.5 bg-destructive/10 border border-destructive/20 rounded-xl text-destructive text-xs font-semibold tracking-wide animate-in shake duration-300"
|
className="p-3.5 bg-destructive/10 border border-destructive/20 rounded-xl text-destructive text-xs font-semibold tracking-wide"
|
||||||
>
|
>
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
@@ -243,7 +243,7 @@ export default function MarkdownToHtmlPage() {
|
|||||||
>
|
>
|
||||||
{/* Markdown 输入翼终端 */}
|
{/* Markdown 输入翼终端 */}
|
||||||
{showInput && (
|
{showInput && (
|
||||||
<div className="border border-border rounded-xl bg-card text-card-foreground shadow-sm overflow-hidden flex flex-col transition-all duration-200 focus-within:ring-1 focus-within:ring-ring focus-within:border-ring animate-in fade-in">
|
<div className="border border-border rounded-xl bg-card text-card-foreground shadow-sm overflow-hidden flex flex-col focus-within:ring-1 focus-within:ring-ring focus-within:border-ring">
|
||||||
<div className="flex h-9 items-center justify-between px-4 bg-muted/50 border-b border-border select-none">
|
<div className="flex h-9 items-center justify-between px-4 bg-muted/50 border-b border-border select-none">
|
||||||
<span className="text-[10px] font-bold text-muted-foreground/90 uppercase tracking-wider">
|
<span className="text-[10px] font-bold text-muted-foreground/90 uppercase tracking-wider">
|
||||||
{t('inputLabel')}
|
{t('inputLabel')}
|
||||||
@@ -263,7 +263,7 @@ export default function MarkdownToHtmlPage() {
|
|||||||
|
|
||||||
{/* 实时 HTML/Iframe 预览翼终端 */}
|
{/* 实时 HTML/Iframe 预览翼终端 */}
|
||||||
{showPreview && (
|
{showPreview && (
|
||||||
<div className="border border-border rounded-xl bg-card text-card-foreground shadow-sm overflow-hidden flex flex-col animate-in fade-in">
|
<div className="border border-border rounded-xl bg-card text-card-foreground shadow-sm overflow-hidden flex flex-col">
|
||||||
<div className="flex h-9 items-center justify-between px-4 bg-muted/50 border-b border-border select-none">
|
<div className="flex h-9 items-center justify-between px-4 bg-muted/50 border-b border-border select-none">
|
||||||
<span className="text-[10px] font-bold text-muted-foreground/90 uppercase tracking-wider">
|
<span className="text-[10px] font-bold text-muted-foreground/90 uppercase tracking-wider">
|
||||||
{(previewMode as string) === 'html' ? t('htmlOutputLabel') : t('previewLabel')}
|
{(previewMode as string) === 'html' ? t('htmlOutputLabel') : t('previewLabel')}
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ export default function GeneratePanel() {
|
|||||||
const { generatorState, setTextToEncode, downloadQrCode, copyQrCode } = useQrCodeContext();
|
const { generatorState, setTextToEncode, downloadQrCode, copyQrCode } = useQrCodeContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 w-full items-stretch select-none p-0.5 animate-in fade-in duration-300">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 w-full items-stretch select-none p-0.5">
|
||||||
{/* 左翼:高性能受控输入翼终端 */}
|
{/* 左翼:高性能受控输入翼终端 */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'border border-border rounded-xl bg-card text-card-foreground shadow-sm flex flex-col p-4 transition-all duration-200',
|
'border border-border rounded-xl bg-card text-card-foreground shadow-sm flex flex-col p-4',
|
||||||
'focus-within:ring-1 focus-within:ring-ring focus-within:border-ring',
|
'focus-within:ring-1 focus-within:ring-ring focus-within:border-ring',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export default function ParsePanel() {
|
|||||||
/* 💡 统一大视觉轴:
|
/* 💡 统一大视觉轴:
|
||||||
- 追加 p-0.5 微隔离,配合 gap-6 建立与生成面板(GeneratePanel)绝对像素对齐的网格天平。
|
- 追加 p-0.5 微隔离,配合 gap-6 建立与生成面板(GeneratePanel)绝对像素对齐的网格天平。
|
||||||
*/
|
*/
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 w-full items-stretch select-none p-0.5 animate-in fade-in duration-300">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 w-full items-stretch select-none p-0.5">
|
||||||
{/* 左翼:图片接收/拖拽/剪贴板上传终端 */}
|
{/* 左翼:图片接收/拖拽/剪贴板上传终端 */}
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<ImageUploader
|
<ImageUploader
|
||||||
@@ -78,7 +78,7 @@ export default function ParsePanel() {
|
|||||||
{/* 右翼:高阶解析出码只读终端 */}
|
{/* 右翼:高阶解析出码只读终端 */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'border border-border rounded-xl bg-card text-card-foreground shadow-sm flex flex-col p-4 transition-all duration-200',
|
'border border-border rounded-xl bg-card text-card-foreground shadow-sm flex flex-col p-4',
|
||||||
// 💡 视觉对称增强:加入相同的聚焦变量环联动,使双翼权重达成完美绝对平衡
|
// 💡 视觉对称增强:加入相同的聚焦变量环联动,使双翼权重达成完美绝对平衡
|
||||||
'focus-within:ring-1 focus-within:ring-ring focus-within:border-ring',
|
'focus-within:ring-1 focus-within:ring-ring focus-within:border-ring',
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export default function Index() {
|
|||||||
- 彻底剥离破坏流式宽度的 max-w-[400px] 枷锁,开启标准的 w-full 全自适应包裹。
|
- 彻底剥离破坏流式宽度的 max-w-[400px] 枷锁,开启标准的 w-full 全自适应包裹。
|
||||||
- 替换为标准的 p-4 呼吸内边距配合 flex flex-col space-y-4,接管系统级重排!
|
- 替换为标准的 p-4 呼吸内边距配合 flex flex-col space-y-4,接管系统级重排!
|
||||||
*/}
|
*/}
|
||||||
<div className="p-4 w-full flex flex-col space-y-4 min-h-[500px] select-none animate-in fade-in duration-300">
|
<div className="p-4 w-full flex flex-col space-y-4 min-h-[500px] select-none">
|
||||||
{/* 流式中央控制切流卡:注入 sm 断点防御,防范单栏状态下发生变形 */}
|
{/* 流式中央控制切流卡:注入 sm 断点防御,防范单栏状态下发生变形 */}
|
||||||
<div className="w-full sm:w-fit pt-0.5">
|
<div className="w-full sm:w-fit pt-0.5">
|
||||||
<SwitchButtonGroup
|
<SwitchButtonGroup
|
||||||
@@ -40,11 +40,11 @@ export default function Index() {
|
|||||||
*/}
|
*/}
|
||||||
<div className="w-full pt-1.5">
|
<div className="w-full pt-1.5">
|
||||||
{qrCode.mode === 'generate' ? (
|
{qrCode.mode === 'generate' ? (
|
||||||
<div className="animate-in fade-in duration-200">
|
<div>
|
||||||
<GeneratePanel />
|
<GeneratePanel />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="animate-in fade-in duration-200">
|
<div>
|
||||||
<ParsePanel />
|
<ParsePanel />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default function RightClickRestorerPage() {
|
|||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center py-12 min-h-[280px] w-full animate-in fade-in duration-200">
|
<div className="flex flex-col items-center justify-center py-12 min-h-[280px] w-full">
|
||||||
<span className="text-xs text-muted-foreground mt-2 font-medium tracking-wide">
|
<span className="text-xs text-muted-foreground mt-2 font-medium tracking-wide">
|
||||||
{t('rightClickRestorer:loading')}
|
{t('rightClickRestorer:loading')}
|
||||||
</span>
|
</span>
|
||||||
@@ -20,9 +20,9 @@ export default function RightClickRestorerPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 w-full flex flex-col space-y-4 animate-in fade-in duration-300">
|
<div className="p-4 w-full flex flex-col space-y-4">
|
||||||
{/* Current Domain */}
|
{/* Current Domain */}
|
||||||
<div className="w-full rounded-xl border border-border bg-card text-card-foreground shadow-sm overflow-hidden transition-all">
|
<div className="w-full rounded-xl border border-border bg-card text-card-foreground shadow-sm overflow-hidden">
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<Label className="text-sm font-medium">{t('rightClickRestorer:currentDomain')}</Label>
|
<Label className="text-sm font-medium">{t('rightClickRestorer:currentDomain')}</Label>
|
||||||
<div className="mt-2 flex items-center justify-between gap-2">
|
<div className="mt-2 flex items-center justify-between gap-2">
|
||||||
@@ -50,7 +50,7 @@ export default function RightClickRestorerPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Unlock Action */}
|
{/* Unlock Action */}
|
||||||
<div className="w-full rounded-xl border border-border bg-card text-card-foreground shadow-sm overflow-hidden transition-all">
|
<div className="w-full rounded-xl border border-border bg-card text-card-foreground shadow-sm overflow-hidden">
|
||||||
<div className="p-4 space-y-3">
|
<div className="p-4 space-y-3">
|
||||||
{isUnsupported ? (
|
{isUnsupported ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export default function AutoRefreshToggle({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-full p-4 rounded-xl border border-border bg-card text-card-foreground shadow-sm transition-all focus-within:ring-1 focus-within:ring-ring flex justify-between items-center',
|
'w-full p-4 rounded-xl border border-border bg-card text-card-foreground shadow-sm flex justify-between items-center',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -17,10 +17,7 @@ export default function CleaningResult({ result, className, ...props }: Cleaning
|
|||||||
const isSuccess = result.success;
|
const isSuccess = result.success;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={cn('w-full', className)} {...props}>
|
||||||
className={cn('animate-in fade-in slide-in-from-top-1 duration-200 w-full', className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{/* 2. 彻底重构容器类名结构:
|
{/* 2. 彻底重构容器类名结构:
|
||||||
- 成功状态:采用 Tailwind 官方推荐的 emerald 体系,利用 /10 (10% 透明度) 和 /20 (边框)。
|
- 成功状态:采用 Tailwind 官方推荐的 emerald 体系,利用 /10 (10% 透明度) 和 /20 (边框)。
|
||||||
- 失败状态:完全放权给标准的 border-destructive/20 和 bg-destructive/5。
|
- 失败状态:完全放权给标准的 border-destructive/20 和 bg-destructive/5。
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export default function ErrorDisplay({ error, className, ...props }: ErrorDispla
|
|||||||
// 1. 精简层级:单层外壳直接搞定居中、响应式高度与外部类名扩展
|
// 1. 精简层级:单层外壳直接搞定居中、响应式高度与外部类名扩展
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex flex-col items-center justify-center py-8 min-h-[240px] sm:min-h-[360px] p-4 text-center animate-in fade-in zoom-in-95 duration-200',
|
'flex flex-col items-center justify-center py-8 min-h-[240px] sm:min-h-[360px] p-4 text-center',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -25,7 +25,7 @@ export default function ErrorDisplay({ error, className, ...props }: ErrorDispla
|
|||||||
*/}
|
*/}
|
||||||
<div className="w-full max-w-xs flex flex-col items-center justify-center rounded-xl p-5 border border-destructive/20 bg-destructive/5 shadow-sm">
|
<div className="w-full max-w-xs flex flex-col items-center justify-center rounded-xl p-5 border border-destructive/20 bg-destructive/5 shadow-sm">
|
||||||
{/* 3. 图标与主要错误信息全面对接 text-destructive 语义色 */}
|
{/* 3. 图标与主要错误信息全面对接 text-destructive 语义色 */}
|
||||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-destructive/10 text-destructive mb-3.5 shrink-0 animate-bounce [animation-duration:2s]">
|
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-destructive/10 text-destructive mb-3.5 shrink-0">
|
||||||
<AlertCircle className="h-5 w-5" />
|
<AlertCircle className="h-5 w-5" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export default function OptionItem({
|
|||||||
// 3. 跨越级交互升级:将外部容器升级为一个高度敏感的可点击 Tab 热区
|
// 3. 跨越级交互升级:将外部容器升级为一个高度敏感的可点击 Tab 热区
|
||||||
onClick={onChange}
|
onClick={onChange}
|
||||||
className={cn(
|
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. 彻底抛弃硬编码黄底:
|
// 4. 彻底抛弃硬编码黄底:
|
||||||
// - 选中时:使用 bg-primary/5 (系统主色超淡叠加) 配合标准 border-primary/30。
|
// - 选中时:使用 bg-primary/5 (系统主色超淡叠加) 配合标准 border-primary/30。
|
||||||
// - 未选中时:保持透明 border-transparent,悬停呈现 bg-muted。
|
// - 未选中时:保持透明 border-transparent,悬停呈现 bg-muted。
|
||||||
@@ -45,7 +45,7 @@ export default function OptionItem({
|
|||||||
<div className="flex-1 min-w-0 mr-4">
|
<div className="flex-1 min-w-0 mr-4">
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'block text-xs font-semibold leading-tight truncate transition-colors',
|
'block text-xs font-semibold leading-tight truncate',
|
||||||
checked ? 'text-foreground font-bold' : 'text-foreground/80',
|
checked ? 'text-foreground font-bold' : 'text-foreground/80',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ export function StorageCleanerConfirm({
|
|||||||
<DialogContent
|
<DialogContent
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-[90%] max-w-[340px] p-6 gap-0 rounded-2xl overflow-hidden shadow-xl border border-border bg-card text-card-foreground',
|
'w-[90%] max-w-[340px] p-6 gap-0 rounded-2xl overflow-hidden shadow-xl border border-border bg-card text-card-foreground',
|
||||||
'animate-in fade-in-50 zoom-in-95 duration-200',
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* 头部标题区域 */}
|
{/* 头部标题区域 */}
|
||||||
@@ -72,7 +71,7 @@ export function StorageCleanerConfirm({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 风险警告横幅 */}
|
{/* 风险警告横幅 */}
|
||||||
<div className="inline-flex items-center justify-center gap-1.5 px-3.5 py-2 rounded-lg bg-destructive/5 border border-dashed border-destructive/20 w-full max-w-[240px] animate-pulse [animation-duration:3s]">
|
<div className="inline-flex items-center justify-center gap-1.5 px-3.5 py-2 rounded-lg bg-destructive/5 border border-dashed border-destructive/20 w-full max-w-[240px]">
|
||||||
<AlertTriangle className="h-3.5 w-3.5 text-destructive shrink-0" />
|
<AlertTriangle className="h-3.5 w-3.5 text-destructive shrink-0" />
|
||||||
<span className="text-[11px] font-bold text-destructive leading-none tracking-tight">
|
<span className="text-[11px] font-bold text-destructive leading-none tracking-tight">
|
||||||
{t('storageCleaner:irreversible')}
|
{t('storageCleaner:irreversible')}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export default function StorageOptionsGrid({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-full rounded-xl border border-border bg-card text-card-foreground shadow-sm overflow-hidden transition-all',
|
'w-full rounded-xl border border-border bg-card text-card-foreground shadow-sm overflow-hidden',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -75,7 +75,7 @@ export default function StorageOptionsGrid({
|
|||||||
*/}
|
*/}
|
||||||
<div
|
<div
|
||||||
onClick={handleToggleAll}
|
onClick={handleToggleAll}
|
||||||
className="border-t border-border flex justify-between items-center px-4 py-2.5 bg-muted/20 hover:bg-muted/50 transition-colors cursor-pointer select-none"
|
className="border-t border-border flex justify-between items-center px-4 py-2.5 bg-muted/20 hover:bg-muted/50 cursor-pointer select-none"
|
||||||
>
|
>
|
||||||
<Label className="text-xs font-bold text-muted-foreground/90 cursor-pointer">
|
<Label className="text-xs font-bold text-muted-foreground/90 cursor-pointer">
|
||||||
{t('storageCleaner:selectAll')}
|
{t('storageCleaner:selectAll')}
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ export default function Index() {
|
|||||||
|
|
||||||
if (isInitializing) {
|
if (isInitializing) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center py-12 min-h-[280px] w-full animate-in fade-in duration-200">
|
<div className="flex flex-col items-center justify-center py-12 min-h-[280px] w-full">
|
||||||
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground/80" />
|
<Loader2 className="h-6 w-6 text-muted-foreground/80" />
|
||||||
<span className="text-xs text-muted-foreground mt-2 font-medium tracking-wide">
|
<span className="text-xs text-muted-foreground mt-2 font-medium tracking-wide">
|
||||||
正在读取站点数据...
|
正在读取站点数据...
|
||||||
</span>
|
</span>
|
||||||
@@ -47,7 +47,7 @@ export default function Index() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 w-full flex flex-col space-y-3.5 animate-in fade-in duration-300">
|
<div className="p-4 w-full flex flex-col space-y-3.5">
|
||||||
<StorageOptionsGrid
|
<StorageOptionsGrid
|
||||||
options={options}
|
options={options}
|
||||||
sizes={sizes}
|
sizes={sizes}
|
||||||
@@ -67,7 +67,7 @@ export default function Index() {
|
|||||||
size="default"
|
size="default"
|
||||||
onClick={() => setShowConfirm(true)}
|
onClick={() => setShowConfirm(true)}
|
||||||
disabled={isButtonDisabled}
|
disabled={isButtonDisabled}
|
||||||
className="w-full h-10 font-bold shadow-sm text-sm tracking-wide transition-all active:scale-[0.99]"
|
className="w-full h-10 font-bold shadow-sm text-sm tracking-wide"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export default function Index() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 w-full space-y-4 animate-in fade-in duration-300">
|
<div className="p-4 w-full space-y-4">
|
||||||
{/* 文本输入区域 */}
|
{/* 文本输入区域 */}
|
||||||
<TextInputArea
|
<TextInputArea
|
||||||
value={text}
|
value={text}
|
||||||
@@ -45,7 +45,6 @@ export default function Index() {
|
|||||||
key={item.label}
|
key={item.label}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex flex-col justify-center items-center p-4 text-center rounded-xl border border-border bg-card shadow-sm text-card-foreground',
|
'flex flex-col justify-center items-center p-4 text-center rounded-xl border border-border bg-card shadow-sm text-card-foreground',
|
||||||
'transition-all duration-200 ease-out',
|
|
||||||
'hover:-translate-y-0.5 hover:shadow-md hover:border-primary/50 focus-within:ring-1 focus-within:ring-ring',
|
'hover:-translate-y-0.5 hover:shadow-md hover:border-primary/50 focus-within:ring-1 focus-within:ring-ring',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ const LiveClock = React.memo(({ unit, onUseNow, className, ...props }: LiveClock
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={handleUseNow}
|
onClick={handleUseNow}
|
||||||
title={t('timestamp:useNowTooltip')}
|
title={t('timestamp:useNowTooltip')}
|
||||||
className="flex h-7 w-7 items-center justify-center rounded-md border border-input bg-background text-muted-foreground shadow-sm transition-all hover:bg-accent hover:text-foreground active:scale-95 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
className="flex h-7 w-7 items-center justify-center rounded-md border border-input bg-background text-muted-foreground shadow-sm hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||||
>
|
>
|
||||||
<Clock className="w-3.5 h-3.5" />
|
<Clock className="w-3.5 h-3.5" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ const ResultView = React.memo(
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex-1 flex items-center justify-center text-sm font-medium border border-dashed border-border/60 rounded-xl py-12 px-4 text-center text-muted-foreground bg-muted/20 min-h-[320px] animate-in fade-in duration-200',
|
'flex-1 flex items-center justify-center text-sm font-medium border border-dashed border-border/60 rounded-xl py-12 px-4 text-center text-muted-foreground bg-muted/20 min-h-[320px]',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -61,13 +61,7 @@ const ResultView = React.memo(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={cn('flex flex-col w-full', className)} {...props}>
|
||||||
className={cn(
|
|
||||||
'animate-in fade-in slide-in-from-bottom-2 duration-300 flex flex-col w-full',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{/* 顶部小标签 */}
|
{/* 顶部小标签 */}
|
||||||
<span className="block text-muted-foreground/90 mb-2.5 text-xs font-semibold tracking-wider uppercase">
|
<span className="block text-muted-foreground/90 mb-2.5 text-xs font-semibold tracking-wider uppercase">
|
||||||
{t('timestamp:resultLabel')}
|
{t('timestamp:resultLabel')}
|
||||||
@@ -78,7 +72,7 @@ const ResultView = React.memo(
|
|||||||
对齐 shadcn 官方卡片风格,使用 bg-card、border-border 构筑多层级阴影。
|
对齐 shadcn 官方卡片风格,使用 bg-card、border-border 构筑多层级阴影。
|
||||||
核心数值直接拉粗为 text-foreground (在黑夜模式下会自动转为大气的纯白,完美避开刺眼强光)
|
核心数值直接拉粗为 text-foreground (在黑夜模式下会自动转为大气的纯白,完美避开刺眼强光)
|
||||||
*/}
|
*/}
|
||||||
<div className="bg-card text-card-foreground border border-border p-4 sm:p-5 rounded-xl relative mb-3.5 shadow-sm flex justify-between items-center gap-4 focus-within:ring-1 focus-within:ring-ring transition-all">
|
<div className="bg-card text-card-foreground border border-border p-4 sm:p-5 rounded-xl relative mb-3.5 shadow-sm flex justify-between items-center gap-4 focus-within:ring-1 focus-within:ring-ring">
|
||||||
<span className="font-mono font-extrabold text-foreground break-all text-xl sm:text-2xl tracking-tight leading-tight select-all tabular-nums">
|
<span className="font-mono font-extrabold text-foreground break-all text-xl sm:text-2xl tracking-tight leading-tight select-all tabular-nums">
|
||||||
{result}
|
{result}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -70,17 +70,13 @@ export default function Index() {
|
|||||||
value={input}
|
value={input}
|
||||||
onChange={(e: { target: { value: string } }) => setInput(e.target.value)}
|
onChange={(e: { target: { value: string } }) => setInput(e.target.value)}
|
||||||
className={cn(
|
className={cn(
|
||||||
'font-mono font-semibold h-10 shadow-sm placeholder:text-muted-foreground/60 focus:bg-background transition-all',
|
'font-mono font-semibold h-10 shadow-sm placeholder:text-muted-foreground/60 focus:bg-background',
|
||||||
error && 'border-destructive focus-visible:ring-destructive',
|
error && 'border-destructive focus-visible:ring-destructive',
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 错误自愈提示 */}
|
{/* 错误自愈提示 */}
|
||||||
{error && (
|
{error && <p className="text-destructive text-xs font-medium px-0.5">{error}</p>}
|
||||||
<p className="text-destructive text-xs font-medium px-0.5 animate-in fade-in slide-in-from-top-1 duration-150">
|
|
||||||
{error}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 核心配置群:单位切换 + 时区选择紧凑横排 */}
|
{/* 核心配置群:单位切换 + 时区选择紧凑横排 */}
|
||||||
|
|||||||
Reference in New Issue
Block a user