fix: 修复暗黑模式样式兼容性

- 在 tailwind.config.js 中启用 darkMode: 'class' 并扩展语义化颜色变量
- 将全项目硬编码颜色(bg-white、text-gray-*、border-gray-* 等)替换为语义化类名
- 修复 popup/index.html 硬编码浅色背景色
- 同步更新相关单元测试中的类名断言
This commit is contained in:
雨霖铃
2026-05-22 16:04:50 +08:00
parent 5b9a0f59d3
commit de7da9f1db
42 changed files with 274 additions and 261 deletions
+7 -7
View File
@@ -73,15 +73,15 @@ export const CopyButton: React.FC<CopyButtonProps> = ({
const iconSize = size === 'small' ? 14 : size === 'medium' ? 16 : 18; const iconSize = size === 'small' ? 14 : size === 'medium' ? 16 : 18;
const colorClasses: Record<string, string> = { const colorClasses: Record<string, string> = {
primary: 'text-blue-600 hover:bg-blue-50', primary: 'text-primary hover:bg-primary/10',
secondary: 'text-gray-600 hover:bg-gray-50', secondary: 'text-foreground hover:bg-muted',
success: 'text-green-600 hover:bg-green-50', success: 'text-green-600 hover:bg-green-500/10',
error: 'text-red-600 hover:bg-red-50', error: 'text-red-600 hover:bg-red-500/10',
info: 'text-blue-600 hover:bg-blue-50', info: 'text-primary hover:bg-primary/10',
warning: 'text-amber-600 hover:bg-amber-50', warning: 'text-amber-600 hover:bg-amber-50',
}; };
const colorClass = colorClasses[color] || `text-[${color}] hover:bg-gray-50`; const colorClass = colorClasses[color] || `text-[${color}] hover:bg-muted`;
return ( return (
<button <button
@@ -91,7 +91,7 @@ export const CopyButton: React.FC<CopyButtonProps> = ({
style={style} style={style}
className={`${sizeClasses[size]} rounded-md flex items-center justify-center transition-all ${ className={`${sizeClasses[size]} rounded-md flex items-center justify-center transition-all ${
copied ? 'text-green-600 bg-green-50' : colorClass copied ? 'text-green-600 bg-green-50' : colorClass
} bg-white shadow-sm hover:shadow-md`} } bg-background shadow-sm hover:shadow-md`}
> >
{copied ? ( {copied ? (
<Check style={{ width: iconSize, height: iconSize }} /> <Check style={{ width: iconSize, height: iconSize }} />
+6 -6
View File
@@ -41,33 +41,33 @@ export default function DecodeResultPaper({
const { t } = useTranslation('base64Converter'); const { t } = useTranslation('base64Converter');
return ( return (
<div className="p-4 rounded-xl bg-blue-50 border border-blue-200"> <div className="p-4 rounded-xl bg-primary/10 border border-primary/30">
{/* 标题 */} {/* 标题 */}
<span className="block mb-2 text-xs font-bold text-gray-500">{title}</span> <span className="block mb-2 text-xs font-bold text-muted-foreground">{title}</span>
{/* 可选预览内容(ImageMode 的图片) */} {/* 可选预览内容(ImageMode 的图片) */}
{children} {children}
{/* 文件信息 */} {/* 文件信息 */}
<div className="flex gap-4 mb-3"> <div className="flex gap-4 mb-3">
<span className="text-xs text-gray-400"> <span className="text-xs text-muted-foreground">
{t('inferredMimeType')}: {mimeType} {t('inferredMimeType')}: {mimeType}
</span> </span>
<span className="text-xs text-gray-400"> <span className="text-xs text-muted-foreground">
{t('decodedSize')}: {formatFileSize(blobSize)} {t('decodedSize')}: {formatFileSize(blobSize)}
</span> </span>
</div> </div>
{/* 文件名输入 */} {/* 文件名输入 */}
<div className="mb-3"> <div className="mb-3">
<label className="block text-xs font-medium text-gray-500 mb-1"> <label className="block text-xs font-medium text-muted-foreground mb-1">
{t('decodedFileName')} {t('decodedFileName')}
</label> </label>
<input <input
type="text" type="text"
value={fileName} value={fileName}
onChange={(e) => onFileNameChange(e.target.value)} onChange={(e) => onFileNameChange(e.target.value)}
className="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"
/> />
</div> </div>
+2 -2
View File
@@ -45,11 +45,11 @@ export class ErrorBoundary extends Component<Props, State> {
<div className="p-6 text-center rounded-xl border border-red-200 bg-red-50"> <div className="p-6 text-center rounded-xl border border-red-200 bg-red-50">
<AlertCircle className="h-16 w-16 text-red-500 mx-auto mb-4" /> <AlertCircle className="h-16 w-16 text-red-500 mx-auto mb-4" />
<h2 className="text-xl font-extrabold text-red-600 mb-2"></h2> <h2 className="text-xl font-extrabold text-red-600 mb-2"></h2>
<p className="text-sm text-gray-500 mb-6"> <p className="text-sm text-muted-foreground mb-6">
</p> </p>
{this.state.error && ( {this.state.error && (
<div className="mb-6 p-4 bg-gray-50 rounded-lg text-left max-h-[200px] overflow-auto"> <div className="mb-6 p-4 bg-muted rounded-lg text-left max-h-[200px] overflow-auto">
<pre className="font-mono text-xs whitespace-pre-wrap break-all text-red-700"> <pre className="font-mono text-xs whitespace-pre-wrap break-all text-red-700">
{this.state.error.toString()} {this.state.error.toString()}
</pre> </pre>
+1 -1
View File
@@ -137,7 +137,7 @@ const severityConfig: Record<
{ icon: React.ElementType; bgClass: string; textClass: string } { icon: React.ElementType; bgClass: string; textClass: string }
> = { > = {
success: { icon: CheckCircle, bgClass: 'bg-green-500', textClass: 'text-white' }, success: { icon: CheckCircle, bgClass: 'bg-green-500', textClass: 'text-white' },
info: { icon: Info, bgClass: 'bg-blue-500', textClass: 'text-white' }, info: { icon: Info, bgClass: 'bg-primary/100', textClass: 'text-white' },
warning: { icon: AlertTriangle, bgClass: 'bg-amber-500', textClass: 'text-white' }, warning: { icon: AlertTriangle, bgClass: 'bg-amber-500', textClass: 'text-white' },
error: { icon: XCircle, bgClass: 'bg-red-500', textClass: 'text-white' }, error: { icon: XCircle, bgClass: 'bg-red-500', textClass: 'text-white' },
}; };
+13 -6
View File
@@ -118,7 +118,7 @@ const ImageUploader = ({
? 'border-green-600 bg-green-50' ? 'border-green-600 bg-green-50'
: selectedFile : selectedFile
? 'border-green-600 bg-green-50/50' ? 'border-green-600 bg-green-50/50'
: 'border-gray-300 bg-gray-50 hover:border-green-600 hover:bg-green-50/50' : 'border-input bg-muted hover:border-green-600 hover:bg-green-500/10/50'
}`} }`}
onDragOver={handleDragOver} onDragOver={handleDragOver}
onDragLeave={handleDragLeave} onDragLeave={handleDragLeave}
@@ -153,14 +153,21 @@ const ImageUploader = ({
<X className="w-3 h-3" /> <X className="w-3 h-3" />
</button> </button>
</div> </div>
<span className="block text-sm text-gray-500 mt-2">{selectedFile.name}</span> <span className="block text-sm text-muted-foreground mt-2">{selectedFile.name}</span>
<span className="block text-xs text-gray-400">{t('qrCode:clickToChange')}</span> <span className="block text-xs text-muted-foreground">{t('qrCode:clickToChange')}</span>
</div> </div>
) : ( ) : (
<> <>
<Image data-testid="ImageIcon" className="w-12 h-12 text-gray-400 mx-auto mb-2" /> <Image
<span className="block text-sm text-gray-500 mb-1">{t('qrCode:clickToUpload')}</span> data-testid="ImageIcon"
<span className="block text-xs text-gray-400">{t('qrCode:supportFormats')}</span> className="w-12 h-12 text-muted-foreground mx-auto mb-2"
/>
<span className="block text-sm text-muted-foreground mb-1">
{t('qrCode:clickToUpload')}
</span>
<span className="block text-xs text-muted-foreground">
{t('qrCode:supportFormats')}
</span>
</> </>
)} )}
</label> </label>
+2 -2
View File
@@ -47,11 +47,11 @@ export class PageErrorBoundary extends Component<Props, State> {
<div className="p-6 text-center rounded-xl border border-red-200 bg-red-50 max-w-md w-full"> <div className="p-6 text-center rounded-xl border border-red-200 bg-red-50 max-w-md w-full">
<AlertCircle className="h-12 w-12 text-red-500 mx-auto mb-3" /> <AlertCircle className="h-12 w-12 text-red-500 mx-auto mb-3" />
<h3 className="text-lg font-bold text-red-600 mb-2"></h3> <h3 className="text-lg font-bold text-red-600 mb-2"></h3>
<p className="text-sm text-gray-500 mb-4"> <p className="text-sm text-muted-foreground mb-4">
</p> </p>
{this.state.error && ( {this.state.error && (
<div className="mb-4 p-3 bg-gray-50 rounded-lg text-left max-h-[160px] overflow-auto"> <div className="mb-4 p-3 bg-muted rounded-lg text-left max-h-[160px] overflow-auto">
<pre className="font-mono text-xs whitespace-pre-wrap break-all text-red-700"> <pre className="font-mono text-xs whitespace-pre-wrap break-all text-red-700">
{this.state.error.toString()} {this.state.error.toString()}
</pre> </pre>
+1 -1
View File
@@ -92,7 +92,7 @@ export default function PageHeader({
</div> </div>
{/* 副标题 */} {/* 副标题 */}
{subtitle && ( {subtitle && (
<span className="text-xs font-semibold text-gray-500" style={subtitleSx}> <span className="text-xs font-semibold text-muted-foreground" style={subtitleSx}>
{subtitle} {subtitle}
</span> </span>
)} )}
+1 -1
View File
@@ -14,7 +14,7 @@ interface PageSkeletonProps {
*/ */
function DashboardCardSkeleton() { function DashboardCardSkeleton() {
return ( return (
<div className="rounded-xl border border-gray-200 p-5 h-[100px]"> <div className="rounded-xl border border-border p-5 h-[100px]">
<div className="flex justify-between items-start"> <div className="flex justify-between items-start">
<div className="flex gap-3 items-center"> <div className="flex gap-3 items-center">
<div className="w-10 h-10 rounded-lg bg-gray-200 animate-pulse" /> <div className="w-10 h-10 rounded-lg bg-gray-200 animate-pulse" />
+6 -4
View File
@@ -15,21 +15,23 @@ const QrCodePreview = ({ qrCodeDataUrl, onDownload, onCopy }: QrCodePreviewProps
if (!qrCodeDataUrl) { if (!qrCodeDataUrl) {
return ( return (
<div className="flex flex-col justify-center items-center min-h-[200px] border-2 border-dashed border-gray-300 rounded-xl p-4 bg-gray-50"> <div className="flex flex-col justify-center items-center min-h-[200px] border-2 border-dashed border-input rounded-xl p-4 bg-muted">
<span className="text-sm text-gray-500 text-center">{t('qrCode:qrCodeWillShow')}</span> <span className="text-sm text-muted-foreground text-center">
{t('qrCode:qrCodeWillShow')}
</span>
</div> </div>
); );
} }
return ( return (
<div className="flex flex-col justify-center items-center min-h-[200px] border-2 border-dashed border-gray-300 rounded-xl p-4 bg-gray-50"> <div className="flex flex-col justify-center items-center min-h-[200px] border-2 border-dashed border-input rounded-xl p-4 bg-muted">
<div className="flex flex-col items-center w-full"> <div className="flex flex-col items-center w-full">
<img src={qrCodeDataUrl} alt="QR Code" className="w-[250px] h-[250px] block" /> <img src={qrCodeDataUrl} alt="QR Code" className="w-[250px] h-[250px] block" />
<div className="flex gap-2 mt-4"> <div className="flex gap-2 mt-4">
<button <button
type="button" type="button"
onClick={onDownload} onClick={onDownload}
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-green-700 bg-white border border-green-600 rounded-lg hover:bg-green-50 transition-colors" className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-green-600 bg-background border border-green-600 rounded-lg hover:bg-green-500/10 transition-colors"
> >
<Download className="w-4 h-4" /> <Download className="w-4 h-4" />
{t('qrCode:downloadButton')} {t('qrCode:downloadButton')}
+3 -3
View File
@@ -28,7 +28,7 @@ export default function SwitchButtonGroup<T extends string | number = string>({
return ( return (
<div <div
className="w-full mb-4 rounded-xl border border-gray-200 bg-gray-50 p-1.5 flex gap-1" className="w-full mb-4 rounded-xl border border-border bg-muted p-1.5 flex gap-1"
style={sx} style={sx}
> >
{options.map((option) => ( {options.map((option) => (
@@ -40,8 +40,8 @@ export default function SwitchButtonGroup<T extends string | number = string>({
sizeClasses[size] sizeClasses[size]
} ${ } ${
value === option.value value === option.value
? 'bg-white text-blue-600 shadow-sm' ? 'bg-background text-primary shadow-sm'
: 'text-gray-500 hover:text-gray-700 hover:bg-white/50' : 'text-muted-foreground hover:text-foreground hover:bg-background/50'
}`} }`}
style={buttonSx} style={buttonSx}
> >
+10 -10
View File
@@ -152,9 +152,9 @@ function ActionButton({
typeof action.disabled === 'function' ? action.disabled(value) : action.disabled || !value; typeof action.disabled === 'function' ? action.disabled(value) : action.disabled || !value;
const typeClasses: Record<string, string> = { const typeClasses: Record<string, string> = {
primary: 'bg-blue-600 text-white hover:bg-blue-700', primary: 'bg-primary text-white hover:bg-primary/90',
danger: 'text-red-600 hover:bg-red-50', danger: 'text-red-600 hover:bg-red-500/10',
default: 'text-gray-500 hover:bg-gray-50', default: 'text-muted-foreground hover:bg-muted',
}; };
const sizeClasses = { const sizeClasses = {
@@ -333,7 +333,7 @@ const TextInputArea = forwardRef<HTMLTextAreaElement, TextInputAreaProps>((props
{hasTopBar && ( {hasTopBar && (
<div className="flex items-center justify-between mb-2 px-1"> <div className="flex items-center justify-between mb-2 px-1">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{title && <span className="text-sm font-semibold text-gray-500">{title}</span>} {title && <span className="text-sm font-semibold text-muted-foreground">{title}</span>}
{topExtra} {topExtra}
</div> </div>
@@ -349,7 +349,7 @@ const TextInputArea = forwardRef<HTMLTextAreaElement, TextInputAreaProps>((props
/> />
))} ))}
{showCount && ( {showCount && (
<span className="text-xs text-gray-400 tabular-nums ml-1"> <span className="text-xs text-muted-foreground tabular-nums ml-1">
{value.length} {value.length}
{maxLength ? ` / ${maxLength}` : ''} {maxLength ? ` / ${maxLength}` : ''}
</span> </span>
@@ -374,10 +374,10 @@ const TextInputArea = forwardRef<HTMLTextAreaElement, TextInputAreaProps>((props
maxHeight: autoResize ? `${maxRows * 1.5}rem` : undefined, maxHeight: autoResize ? `${maxRows * 1.5}rem` : undefined,
}} }}
className={`w-full rounded-lg border ${ className={`w-full rounded-lg border ${
displayError ? 'border-red-300' : 'border-gray-200' displayError ? 'border-red-300' : 'border-border'
} bg-white px-3 py-3 font-mono text-sm leading-relaxed transition-all resize-y ${ } bg-background px-3 py-3 font-mono text-sm leading-relaxed transition-all resize-y ${
showClear || allowCopy || bottomActions.length > 0 ? 'pb-10' : '' showClear || allowCopy || bottomActions.length > 0 ? 'pb-10' : ''
} focus:outline-none focus:ring-2 focus:ring-blue-500 focus:bg-white hover:bg-gray-50 ${ } focus:outline-none focus:ring-2 focus:ring-primary focus:bg-background hover:bg-muted ${
displayError ? 'focus:ring-red-500' : '' displayError ? 'focus:ring-red-500' : ''
}`} }`}
/> />
@@ -403,7 +403,7 @@ const TextInputArea = forwardRef<HTMLTextAreaElement, TextInputAreaProps>((props
type="button" type="button"
onClick={handleCopy} onClick={handleCopy}
title={t('textInputArea.copyContent')} title={t('textInputArea.copyContent')}
className="p-1 rounded-md text-gray-400 hover:text-blue-600 hover:bg-blue-50 transition-colors" className="p-1 rounded-md text-muted-foreground hover:text-primary hover:bg-primary/10 transition-colors"
> >
<Copy className="h-4 w-4" /> <Copy className="h-4 w-4" />
</button> </button>
@@ -413,7 +413,7 @@ const TextInputArea = forwardRef<HTMLTextAreaElement, TextInputAreaProps>((props
type="button" type="button"
onClick={handleClear} onClick={handleClear}
title={t('textInputArea.clear')} title={t('textInputArea.clear')}
className="p-1 rounded-md text-gray-400 hover:text-red-600 hover:bg-red-50 transition-colors" className="p-1 rounded-md text-muted-foreground hover:text-red-600 hover:bg-red-500/10 transition-colors"
> >
<X className="h-4 w-4" /> <X className="h-4 w-4" />
</button> </button>
+20 -20
View File
@@ -156,27 +156,27 @@ export default function TopBar({ onOpenOptions }: { onOpenOptions: () => void })
const isDashboard = currentPage === 'dashboard'; const isDashboard = currentPage === 'dashboard';
return ( return (
<div className="flex justify-between items-center px-3 sm:px-4 py-3 border-b border-gray-200 bg-white relative z-[1100]"> <div className="flex justify-between items-center px-3 sm:px-4 py-3 border-b border-border bg-background relative z-[1100]">
<div className="w-8 sm:w-10"> <div className="w-8 sm:w-10">
{!isDashboard && ( {!isDashboard && (
<button <button
type="button" type="button"
onClick={goBack} onClick={goBack}
aria-label={t('common:buttons.back')} aria-label={t('common:buttons.back')}
className="p-1.5 rounded-md bg-gray-50 hover:bg-gray-100 transition-colors" className="p-1.5 rounded-md bg-muted hover:bg-accent transition-colors"
> >
<ArrowLeft className="h-3.5 w-3.5" /> <ArrowLeft className="h-3.5 w-3.5" />
</button> </button>
)} )}
</div> </div>
<span className="hidden md:block text-xs font-extrabold tracking-wider uppercase text-gray-500 ml-2"> <span className="hidden md:block text-xs font-extrabold tracking-wider uppercase text-muted-foreground ml-2">
{t('common:appName')} {t('common:appName')}
</span> </span>
<div className="flex-1 mx-2 sm:mx-4 relative max-w-[400px]"> <div className="flex-1 mx-2 sm:mx-4 relative max-w-[400px]">
<div className="relative"> <div className="relative">
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"> <div className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground">
<Search className="h-4 w-4" /> <Search className="h-4 w-4" />
</div> </div>
<input <input
@@ -188,7 +188,7 @@ export default function TopBar({ onOpenOptions }: { onOpenOptions: () => void })
onFocus={() => setShowResults(true)} onFocus={() => setShowResults(true)}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
aria-label={t('common:buttons.search')} aria-label={t('common:buttons.search')}
className="w-full pl-9 pr-8 py-1.5 text-sm rounded-lg border border-transparent bg-gray-50 focus:bg-white focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 transition-all" className="w-full pl-9 pr-8 py-1.5 text-sm rounded-lg border border-transparent bg-muted focus:bg-background focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all"
/> />
{searchQuery && ( {searchQuery && (
<button <button
@@ -198,7 +198,7 @@ export default function TopBar({ onOpenOptions }: { onOpenOptions: () => void })
setSelectedIndex(-1); setSelectedIndex(-1);
}} }}
aria-label={t('common:buttons.clearSearch')} aria-label={t('common:buttons.clearSearch')}
className="absolute right-2 top-1/2 -translate-y-1/2 p-0.5 rounded-md text-gray-400 hover:text-gray-600" className="absolute right-2 top-1/2 -translate-y-1/2 p-0.5 rounded-md text-muted-foreground hover:text-foreground"
> >
<X className="h-3.5 w-3.5" /> <X className="h-3.5 w-3.5" />
</button> </button>
@@ -206,7 +206,7 @@ export default function TopBar({ onOpenOptions }: { onOpenOptions: () => void })
</div> </div>
{showResults && (searchQuery.trim() || displayedHistory.length > 0) && ( {showResults && (searchQuery.trim() || displayedHistory.length > 0) && (
<div className="absolute top-full left-0 right-0 mt-2 bg-white rounded-lg shadow-lg border border-gray-200 max-h-[300px] overflow-auto z-[1200]"> <div className="absolute top-full left-0 right-0 mt-2 bg-popover rounded-lg shadow-lg border border-border max-h-[300px] overflow-auto z-[1200]">
<ul role="listbox" className="py-1"> <ul role="listbox" className="py-1">
{searchQuery.trim() ? ( {searchQuery.trim() ? (
searchResults.length > 0 ? ( searchResults.length > 0 ? (
@@ -217,17 +217,17 @@ export default function TopBar({ onOpenOptions }: { onOpenOptions: () => void })
aria-selected={selectedIndex === index} aria-selected={selectedIndex === index}
onClick={() => handleSelectFeature(feature)} onClick={() => handleSelectFeature(feature)}
className={`flex items-center gap-3 px-3 py-2 cursor-pointer transition-colors ${ className={`flex items-center gap-3 px-3 py-2 cursor-pointer transition-colors ${
selectedIndex === index ? 'bg-blue-50' : 'hover:bg-gray-50' selectedIndex === index ? 'bg-primary/10' : 'hover:bg-muted'
}`} }`}
> >
<div className="w-8 h-8 flex items-center justify-center text-gray-500"> <div className="w-8 h-8 flex items-center justify-center text-muted-foreground">
{feature.icon && <feature.icon className="h-5 w-5" />} {feature.icon && <feature.icon className="h-5 w-5" />}
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm font-semibold text-gray-900 truncate"> <p className="text-sm font-semibold text-foreground truncate">
{t(feature.labelKey)} {t(feature.labelKey)}
</p> </p>
<p className="text-xs text-gray-500 truncate"> <p className="text-xs text-muted-foreground truncate">
{t(feature.descriptionKey)} {t(feature.descriptionKey)}
</p> </p>
</div> </div>
@@ -235,13 +235,13 @@ export default function TopBar({ onOpenOptions }: { onOpenOptions: () => void })
)) ))
) : ( ) : (
<li className="px-4 py-6 text-center"> <li className="px-4 py-6 text-center">
<p className="text-sm text-gray-500">{t('common:buttons.noResults')}</p> <p className="text-sm text-muted-foreground">{t('common:buttons.noResults')}</p>
</li> </li>
) )
) : ( ) : (
<> <>
<li className="px-4 py-2"> <li className="px-4 py-2">
<span className="text-xs font-bold text-gray-400"> <span className="text-xs font-bold text-muted-foreground">
{t('common:buttons.recentSearch')} {t('common:buttons.recentSearch')}
</span> </span>
</li> </li>
@@ -255,13 +255,13 @@ export default function TopBar({ onOpenOptions }: { onOpenOptions: () => void })
setSelectedIndex(-1); setSelectedIndex(-1);
}} }}
className={`flex items-center gap-3 px-3 py-2 cursor-pointer transition-colors ${ className={`flex items-center gap-3 px-3 py-2 cursor-pointer transition-colors ${
selectedIndex === index ? 'bg-blue-50' : 'hover:bg-gray-50' selectedIndex === index ? 'bg-primary/10' : 'hover:bg-muted'
}`} }`}
> >
<div className="w-8 h-8 flex items-center justify-center text-gray-400"> <div className="w-8 h-8 flex items-center justify-center text-muted-foreground">
<History className="h-4 w-4" /> <History className="h-4 w-4" />
</div> </div>
<span className="text-sm text-gray-700">{item}</span> <span className="text-sm text-foreground">{item}</span>
</li> </li>
))} ))}
</> </>
@@ -277,7 +277,7 @@ export default function TopBar({ onOpenOptions }: { onOpenOptions: () => void })
onClick={toggleLanguage} onClick={toggleLanguage}
aria-label={t('common:buttons.toggleLanguage')} aria-label={t('common:buttons.toggleLanguage')}
title={t('common:buttons.toggleLanguage')} title={t('common:buttons.toggleLanguage')}
className="p-1.5 rounded-md hover:bg-gray-100 transition-colors" className="p-1.5 rounded-md hover:bg-accent transition-colors"
> >
<Globe className="h-4 w-4" /> <Globe className="h-4 w-4" />
</button> </button>
@@ -286,7 +286,7 @@ export default function TopBar({ onOpenOptions }: { onOpenOptions: () => void })
onClick={cycleThemeMode} onClick={cycleThemeMode}
aria-label={t('common:buttons.toggleTheme')} aria-label={t('common:buttons.toggleTheme')}
title={t(`common:buttons.themeMode.${mode}`)} title={t(`common:buttons.themeMode.${mode}`)}
className="p-1.5 rounded-md hover:bg-gray-100 transition-colors" className="p-1.5 rounded-md hover:bg-accent transition-colors"
> >
<ThemeIcon className="h-4 w-4" /> <ThemeIcon className="h-4 w-4" />
</button> </button>
@@ -295,7 +295,7 @@ export default function TopBar({ onOpenOptions }: { onOpenOptions: () => void })
onClick={handleOpenInTab} onClick={handleOpenInTab}
aria-label={t('common:buttons.openInTab')} aria-label={t('common:buttons.openInTab')}
title={t('common:buttons.openInTab')} title={t('common:buttons.openInTab')}
className="p-1.5 rounded-md hover:bg-gray-100 transition-colors" className="p-1.5 rounded-md hover:bg-accent transition-colors"
> >
<ExternalLink className="h-4 w-4" /> <ExternalLink className="h-4 w-4" />
</button> </button>
@@ -304,7 +304,7 @@ export default function TopBar({ onOpenOptions }: { onOpenOptions: () => void })
onClick={onOpenOptions} onClick={onOpenOptions}
aria-label={t('common:buttons.settings')} aria-label={t('common:buttons.settings')}
title={t('common:buttons.settings')} title={t('common:buttons.settings')}
className="p-1.5 rounded-md hover:bg-gray-100 transition-colors" className="p-1.5 rounded-md hover:bg-accent transition-colors"
> >
<Settings className="h-4 w-4" /> <Settings className="h-4 w-4" />
</button> </button>
+1 -1
View File
@@ -84,7 +84,7 @@ describe('PageHeader 组件系统', () => {
it('无副标题时不渲染副标题区域', () => { it('无副标题时不渲染副标题区域', () => {
const { container } = render(<PageHeader icon={defaultProps.icon} title="仅标题" />); const { container } = render(<PageHeader icon={defaultProps.icon} title="仅标题" />);
const subtitles = container.querySelectorAll('.text-gray-500'); const subtitles = container.querySelectorAll('.text-muted-foreground');
expect(subtitles.length).toBe(0); expect(subtitles.length).toBe(0);
}); });
}); });
@@ -21,10 +21,10 @@ describe('SwitchButtonGroup 组件', () => {
const buttonA = screen.getByRole('button', { name: /选项A/i }); const buttonA = screen.getByRole('button', { name: /选项A/i });
const buttonB = screen.getByRole('button', { name: /选项B/i }); const buttonB = screen.getByRole('button', { name: /选项B/i });
// 选中的按钮有 bg-white text-blue-600 shadow-sm 类 // 选中的按钮有 bg-background text-primary shadow-sm 类
expect(buttonA).toHaveClass('bg-white', 'text-blue-600', 'shadow-sm'); expect(buttonA).toHaveClass('bg-background', 'text-primary', 'shadow-sm');
// 未选中的按钮有 text-gray-500 // 未选中的按钮有 text-muted-foreground
expect(buttonB).toHaveClass('text-gray-500'); expect(buttonB).toHaveClass('text-muted-foreground');
}); });
it('点击未选中按钮时应触发 onChange 并传入选中值', () => { it('点击未选中按钮时应触发 onChange 并传入选中值', () => {
@@ -122,8 +122,8 @@ describe('SwitchButtonGroup 组件', () => {
const button2 = screen.getByRole('button', { name: /^2$/i }); const button2 = screen.getByRole('button', { name: /^2$/i });
const button4 = screen.getByRole('button', { name: /^4$/i }); const button4 = screen.getByRole('button', { name: /^4$/i });
expect(button2).toHaveClass('text-gray-500'); expect(button2).toHaveClass('text-muted-foreground');
expect(button4).toHaveClass('bg-white', 'text-blue-600', 'shadow-sm'); expect(button4).toHaveClass('bg-background', 'text-primary', 'shadow-sm');
}); });
it('点击 number 选项时应传回 number 值', () => { it('点击 number 选项时应传回 number 值', () => {
+2 -2
View File
@@ -334,7 +334,7 @@ describe('TextInputArea 组件', () => {
); );
const btn = screen.getByText('主要'); const btn = screen.getByText('主要');
expect(btn).toHaveClass('bg-blue-600', 'text-white'); expect(btn).toHaveClass('bg-primary', 'text-white');
}); });
}); });
@@ -346,7 +346,7 @@ describe('TextInputArea 组件', () => {
it('不设置 title 时不渲染标题', () => { it('不设置 title 时不渲染标题', () => {
const { container } = render(<TextInputArea value="" onChange={() => {}} />); const { container } = render(<TextInputArea value="" onChange={() => {}} />);
expect(container.querySelector('.text-gray-500')).not.toBeInTheDocument(); expect(container.querySelector('.text-muted-foreground')).not.toBeInTheDocument();
}); });
}); });
+15 -15
View File
@@ -91,14 +91,14 @@ function SortableFeatureRow({
<div <div
ref={setNodeRef} ref={setNodeRef}
style={style} style={style}
className={`flex items-center justify-between p-4 sm:p-5 transition-all duration-200 hover:bg-gray-50 ${ className={`flex items-center justify-between p-4 sm:p-5 transition-all duration-200 hover:bg-muted ${
isLast ? '' : 'border-b border-gray-200' isLast ? '' : 'border-b border-border'
}`} }`}
> >
<div className="flex items-center gap-3 sm:gap-4 flex-1 min-w-0"> <div className="flex items-center gap-3 sm:gap-4 flex-1 min-w-0">
{/* 拖拽手柄 */} {/* 拖拽手柄 */}
<div <div
className="drag-handle text-gray-300 cursor-grab touch-none transition-colors duration-200 hover:text-gray-500 active:cursor-grabbing" className="drag-handle text-muted-foreground cursor-grab touch-none transition-colors duration-200 hover:text-foreground active:cursor-grabbing"
{...attributes} {...attributes}
{...listeners} {...listeners}
aria-label={`拖拽以调整 ${t(feature.labelKey)} 的位置`} aria-label={`拖拽以调整 ${t(feature.labelKey)} 的位置`}
@@ -119,12 +119,12 @@ function SortableFeatureRow({
{/* 文本信息 */} {/* 文本信息 */}
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="font-bold text-[0.95rem] leading-tight text-gray-900"> <div className="font-bold text-[0.95rem] leading-tight text-foreground">
{t(feature.labelKey)} {t(feature.labelKey)}
</div> </div>
{feature.descriptionKey && ( {feature.descriptionKey && (
<div <div
className="text-xs text-gray-500 font-medium mt-0.5 overflow-hidden text-ellipsis whitespace-nowrap" className="text-xs text-muted-foreground font-medium mt-0.5 overflow-hidden text-ellipsis whitespace-nowrap"
title={t(feature.descriptionKey)} title={t(feature.descriptionKey)}
> >
{t(feature.descriptionKey)} {t(feature.descriptionKey)}
@@ -140,12 +140,12 @@ function SortableFeatureRow({
aria-checked={isChecked} aria-checked={isChecked}
disabled={isDisabled} disabled={isDisabled}
onClick={() => onToggle(pageKey)} onClick={() => onToggle(pageKey)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 ${ className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 ${
isChecked ? 'bg-blue-600' : 'bg-gray-200' isChecked ? 'bg-primary' : 'bg-muted'
} ${isDisabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`} } ${isDisabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
> >
<span <span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform duration-200 ${ className={`inline-block h-4 w-4 transform rounded-full bg-background transition-transform duration-200 ${
isChecked ? 'translate-x-6' : 'translate-x-1' isChecked ? 'translate-x-6' : 'translate-x-1'
}`} }`}
/> />
@@ -306,16 +306,16 @@ export default function App() {
if (!isLoaded) { if (!isLoaded) {
return ( return (
<div className="flex justify-center items-center min-h-screen"> <div className="flex justify-center items-center min-h-screen">
<div className="w-6 h-6 border-2 border-blue-600 border-t-transparent rounded-full animate-spin" /> <div className="w-6 h-6 border-2 border-primary border-t-transparent rounded-full animate-spin" />
</div> </div>
); );
} }
return ( return (
<div className="app min-h-screen bg-gray-50 flex flex-col"> <div className="app min-h-screen bg-background flex flex-col">
<PageErrorBoundary> <PageErrorBoundary>
{/* 顶部标题与导航栏 */} {/* 顶部标题与导航栏 */}
<div className="w-full bg-white border-b border-gray-200 pt-8 sm:pt-12 pb-0 px-4 sm:px-8"> <div className="w-full bg-background border-b border-border pt-8 sm:pt-12 pb-0 px-4 sm:px-8">
<div className="max-w-3xl mx-auto"> <div className="max-w-3xl mx-auto">
<PageHeader <PageHeader
icon={<Settings size={20} />} icon={<Settings size={20} />}
@@ -333,8 +333,8 @@ export default function App() {
onClick={(e) => handleWindowTypeChange(e, type)} onClick={(e) => handleWindowTypeChange(e, type)}
className={`px-4 sm:px-6 py-2 text-[0.9rem] font-bold transition-colors duration-200 ${ className={`px-4 sm:px-6 py-2 text-[0.9rem] font-bold transition-colors duration-200 ${
windowType === type windowType === type
? 'text-blue-600 border-b-2 border-blue-600' ? 'text-primary border-b-2 border-primary'
: 'text-gray-500 hover:text-gray-700' : 'text-muted-foreground hover:text-foreground'
}`} }`}
> >
{type === 'popup' ? 'Popup 窗口' : type === 'sidepanel' ? '侧边栏' : '标签页'} {type === 'popup' ? 'Popup 窗口' : type === 'sidepanel' ? '侧边栏' : '标签页'}
@@ -343,7 +343,7 @@ export default function App() {
</div> </div>
<button <button
onClick={handleRestoreDefaults} onClick={handleRestoreDefaults}
className="mb-1 ml-2 p-1.5 text-gray-500 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors duration-200" className="mb-1 ml-2 p-1.5 text-muted-foreground hover:text-primary hover:bg-primary/10 rounded-lg transition-colors duration-200"
title="恢复当前模式默认" title="恢复当前模式默认"
> >
<RefreshCw size={16} /> <RefreshCw size={16} />
@@ -355,7 +355,7 @@ export default function App() {
{/* 主内容区域 */} {/* 主内容区域 */}
<div className="flex-1 p-4 sm:p-8"> <div className="flex-1 p-4 sm:p-8">
<div className="max-w-3xl mx-auto"> <div className="max-w-3xl mx-auto">
<div className="rounded-2xl border border-gray-200 overflow-hidden bg-white shadow-sm"> <div className="rounded-2xl border border-border overflow-hidden bg-card shadow-sm">
<DndContext <DndContext
sensors={sensors} sensors={sensors}
collisionDetection={closestCenter} collisionDetection={closestCenter}
+1 -1
View File
@@ -13,7 +13,7 @@
margin: 0; margin: 0;
padding: 0; padding: 0;
overflow: hidden; overflow: hidden;
background-color: #f5f5f5; /* Light mode default */ background-color: hsl(var(--background));
} }
/* Ensure full size for the root container */ /* Ensure full size for the root container */
#root { #root {
-30
View File
@@ -2182,9 +2182,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2201,9 +2198,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2220,9 +2214,6 @@
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2239,9 +2230,6 @@
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2258,9 +2246,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2277,9 +2262,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -7564,9 +7546,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"glibc"
],
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -7587,9 +7566,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"musl"
],
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -7610,9 +7586,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"glibc"
],
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -7633,9 +7606,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"musl"
],
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
+16 -16
View File
@@ -179,10 +179,10 @@ export default function FileMode() {
onClick={() => fileInputRef.current?.click()} onClick={() => fileInputRef.current?.click()}
className={`flex flex-col items-center justify-center min-h-[180px] border-2 border-dashed rounded-xl p-8 cursor-pointer transition-all duration-200 ${ className={`flex flex-col items-center justify-center min-h-[180px] border-2 border-dashed rounded-xl p-8 cursor-pointer transition-all duration-200 ${
isDragging isDragging
? 'border-blue-500 bg-blue-50' ? 'border-primary bg-primary/10'
: info : info
? 'border-blue-500 bg-blue-50/50' ? 'border-primary bg-primary/5'
: 'border-gray-300 bg-gray-50 hover:border-blue-500 hover:bg-blue-50/50' : 'border-input bg-muted hover:border-primary hover:bg-primary/5'
}`} }`}
> >
<input <input
@@ -195,23 +195,23 @@ export default function FileMode() {
}} }}
/> />
{isLoading ? ( {isLoading ? (
<div className="w-10 h-10 border-4 border-blue-200 border-t-blue-600 rounded-full animate-spin" /> <div className="w-10 h-10 border-4 border-primary/30 border-t-blue-600 rounded-full animate-spin" />
) : info ? ( ) : info ? (
<div className="flex flex-col items-center gap-1"> <div className="flex flex-col items-center gap-1">
<Upload className="w-10 h-10 text-blue-600" /> <Upload className="w-10 h-10 text-primary" />
<span className="text-sm font-bold">{info.name}</span> <span className="text-sm font-bold">{info.name}</span>
<span className="text-xs text-gray-500"> <span className="text-xs text-muted-foreground">
{formatFileSize(info.size)} · {info.type} {formatFileSize(info.size)} · {info.type}
</span> </span>
<span className="text-xs text-gray-400">{t('clickOrDropToReplace')}</span> <span className="text-xs text-muted-foreground">{t('clickOrDropToReplace')}</span>
</div> </div>
) : ( ) : (
<div className="flex flex-col items-center gap-1"> <div className="flex flex-col items-center gap-1">
<Upload className="w-10 h-10 text-gray-400" /> <Upload className="w-10 h-10 text-muted-foreground" />
<span className="text-sm text-gray-500 font-semibold"> <span className="text-sm text-muted-foreground font-semibold">
{t('clickOrDropToFile')} {t('clickOrDropToFile')}
</span> </span>
<span className="text-xs text-gray-400"> <span className="text-xs text-muted-foreground">
{t('maxFileSize', { max: `${MAX_FILE_SIZE / 1024 / 1024} MB` })} {t('maxFileSize', { max: `${MAX_FILE_SIZE / 1024 / 1024} MB` })}
</span> </span>
</div> </div>
@@ -228,9 +228,9 @@ export default function FileMode() {
)} )}
{result && ( {result && (
<div className="p-4 rounded-xl bg-blue-50 border border-blue-200"> <div className="p-4 rounded-xl bg-primary/10 border border-primary/30">
<div className="flex justify-between items-center mb-2"> <div className="flex justify-between items-center mb-2">
<span className="text-xs font-bold text-gray-500">{t('base64Output')}</span> <span className="text-xs font-bold text-muted-foreground">{t('base64Output')}</span>
<div className="flex gap-1"> <div className="flex gap-1">
<CopyButton text={result.rawBase64} tooltip={t('copyRaw')} /> <CopyButton text={result.rawBase64} tooltip={t('copyRaw')} />
<CopyButton text={result.output} tooltip={t('copyDataUri')} color="info" /> <CopyButton text={result.output} tooltip={t('copyDataUri')} color="info" />
@@ -247,17 +247,17 @@ export default function FileMode() {
showCount showCount
/> />
<div className="flex items-center gap-4 mt-2"> <div className="flex items-center gap-4 mt-2">
<span className="text-xs text-gray-400"> <span className="text-xs text-muted-foreground">
{t('originalSize')}: {formatFileSize(result.originalBytes)} {t('originalSize')}: {formatFileSize(result.originalBytes)}
</span> </span>
<span className="text-xs text-gray-400"> <span className="text-xs text-muted-foreground">
{t('encodedSize')}: {formatFileSize(result.outputBytes)} {t('encodedSize')}: {formatFileSize(result.outputBytes)}
</span> </span>
<div className="flex-1" /> <div className="flex-1" />
<button <button
type="button" type="button"
onClick={handleClear} onClick={handleClear}
className="flex items-center gap-1 px-2 py-1 text-xs text-gray-500 hover:bg-gray-100 rounded-md transition-colors" className="flex items-center gap-1 px-2 py-1 text-xs text-muted-foreground hover:bg-accent rounded-md transition-colors"
> >
<Trash2 className="w-3 h-3" /> <Trash2 className="w-3 h-3" />
{t('clear')} {t('clear')}
@@ -270,7 +270,7 @@ export default function FileMode() {
<button <button
type="button" type="button"
onClick={handleClear} onClick={handleClear}
className="flex items-center gap-1 px-3 py-2 text-sm text-gray-500 hover:bg-gray-100 rounded-lg transition-colors" className="flex items-center gap-1 px-3 py-2 text-sm text-muted-foreground hover:bg-accent rounded-lg transition-colors"
> >
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
{t('clear')} {t('clear')}
+15 -15
View File
@@ -185,10 +185,10 @@ export default function ImageMode() {
onClick={() => imageInputRef.current?.click()} onClick={() => imageInputRef.current?.click()}
className={`flex flex-col items-center justify-center min-h-[180px] border-2 border-dashed rounded-xl p-8 cursor-pointer transition-all duration-200 ${ className={`flex flex-col items-center justify-center min-h-[180px] border-2 border-dashed rounded-xl p-8 cursor-pointer transition-all duration-200 ${
isDragging isDragging
? 'border-blue-500 bg-blue-50' ? 'border-primary bg-primary/10'
: info : info
? 'border-blue-500 bg-blue-50/50' ? 'border-primary bg-primary/5'
: 'border-gray-300 bg-gray-50 hover:border-blue-500 hover:bg-blue-50/50' : 'border-input bg-muted hover:border-primary hover:bg-primary/5'
}`} }`}
> >
<input <input
@@ -202,7 +202,7 @@ export default function ImageMode() {
}} }}
/> />
{isLoading ? ( {isLoading ? (
<div className="w-10 h-10 border-4 border-blue-200 border-t-blue-600 rounded-full animate-spin" /> <div className="w-10 h-10 border-4 border-primary/30 border-t-blue-600 rounded-full animate-spin" />
) : info ? ( ) : info ? (
<div className="flex flex-col items-center gap-1"> <div className="flex flex-col items-center gap-1">
{result && ( {result && (
@@ -213,18 +213,18 @@ export default function ImageMode() {
/> />
)} )}
<span className="text-sm font-bold">{info.name}</span> <span className="text-sm font-bold">{info.name}</span>
<span className="text-xs text-gray-500"> <span className="text-xs text-muted-foreground">
{formatFileSize(info.size)} · {info.type} {formatFileSize(info.size)} · {info.type}
</span> </span>
<span className="text-xs text-gray-400">{t('clickOrDropToReplace')}</span> <span className="text-xs text-muted-foreground">{t('clickOrDropToReplace')}</span>
</div> </div>
) : ( ) : (
<div className="flex flex-col items-center gap-1"> <div className="flex flex-col items-center gap-1">
<Image className="w-10 h-10 text-gray-400" /> <Image className="w-10 h-10 text-muted-foreground" />
<span className="text-sm text-gray-500 font-semibold"> <span className="text-sm text-muted-foreground font-semibold">
{t('clickOrDropToImage')} {t('clickOrDropToImage')}
</span> </span>
<span className="text-xs text-gray-400">{t('supportedFormats')}</span> <span className="text-xs text-muted-foreground">{t('supportedFormats')}</span>
</div> </div>
)} )}
</div> </div>
@@ -239,24 +239,24 @@ export default function ImageMode() {
)} )}
{result && ( {result && (
<div className="p-4 rounded-xl bg-blue-50 border border-blue-200"> <div className="p-4 rounded-xl bg-primary/10 border border-primary/30">
<div className="flex justify-between items-center mb-2"> <div className="flex justify-between items-center mb-2">
<span className="text-xs font-bold text-gray-500">{t('base64Output')}</span> <span className="text-xs font-bold text-muted-foreground">{t('base64Output')}</span>
<div className="flex gap-1"> <div className="flex gap-1">
<CopyButton text={result.rawBase64} tooltip={t('copyRaw')} /> <CopyButton text={result.rawBase64} tooltip={t('copyRaw')} />
<CopyButton text={result.output} tooltip={t('copyDataUri')} color="info" /> <CopyButton text={result.output} tooltip={t('copyDataUri')} color="info" />
</div> </div>
</div> </div>
<div className="font-mono text-xs break-all max-h-[200px] overflow-y-auto leading-relaxed text-blue-600 font-semibold"> <div className="font-mono text-xs break-all max-h-[200px] overflow-y-auto leading-relaxed text-primary font-semibold">
{result.output.length > 2000 {result.output.length > 2000
? `${result.output.substring(0, 2000)}...` ? `${result.output.substring(0, 2000)}...`
: result.output} : result.output}
</div> </div>
<div className="flex gap-4 mt-2"> <div className="flex gap-4 mt-2">
<span className="text-xs text-gray-400"> <span className="text-xs text-muted-foreground">
{t('originalSize')}: {formatFileSize(result.originalBytes)} {t('originalSize')}: {formatFileSize(result.originalBytes)}
</span> </span>
<span className="text-xs text-gray-400"> <span className="text-xs text-muted-foreground">
{t('encodedSize')}: {formatFileSize(result.outputBytes)} {t('encodedSize')}: {formatFileSize(result.outputBytes)}
</span> </span>
</div> </div>
@@ -267,7 +267,7 @@ export default function ImageMode() {
<button <button
type="button" type="button"
onClick={handleClear} onClick={handleClear}
className="flex items-center gap-1 px-3 py-2 text-sm text-gray-500 hover:bg-gray-100 rounded-lg transition-colors" className="flex items-center gap-1 px-3 py-2 text-sm text-muted-foreground hover:bg-accent rounded-lg transition-colors"
> >
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
{t('clear')} {t('clear')}
+5 -5
View File
@@ -120,12 +120,12 @@ export default function TextMode({ onSwitchToImageMode }: TextModeProps = {}) {
/> />
{showImageHint && ( {showImageHint && (
<div className="flex items-center justify-between p-3 rounded-lg bg-blue-50 border border-blue-200"> <div className="flex items-center justify-between p-3 rounded-lg bg-primary/10 border border-primary/30">
<span className="text-sm text-blue-700">{t('imageDataUriHint')}</span> <span className="text-sm text-primary">{t('imageDataUriHint')}</span>
<button <button
type="button" type="button"
onClick={onSwitchToImageMode} onClick={onSwitchToImageMode}
className="px-3 py-1 text-sm font-medium text-blue-600 hover:bg-blue-100 rounded-md transition-colors" className="px-3 py-1 text-sm font-medium text-primary hover:bg-blue-100 rounded-md transition-colors"
> >
{t('switchToImageMode')} {t('switchToImageMode')}
</button> </button>
@@ -133,9 +133,9 @@ export default function TextMode({ onSwitchToImageMode }: TextModeProps = {}) {
)} )}
{output && ( {output && (
<div className="p-4 rounded-xl bg-blue-50 border border-blue-200"> <div className="p-4 rounded-xl bg-primary/10 border border-primary/30">
<div className="flex justify-between items-center mb-2"> <div className="flex justify-between items-center mb-2">
<span className="text-xs font-bold text-gray-500">{outputLabel}</span> <span className="text-xs font-bold text-muted-foreground">{outputLabel}</span>
<CopyButton text={output} /> <CopyButton text={output} />
</div> </div>
<TextInputArea <TextInputArea
+5 -7
View File
@@ -33,7 +33,7 @@ export default function ToolCard({
return ( return (
<div <div
className="group relative rounded-2xl border border-gray-200 h-full box-border transition-all duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] hover:-translate-y-1" className="group relative rounded-2xl border border-border h-full box-border transition-all duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] hover:-translate-y-1"
style={{ style={{
borderColor: undefined, borderColor: undefined,
['--hover-color' as string]: colorCode, ['--hover-color' as string]: colorCode,
@@ -65,18 +65,18 @@ export default function ToolCard({
<IconComponent size={20} /> <IconComponent size={20} />
</div> </div>
<div> <div>
<span className="font-bold text-sm leading-tight text-gray-900 flex items-center gap-1"> <span className="font-bold text-sm leading-tight text-foreground flex items-center gap-1">
{title} {title}
</span> </span>
{description && ( {description && (
<span className="block text-xs text-gray-500 font-medium mt-1 overflow-hidden text-ellipsis whitespace-nowrap max-w-[200px]"> <span className="block text-xs text-muted-foreground font-medium mt-1 overflow-hidden text-ellipsis whitespace-nowrap max-w-[200px]">
{description} {description}
</span> </span>
)} )}
</div> </div>
</div> </div>
<ChevronRight <ChevronRight
className="w-3 h-3 text-gray-300 mt-1 transition-all duration-300 ease-in-out group-hover:translate-x-1" className="w-3 h-3 text-muted-foreground mt-1 transition-all duration-300 ease-in-out group-hover:translate-x-1"
style={{ color: undefined }} style={{ color: undefined }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
e.currentTarget.style.color = colorCode; e.currentTarget.style.color = colorCode;
@@ -88,9 +88,7 @@ export default function ToolCard({
</div> </div>
{snapshot != null && ( {snapshot != null && (
<div className="mt-auto pt-4 border-t border-dashed border-gray-200 w-full"> <div className="mt-auto pt-4 border-t border-dashed border-border w-full">{snapshot}</div>
{snapshot}
</div>
)} )}
</button> </button>
</div> </div>
+15 -13
View File
@@ -65,14 +65,14 @@ export default function HtmlToMarkdownPage() {
<button <button
onClick={handleDownload} onClick={handleDownload}
disabled={!result.markdown} disabled={!result.markdown}
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors" className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-foreground bg-background border border-input rounded-lg hover:bg-muted disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
> >
<Download size={14} /> <Download size={14} />
{t('download')} {t('download')}
</button> </button>
<button <button
onClick={handleClear} onClick={handleClear}
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors" className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-foreground bg-background border border-input rounded-lg hover:bg-muted transition-colors"
> >
<Trash2 size={14} /> <Trash2 size={14} />
{t('clear')} {t('clear')}
@@ -95,10 +95,10 @@ export default function HtmlToMarkdownPage() {
> >
{/* HTML 输入区 */} {/* HTML 输入区 */}
{showInput && ( {showInput && (
<div className="border border-gray-200 rounded-xl overflow-hidden flex flex-col"> <div className="border border-border rounded-xl overflow-hidden flex flex-col">
<div className="flex items-center justify-between px-4 py-2 bg-blue-50/50 border-b border-gray-200"> <div className="flex items-center justify-between px-4 py-2 bg-primary/5 border-b border-border">
<span className="text-xs font-bold text-gray-500">{t('inputLabel')}</span> <span className="text-xs font-bold text-muted-foreground">{t('inputLabel')}</span>
<span className="text-xs text-gray-400"> <span className="text-xs text-muted-foreground">
{t('charCount', { count: html.length })} {t('charCount', { count: html.length })}
</span> </span>
</div> </div>
@@ -113,15 +113,15 @@ export default function HtmlToMarkdownPage() {
{/* Markdown 输出区 */} {/* Markdown 输出区 */}
{showOutput && ( {showOutput && (
<div className="border border-gray-200 rounded-xl overflow-hidden flex flex-col"> <div className="border border-border rounded-xl overflow-hidden flex flex-col">
<div className="flex items-center justify-between px-4 py-2 bg-blue-50/50 border-b border-gray-200"> <div className="flex items-center justify-between px-4 py-2 bg-primary/5 border-b border-border">
<span className="text-xs font-bold text-gray-500"> <span className="text-xs font-bold text-muted-foreground">
{(previewMode as string) === 'markdown' {(previewMode as string) === 'markdown'
? t('markdownOutputLabel') ? t('markdownOutputLabel')
: t('previewLabel')} : t('previewLabel')}
</span> </span>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span className="text-xs text-gray-400"> <span className="text-xs text-muted-foreground">
{t('charCount', { count: result.markdownLength })} {t('charCount', { count: result.markdownLength })}
</span> </span>
<CopyButton text={result.markdown} size="small" /> <CopyButton text={result.markdown} size="small" />
@@ -132,11 +132,13 @@ export default function HtmlToMarkdownPage() {
<textarea <textarea
value={result.markdown} value={result.markdown}
readOnly readOnly
className="flex-1 min-h-[400px] p-4 font-mono text-[0.85rem] leading-relaxed resize-none focus:outline-none bg-gray-50" className="flex-1 min-h-[400px] p-4 font-mono text-[0.85rem] leading-relaxed resize-none focus:outline-none bg-muted"
/> />
) : ( ) : (
<div className="flex-1 p-4 min-h-[400px] overflow-auto bg-white font-mono text-[0.85rem] leading-relaxed whitespace-pre-wrap break-words"> <div className="flex-1 p-4 min-h-[400px] overflow-auto bg-background font-mono text-[0.85rem] leading-relaxed whitespace-pre-wrap break-words">
{result.markdown || <span className="text-gray-400">{t('emptyHint')}</span>} {result.markdown || (
<span className="text-muted-foreground">{t('emptyHint')}</span>
)}
</div> </div>
)} )}
</div> </div>
+3 -3
View File
@@ -14,14 +14,14 @@ export default function DiffNavigator({ total, currentIndex, onPrev, onNext }: D
if (total === 0) { if (total === 0) {
return ( return (
<div className="flex items-center justify-center gap-3 p-2.5 rounded-lg bg-blue-50 border border-blue-200"> <div className="flex items-center justify-center gap-3 p-2.5 rounded-lg bg-primary/10 border border-primary/30">
<span className="text-sm font-bold text-gray-500">{t('jsonDiff:noDiffs')}</span> <span className="text-sm font-bold text-muted-foreground">{t('jsonDiff:noDiffs')}</span>
</div> </div>
); );
} }
return ( return (
<div className="flex items-center justify-center gap-3 p-2.5 rounded-lg bg-blue-50 border border-blue-200"> <div className="flex items-center justify-center gap-3 p-2.5 rounded-lg bg-primary/10 border border-primary/30">
<button <button
type="button" type="button"
aria-label={t('jsonDiff:previousDiff')} aria-label={t('jsonDiff:previousDiff')}
+3 -3
View File
@@ -27,14 +27,14 @@ export default function DiffResult({ result, viewMode, activePath }: DiffResultP
} }
return ( return (
<div className="p-3 rounded-lg bg-white border border-gray-200 font-mono text-sm overflow-x-auto min-h-[200px] max-h-[480px] overflow-y-auto"> <div className="p-3 rounded-lg bg-background border border-border font-mono text-sm overflow-x-auto min-h-[200px] max-h-[480px] overflow-y-auto">
<UnifiedView node={result.root} depth={0} activePath={activePath} /> <UnifiedView node={result.root} depth={0} activePath={activePath} />
</div> </div>
); );
} }
const SectionLabel = ({ text }: { text: string }) => ( const SectionLabel = ({ text }: { text: string }) => (
<span className="block mb-1.5 text-[11px] font-extrabold tracking-wider text-gray-500 uppercase"> <span className="block mb-1.5 text-[11px] font-extrabold tracking-wider text-muted-foreground uppercase">
{text} {text}
</span> </span>
); );
@@ -61,7 +61,7 @@ const colorForType = (type: DiffType): string => {
if (type === 'added') return 'text-green-600'; if (type === 'added') return 'text-green-600';
if (type === 'removed') return 'text-red-600'; if (type === 'removed') return 'text-red-600';
if (type === 'modified') return 'text-amber-600'; if (type === 'modified') return 'text-amber-600';
return 'text-gray-900'; return 'text-foreground';
}; };
const bgForType = (type: DiffType): string | undefined => { const bgForType = (type: DiffType): string | undefined => {
+9 -7
View File
@@ -120,17 +120,17 @@ export default function JsonConvertSection({
{/* 转换结果 */} {/* 转换结果 */}
{result && result.output ? ( {result && result.output ? (
<div className="relative rounded-lg bg-white border border-gray-200 overflow-hidden"> <div className="relative rounded-lg bg-background border border-border overflow-hidden">
{/* 结果头部 */} {/* 结果头部 */}
<div className="flex justify-between items-center px-4 py-2 border-b border-gray-200 bg-gray-50"> <div className="flex justify-between items-center px-4 py-2 border-b border-border bg-muted">
<div className="flex gap-4 items-center"> <div className="flex gap-4 items-center">
<span className="text-[11px] font-extrabold text-gray-500"> <span className="text-[11px] font-extrabold text-muted-foreground">
{t(`jsonFormat:${pk}OutputLabel`)} {t(`jsonFormat:${pk}OutputLabel`)}
</span> </span>
<span className="text-[10px] text-gray-400"> <span className="text-[10px] text-muted-foreground">
{t('jsonFormat:originalSize')}: {formatByteSize(result.originalBytes)} {t('jsonFormat:originalSize')}: {formatByteSize(result.originalBytes)}
</span> </span>
<span className="text-[10px] text-gray-400"> <span className="text-[10px] text-muted-foreground">
{t('jsonFormat:formattedSize')}: {formatByteSize(result.outputBytes)} {t('jsonFormat:formattedSize')}: {formatByteSize(result.outputBytes)}
</span> </span>
</div> </div>
@@ -143,8 +143,10 @@ export default function JsonConvertSection({
</div> </div>
</div> </div>
) : ( ) : (
<div className="p-4 rounded-lg bg-gray-50 border border-dashed border-gray-300 text-center"> <div className="p-4 rounded-lg bg-muted border border-dashed border-input text-center">
<p className="text-sm font-semibold text-gray-500">{t(`jsonFormat:${pk}EmptyHint`)}</p> <p className="text-sm font-semibold text-muted-foreground">
{t(`jsonFormat:${pk}EmptyHint`)}
</p>
</div> </div>
)} )}
</div> </div>
+1 -1
View File
@@ -17,7 +17,7 @@ export default function JsonDiffInput({
}: JsonDiffInputProps) { }: JsonDiffInputProps) {
return ( return (
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<span className="block mb-1.5 text-[11px] font-extrabold tracking-wider text-gray-500 uppercase"> <span className="block mb-1.5 text-[11px] font-extrabold tracking-wider text-muted-foreground uppercase">
{label} {label}
</span> </span>
<TextInputArea <TextInputArea
+11 -11
View File
@@ -73,7 +73,7 @@ export default function JsonFormatSection() {
<div className="flex flex-col sm:flex-row gap-3 justify-between items-stretch sm:items-center"> <div className="flex flex-col sm:flex-row gap-3 justify-between items-stretch sm:items-center">
<div className="flex gap-3 items-center"> <div className="flex gap-3 items-center">
{/* 缩进选择 */} {/* 缩进选择 */}
<span className="text-[11px] font-extrabold text-gray-500"> <span className="text-[11px] font-extrabold text-muted-foreground">
{t('jsonFormat:indentSize')} {t('jsonFormat:indentSize')}
</span> </span>
<SwitchButtonGroup <SwitchButtonGroup
@@ -90,7 +90,7 @@ export default function JsonFormatSection() {
type="checkbox" type="checkbox"
checked={sortKeys} checked={sortKeys}
onChange={(e) => setSortKeys(e.target.checked)} onChange={(e) => setSortKeys(e.target.checked)}
className="h-4 w-4 rounded border-gray-300 text-blue-500 focus:ring-blue-500" className="h-4 w-4 rounded border-input text-blue-500 focus:ring-primary"
/> />
<span className="text-xs font-bold">{t('jsonFormat:sortKeys')}</span> <span className="text-xs font-bold">{t('jsonFormat:sortKeys')}</span>
</label> </label>
@@ -119,8 +119,8 @@ export default function JsonFormatSection() {
onChange={(e) => setInput(e.target.value)} onChange={(e) => setInput(e.target.value)}
rows={6} rows={6}
className={`w-full rounded-lg border ${ className={`w-full rounded-lg border ${
error ? 'border-red-300' : 'border-gray-200' error ? 'border-red-300' : 'border-border'
} p-3 font-mono text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 resize-y`} } p-3 font-mono text-sm focus:outline-none focus:ring-2 focus:ring-primary resize-y`}
/> />
{error && ( {error && (
<p className="mx-3 mt-1 text-xs font-semibold text-red-500"> <p className="mx-3 mt-1 text-xs font-semibold text-red-500">
@@ -131,17 +131,17 @@ export default function JsonFormatSection() {
{/* 格式化结果 */} {/* 格式化结果 */}
{result && result.formatted ? ( {result && result.formatted ? (
<div className="relative rounded-lg bg-white border border-gray-200 overflow-hidden"> <div className="relative rounded-lg bg-background border border-border overflow-hidden">
{/* 结果头部 */} {/* 结果头部 */}
<div className="flex justify-between items-center px-4 py-2 border-b border-gray-200 bg-gray-50"> <div className="flex justify-between items-center px-4 py-2 border-b border-border bg-muted">
<div className="flex gap-4 items-center"> <div className="flex gap-4 items-center">
<span className="text-[11px] font-extrabold text-gray-500"> <span className="text-[11px] font-extrabold text-muted-foreground">
{t('jsonFormat:outputLabel')} {t('jsonFormat:outputLabel')}
</span> </span>
<span className="text-[10px] text-gray-400"> <span className="text-[10px] text-muted-foreground">
{t('jsonFormat:originalSize')}: {formatByteSize(result.originalBytes)} {t('jsonFormat:originalSize')}: {formatByteSize(result.originalBytes)}
</span> </span>
<span className="text-[10px] text-gray-400"> <span className="text-[10px] text-muted-foreground">
{t('jsonFormat:formattedSize')}: {formatByteSize(result.formattedBytes)} {t('jsonFormat:formattedSize')}: {formatByteSize(result.formattedBytes)}
</span> </span>
</div> </div>
@@ -154,8 +154,8 @@ export default function JsonFormatSection() {
</div> </div>
</div> </div>
) : ( ) : (
<div className="p-4 rounded-lg bg-gray-50 border border-dashed border-gray-300 text-center"> <div className="p-4 rounded-lg bg-muted border border-dashed border-input text-center">
<p className="text-sm font-semibold text-gray-500">{t('jsonFormat:emptyHint')}</p> <p className="text-sm font-semibold text-muted-foreground">{t('jsonFormat:emptyHint')}</p>
</div> </div>
)} )}
</div> </div>
+10 -10
View File
@@ -121,17 +121,17 @@ const NodeRow = ({
onClick={() => setOverride(expanded ? 'closed' : 'open')} onClick={() => setOverride(expanded ? 'closed' : 'open')}
className={`cursor-pointer pr-1 py-0.5 ${bg ?? ''} ${ className={`cursor-pointer pr-1 py-0.5 ${bg ?? ''} ${
isActive ? 'ring-2 ring-blue-500 rounded' : '' isActive ? 'ring-2 ring-blue-500 rounded' : ''
} flex items-center gap-1 whitespace-nowrap hover:${bg ? 'bg-opacity-80' : 'bg-gray-50'}`} } flex items-center gap-1 whitespace-nowrap hover:${bg ? 'bg-opacity-80' : 'bg-muted'}`}
style={{ paddingLeft: `${depth * 1.5}rem` }} style={{ paddingLeft: `${depth * 1.5}rem` }}
> >
<span className="w-3 text-gray-500 text-[11px]">{expanded ? '▾' : '▸'}</span> <span className="w-3 text-muted-foreground text-[11px]">{expanded ? '▾' : '▸'}</span>
{!isRoot && ( {!isRoot && (
<span className="text-gray-900 font-bold">{isArrayKeyDisplay(node.key)}:</span> <span className="text-foreground font-bold">{isArrayKeyDisplay(node.key)}:</span>
)} )}
<span className="text-gray-500">{open}</span> <span className="text-muted-foreground">{open}</span>
{!expanded && <span className="text-gray-400 italic">{summarize(value)}</span>} {!expanded && <span className="text-muted-foreground italic">{summarize(value)}</span>}
{!expanded && ( {!expanded && (
<span className="text-gray-500"> <span className="text-muted-foreground">
{close} {close}
{isLastChild ? '' : ','} {isLastChild ? '' : ','}
</span> </span>
@@ -154,7 +154,7 @@ const NodeRow = ({
)} )}
{expanded && ( {expanded && (
<div <div
className="text-gray-500 whitespace-nowrap" className="text-muted-foreground whitespace-nowrap"
style={{ paddingLeft: `${depth * 1.5 + 1.0625}rem` }} style={{ paddingLeft: `${depth * 1.5 + 1.0625}rem` }}
> >
{close} {close}
@@ -175,8 +175,8 @@ const NodeRow = ({
style={{ paddingLeft: `${depth * 1.5}rem` }} style={{ paddingLeft: `${depth * 1.5}rem` }}
> >
<span className="w-3" /> <span className="w-3" />
{!isRoot && <span className="text-gray-900 font-bold">{isArrayKeyDisplay(node.key)}:</span>} {!isRoot && <span className="text-foreground font-bold">{isArrayKeyDisplay(node.key)}:</span>}
<span className={valueColor ?? 'text-gray-900'}> <span className={valueColor ?? 'text-foreground'}>
{formatPrimitive(value)} {formatPrimitive(value)}
{isLastChild ? '' : ','} {isLastChild ? '' : ','}
</span> </span>
@@ -206,7 +206,7 @@ export default function JsonTree({
}: JsonTreeProps) { }: JsonTreeProps) {
const sideKey = useMemo(() => side, [side]); const sideKey = useMemo(() => side, [side]);
return ( return (
<div className="p-3 rounded-lg bg-white border border-gray-200 font-mono text-sm overflow-x-auto min-h-[200px] max-h-[480px] overflow-y-auto"> <div className="p-3 rounded-lg bg-background border border-border font-mono text-sm overflow-x-auto min-h-[200px] max-h-[480px] overflow-y-auto">
<NodeRow <NodeRow
node={node} node={node}
side={sideKey} side={sideKey}
+4 -2
View File
@@ -219,8 +219,10 @@ export default function Index() {
<DiffResult result={diffResult} viewMode={viewMode} activePath={activePath} /> <DiffResult result={diffResult} viewMode={viewMode} activePath={activePath} />
</> </>
) : ( ) : (
<div className="p-4 rounded-lg bg-gray-50 border border-dashed border-gray-300 text-center"> <div className="p-4 rounded-lg bg-muted border border-dashed border-input text-center">
<p className="text-sm font-semibold text-gray-500">{t('jsonDiff:emptyHint')}</p> <p className="text-sm font-semibold text-muted-foreground">
{t('jsonDiff:emptyHint')}
</p>
</div> </div>
)} )}
</> </>
+4 -4
View File
@@ -32,7 +32,7 @@ const Section = ({ title, content, color }: SectionProps) => {
<CopyButton text={JSON.stringify(content)} size="small" /> <CopyButton text={JSON.stringify(content)} size="small" />
</div> </div>
</div> </div>
<pre className="m-0 p-3 bg-white/60 rounded-lg text-xs font-mono overflow-x-auto whitespace-pre-wrap break-all border border-black/5"> <pre className="m-0 p-3 bg-background/60 rounded-lg text-xs font-mono overflow-x-auto whitespace-pre-wrap break-all border border-black/5">
{content ? stringifyJson(content) : t('jwt:invalidFormat')} {content ? stringifyJson(content) : t('jwt:invalidFormat')}
</pre> </pre>
</div> </div>
@@ -91,14 +91,14 @@ export default function Index() {
content={result.payload} content={result.payload}
color="#d63aff" // JWT.io Payload Color color="#d63aff" // JWT.io Payload Color
/> />
<div className="p-4 rounded-xl border border-blue-200 bg-blue-50"> <div className="p-4 rounded-xl border border-primary/30 bg-primary/10">
<div className="flex justify-between items-center mb-2"> <div className="flex justify-between items-center mb-2">
<span className="text-sm font-bold tracking-wide text-blue-600"> <span className="text-sm font-bold tracking-wide text-primary">
{t('jwt:signatureTitle')} {t('jwt:signatureTitle')}
</span> </span>
<CopyButton text={JSON.stringify(result.raw.signature)} size="small" /> <CopyButton text={JSON.stringify(result.raw.signature)} size="small" />
</div> </div>
<span className="block text-sm font-mono break-all text-gray-500 bg-white/60 p-3 rounded-lg border border-black/5"> <span className="block text-sm font-mono break-all text-muted-foreground bg-background/60 p-3 rounded-lg border border-black/5">
{result.signature || t('jwt:noSignature')} {result.signature || t('jwt:noSignature')}
</span> </span>
</div> </div>
+13 -13
View File
@@ -168,21 +168,21 @@ export default function MarkdownToHtmlPage() {
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
onClick={handleClear} onClick={handleClear}
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors" className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-foreground bg-background border border-input rounded-lg hover:bg-muted transition-colors"
> >
<Trash2 size={14} /> <Trash2 size={14} />
{t('clear')} {t('clear')}
</button> </button>
<button <button
onClick={handlePrint} onClick={handlePrint}
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors" className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-foreground bg-background border border-input rounded-lg hover:bg-muted transition-colors"
> >
<Printer size={14} /> <Printer size={14} />
{t('print')} {t('print')}
</button> </button>
<button <button
onClick={handleDownload} onClick={handleDownload}
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors" className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-foreground bg-background border border-input rounded-lg hover:bg-muted transition-colors"
> >
<Download size={14} /> <Download size={14} />
{t('download')} {t('download')}
@@ -205,10 +205,10 @@ export default function MarkdownToHtmlPage() {
> >
{/* Markdown 输入区 */} {/* Markdown 输入区 */}
{showInput && ( {showInput && (
<div className="border border-gray-200 rounded-xl overflow-hidden flex flex-col"> <div className="border border-border rounded-xl overflow-hidden flex flex-col">
<div className="flex items-center justify-between px-4 py-2 bg-blue-50/50 border-b border-gray-200"> <div className="flex items-center justify-between px-4 py-2 bg-primary/5 border-b border-border">
<span className="text-xs font-bold text-gray-500">{t('inputLabel')}</span> <span className="text-xs font-bold text-muted-foreground">{t('inputLabel')}</span>
<span className="text-xs text-gray-400"> <span className="text-xs text-muted-foreground">
{t('charCount', { count: markdown.length })} {t('charCount', { count: markdown.length })}
</span> </span>
</div> </div>
@@ -223,13 +223,13 @@ export default function MarkdownToHtmlPage() {
{/* 预览/输出区 */} {/* 预览/输出区 */}
{showPreview && ( {showPreview && (
<div className="border border-gray-200 rounded-xl overflow-hidden flex flex-col"> <div className="border border-border rounded-xl overflow-hidden flex flex-col">
<div className="flex items-center justify-between px-4 py-2 bg-blue-50/50 border-b border-gray-200"> <div className="flex items-center justify-between px-4 py-2 bg-primary/5 border-b border-border">
<span className="text-xs font-bold text-gray-500"> <span className="text-xs font-bold text-muted-foreground">
{(previewMode as string) === 'html' ? t('htmlOutputLabel') : t('previewLabel')} {(previewMode as string) === 'html' ? t('htmlOutputLabel') : t('previewLabel')}
</span> </span>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span className="text-xs text-gray-400"> <span className="text-xs text-muted-foreground">
{t('charCount', { count: result.htmlLength })} {t('charCount', { count: result.htmlLength })}
</span> </span>
<CopyButton text={result.html} size="small" /> <CopyButton text={result.html} size="small" />
@@ -240,10 +240,10 @@ export default function MarkdownToHtmlPage() {
<textarea <textarea
value={result.html} value={result.html}
readOnly readOnly
className="flex-1 min-h-[400px] p-4 font-mono text-[0.8rem] leading-relaxed resize-none focus:outline-none bg-gray-50" className="flex-1 min-h-[400px] p-4 font-mono text-[0.8rem] leading-relaxed resize-none focus:outline-none bg-muted"
/> />
) : ( ) : (
<div className="flex-1 p-4 min-h-[400px] overflow-auto bg-white"> <div className="flex-1 p-4 min-h-[400px] overflow-auto bg-background">
<iframe <iframe
ref={iframeRef} ref={iframeRef}
title="markdown-preview" title="markdown-preview"
+3 -3
View File
@@ -8,8 +8,8 @@ interface AutoRefreshToggleProps {
export default function AutoRefreshToggle({ autoRefresh, onChange }: AutoRefreshToggleProps) { export default function AutoRefreshToggle({ autoRefresh, onChange }: AutoRefreshToggleProps) {
const { t } = useLazyTranslation('storageCleaner'); const { t } = useLazyTranslation('storageCleaner');
return ( return (
<div className="mb-3 p-4 rounded-lg bg-white border border-gray-200 flex justify-between items-center shadow-sm transition-all hover:shadow-md"> <div className="mb-3 p-4 rounded-lg bg-background border border-border flex justify-between items-center shadow-sm transition-all hover:shadow-md">
<span className="text-sm font-bold text-gray-700 px-3"> <span className="text-sm font-bold text-foreground px-3">
{t('storageCleaner:autoRefresh')} {t('storageCleaner:autoRefresh')}
</span> </span>
<label className="relative inline-flex items-center cursor-pointer"> <label className="relative inline-flex items-center cursor-pointer">
@@ -19,7 +19,7 @@ export default function AutoRefreshToggle({ autoRefresh, onChange }: AutoRefresh
onChange={(e) => onChange(e.target.checked)} onChange={(e) => onChange(e.target.checked)}
className="sr-only peer" className="sr-only peer"
/> />
<div className="w-9 h-5 bg-gray-200 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-amber-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-amber-500" /> <div className="w-9 h-5 bg-gray-200 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-amber-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-background after:border-input after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-amber-500" />
</label> </label>
</div> </div>
); );
+1 -1
View File
@@ -27,7 +27,7 @@ export default function CleaningResult({ result }: CleaningResultProps) {
)} )}
<span <span
className={`text-sm font-semibold leading-relaxed ${ className={`text-sm font-semibold leading-relaxed ${
isSuccess ? 'text-green-700' : 'text-red-700' isSuccess ? 'text-green-600' : 'text-red-700'
}`} }`}
> >
{isSuccess {isSuccess
+1 -1
View File
@@ -13,7 +13,7 @@ export default function ErrorDisplay({ error }: ErrorDisplayProps) {
<div className="flex flex-col items-center justify-center rounded-lg p-6 shadow-lg border border-red-200 bg-red-50"> <div className="flex flex-col items-center justify-center rounded-lg p-6 shadow-lg border border-red-200 bg-red-50">
<AlertCircle className="h-9 w-9 text-red-500 mb-3" /> <AlertCircle className="h-9 w-9 text-red-500 mb-3" />
<p className="text-sm font-bold leading-relaxed mb-4 text-red-600">{error}</p> <p className="text-sm font-bold leading-relaxed mb-4 text-red-600">{error}</p>
<p className="text-xs font-medium leading-relaxed text-gray-500"> <p className="text-xs font-medium leading-relaxed text-muted-foreground">
{t('storageCleaner:errorStandardOnly')} {t('storageCleaner:errorStandardOnly')}
</p> </p>
</div> </div>
+5 -5
View File
@@ -22,23 +22,23 @@ export default function OptionItem({
className={`flex justify-between items-center py-2 px-3 rounded-lg transition-all duration-200 ${ className={`flex justify-between items-center py-2 px-3 rounded-lg transition-all duration-200 ${
checked checked
? 'bg-amber-50 border border-amber-200' ? 'bg-amber-50 border border-amber-200'
: 'bg-transparent border border-transparent hover:bg-gray-50 hover:shadow-sm' : 'bg-transparent border border-transparent hover:bg-muted hover:shadow-sm'
}`} }`}
> >
<div className="flex-1 min-w-0 mr-3"> <div className="flex-1 min-w-0 mr-3">
<span <span
className={`block text-xs leading-tight truncate transition-colors ${ className={`block text-xs leading-tight truncate transition-colors ${
checked ? 'text-amber-700 font-bold' : 'text-gray-900 font-bold' checked ? 'text-amber-700 font-bold' : 'text-foreground font-bold'
}`} }`}
> >
{t(labelKey)} {t(labelKey)}
</span> </span>
{size !== undefined && size > 0 ? ( {size !== undefined && size > 0 ? (
<span className="block text-[10px] font-semibold text-gray-500 mt-0.5 opacity-80"> <span className="block text-[10px] font-semibold text-muted-foreground mt-0.5 opacity-80">
{isCount ? `${size} ${t('storageCleaner:countUnit')}` : formatSize(size)} {isCount ? `${size} ${t('storageCleaner:countUnit')}` : formatSize(size)}
</span> </span>
) : ( ) : (
<span className="block text-[10px] font-medium text-gray-400 mt-0.5 italic"> <span className="block text-[10px] font-medium text-muted-foreground mt-0.5 italic">
{t('storageCleaner:noData')} {t('storageCleaner:noData')}
</span> </span>
)} )}
@@ -47,7 +47,7 @@ export default function OptionItem({
type="checkbox" type="checkbox"
checked={checked} checked={checked}
onChange={onChange} onChange={onChange}
className="h-4 w-4 rounded border-gray-300 text-amber-500 focus:ring-amber-500" className="h-4 w-4 rounded border-input text-amber-500 focus:ring-amber-500"
/> />
</div> </div>
); );
+6 -4
View File
@@ -31,7 +31,7 @@ export default function StorageOptionsGrid({
]; ];
return ( return (
<div className="mb-3 border rounded-lg bg-white shadow-sm overflow-hidden transition-all hover:shadow-md"> <div className="mb-3 border rounded-lg bg-background shadow-sm overflow-hidden transition-all hover:shadow-md">
<div className="p-3"> <div className="p-3">
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
{optionKeys.map(({ key, isCount }) => ( {optionKeys.map(({ key, isCount }) => (
@@ -48,13 +48,15 @@ export default function StorageOptionsGrid({
</div> </div>
</div> </div>
<div className="border-t" /> <div className="border-t" />
<div className="flex justify-between items-center px-5 py-2 transition-colors hover:bg-gray-50 rounded-b-lg"> <div className="flex justify-between items-center px-5 py-2 transition-colors hover:bg-muted rounded-b-lg">
<span className="text-xs font-bold text-gray-500">{t('storageCleaner:selectAll')}</span> <span className="text-xs font-bold text-muted-foreground">
{t('storageCleaner:selectAll')}
</span>
<input <input
type="checkbox" type="checkbox"
checked={allSelected} checked={allSelected}
onChange={(e) => onSelectAll(e.target.checked)} onChange={(e) => onSelectAll(e.target.checked)}
className="h-4 w-4 rounded border-gray-300 text-amber-500 focus:ring-amber-500" className="h-4 w-4 rounded border-input text-amber-500 focus:ring-amber-500"
/> />
</div> </div>
</div> </div>
+3 -1
View File
@@ -66,7 +66,9 @@ export default function Index() {
borderColor: `${textStatisticsPageStyles.primaryColor}1a`, borderColor: `${textStatisticsPageStyles.primaryColor}1a`,
}} }}
> >
<span className="text-gray-500 whitespace-nowrap mb-0 md:mb-1">{item.label}</span> <span className="text-muted-foreground whitespace-nowrap mb-0 md:mb-1">
{item.label}
</span>
<span <span
className="break-all font-semibold" className="break-all font-semibold"
style={{ color: textStatisticsPageStyles.primaryColor }} style={{ color: textStatisticsPageStyles.primaryColor }}
+4 -4
View File
@@ -37,17 +37,17 @@ const LiveClock = React.memo(({ unit, onUseNow }: LiveClockProps) => {
}, [now, showMessage, t]); }, [now, showMessage, t]);
return ( return (
<div className="flex items-center gap-3 px-4 py-2 mb-4 bg-blue-50 rounded-lg border border-blue-100"> <div className="flex items-center gap-3 px-4 py-2 mb-4 bg-primary/10 rounded-lg border border-primary/20">
<span className="text-blue-700 font-bold text-[0.65rem] uppercase tracking-wider whitespace-nowrap"> <span className="text-primary font-bold text-[0.65rem] uppercase tracking-wider whitespace-nowrap">
{t('timestamp:currentTs')} {t('timestamp:currentTs')}
</span> </span>
<span className="flex-1 font-mono font-bold text-blue-700 text-sm tracking-tight leading-tight overflow-hidden text-ellipsis"> <span className="flex-1 font-mono font-bold text-primary text-sm tracking-tight leading-tight overflow-hidden text-ellipsis">
{displayVal} {displayVal}
</span> </span>
<button <button
onClick={handleUseNow} onClick={handleUseNow}
title={t('timestamp:useNowTooltip')} title={t('timestamp:useNowTooltip')}
className="p-1.5 rounded-md bg-white text-blue-700 shadow-sm hover:bg-blue-700 hover:text-white transition-colors" className="p-1.5 rounded-md bg-background text-primary shadow-sm hover:bg-blue-700 hover:text-white transition-colors"
> >
<Clock className="w-4 h-4" /> <Clock className="w-4 h-4" />
</button> </button>
+7 -7
View File
@@ -37,7 +37,7 @@ const ResultView = React.memo(
if (!result) { if (!result) {
if (!showEmptyPlaceholder) return null; if (!showEmptyPlaceholder) return null;
return ( return (
<div className="flex-1 flex items-center justify-center text-gray-400 text-sm font-semibold py-12 text-center"> <div className="flex-1 flex items-center justify-center text-muted-foreground text-sm font-semibold py-12 text-center">
{t('timestamp:resultEmpty')} {t('timestamp:resultEmpty')}
</div> </div>
); );
@@ -45,12 +45,12 @@ const ResultView = React.memo(
return ( return (
<div className="animate-in fade-in duration-300"> <div className="animate-in fade-in duration-300">
<span className="block text-gray-500 mb-3 text-xs font-bold"> <span className="block text-muted-foreground mb-3 text-xs font-bold">
{t('timestamp:resultLabel')} {t('timestamp:resultLabel')}
</span> </span>
<div className="bg-blue-50 p-5 rounded-xl relative mb-4 border border-blue-200 flex justify-between items-center"> <div className="bg-primary/10 p-5 rounded-xl relative mb-4 border border-primary/30 flex justify-between items-center">
<span className="font-mono font-bold text-blue-700 break-all pr-4 text-xl tracking-tight leading-tight"> <span className="font-mono font-bold text-primary break-all pr-4 text-xl tracking-tight leading-tight">
{result} {result}
</span> </span>
<CopyButton <CopyButton
@@ -61,18 +61,18 @@ const ResultView = React.memo(
/> />
</div> </div>
<div className="bg-blue-50/50 p-4 rounded-xl border border-blue-100 flex flex-col gap-3"> <div className="bg-primary/5 p-4 rounded-xl border border-primary/20 flex flex-col gap-3">
{[ {[
{ label: t('timestamp:relativeTime'), value: extraInfo?.relative }, { label: t('timestamp:relativeTime'), value: extraInfo?.relative },
{ label: t('timestamp:iso8601'), value: extraInfo?.iso }, { label: t('timestamp:iso8601'), value: extraInfo?.iso },
{ label: t('timestamp:utcTime'), value: extraInfo?.utc }, { label: t('timestamp:utcTime'), value: extraInfo?.utc },
].map((item) => ( ].map((item) => (
<div key={item.label} className="flex justify-between items-center"> <div key={item.label} className="flex justify-between items-center">
<span className="text-gray-400 font-bold text-xs pr-2 whitespace-nowrap"> <span className="text-muted-foreground font-bold text-xs pr-2 whitespace-nowrap">
{item.label} {item.label}
</span> </span>
<div className="flex items-center gap-2 min-w-0"> <div className="flex items-center gap-2 min-w-0">
<span className="font-mono text-blue-700 font-semibold text-xs break-all text-right"> <span className="font-mono text-primary font-semibold text-xs break-all text-right">
{item.value} {item.value}
</span> </span>
{item.value && ( {item.value && (
+6 -6
View File
@@ -41,7 +41,7 @@ export default function Index() {
{/* 桌面端 md+ 左右分栏;移动端单栏堆叠 */} {/* 桌面端 md+ 左右分栏;移动端单栏堆叠 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 items-stretch"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4 items-stretch">
{/* 左栏:转换工作台 */} {/* 左栏:转换工作台 */}
<div className="p-5 rounded-xl bg-white border border-gray-200 shadow-sm"> <div className="p-5 rounded-xl bg-background border border-border shadow-sm">
{/* 模式切换 */} {/* 模式切换 */}
<SwitchButtonGroup <SwitchButtonGroup
value={mode} value={mode}
@@ -63,8 +63,8 @@ export default function Index() {
value={mode === 'ts2dt' ? tsInput : dtInput} value={mode === 'ts2dt' ? tsInput : dtInput}
onChange={(e) => setInput(e.target.value)} onChange={(e) => setInput(e.target.value)}
className={`w-full px-4 py-2.5 rounded-lg border ${ className={`w-full px-4 py-2.5 rounded-lg border ${
error ? 'border-red-500' : 'border-gray-300' error ? 'border-red-500' : 'border-input'
} bg-white text-sm font-mono font-semibold focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all`} } bg-background text-sm font-mono font-semibold focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all`}
/> />
{error && <p className="text-red-500 text-xs mt-1">{error}</p>} {error && <p className="text-red-500 text-xs mt-1">{error}</p>}
@@ -84,7 +84,7 @@ export default function Index() {
<select <select
value={zone} value={zone}
onChange={(e) => setZone(e.target.value as typeof zone)} onChange={(e) => setZone(e.target.value as typeof zone)}
className="flex-1 min-w-0 px-4 py-2.5 rounded-lg border border-gray-300 bg-white text-sm font-mono font-semibold focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all" className="flex-1 min-w-0 px-4 py-2.5 rounded-lg border border-input bg-background text-sm font-mono font-semibold focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all"
> >
{ZONES.map((z) => ( {ZONES.map((z) => (
<option key={z} value={z} className="text-sm font-semibold"> <option key={z} value={z} className="text-sm font-semibold">
@@ -98,14 +98,14 @@ export default function Index() {
{/* 立即转换 */} {/* 立即转换 */}
<button <button
onClick={convert} onClick={convert}
className="block mx-auto mt-4 mb-1 max-w-[240px] w-full py-2.5 text-sm font-semibold rounded-lg bg-blue-600 text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-all" className="block mx-auto mt-4 mb-1 max-w-[240px] w-full py-2.5 text-sm font-semibold rounded-lg bg-primary text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 transition-all"
> >
{t('timestamp:convertButton')} {t('timestamp:convertButton')}
</button> </button>
</div> </div>
{/* 右栏:结果展示卡片 */} {/* 右栏:结果展示卡片 */}
<div className="p-5 rounded-xl bg-white border border-gray-200 shadow-sm h-full flex flex-col"> <div className="p-5 rounded-xl bg-background border border-border shadow-sm h-full flex flex-col">
<ResultView result={result} mode={mode} unit={unit} zone={zone} showEmptyPlaceholder /> <ResultView result={result} mode={mode} unit={unit} zone={zone} showEmptyPlaceholder />
</div> </div>
</div> </div>
+27 -1
View File
@@ -1,5 +1,6 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
export default { export default {
darkMode: 'class',
content: [ content: [
'./entrypoints/**/*.{js,ts,jsx,tsx}', './entrypoints/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}',
@@ -9,7 +10,32 @@ export default {
'./config/**/*.{js,ts,jsx,tsx}', './config/**/*.{js,ts,jsx,tsx}',
], ],
theme: { theme: {
extend: {}, extend: {
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
card: 'hsl(var(--card))',
'card-foreground': 'hsl(var(--card-foreground))',
popover: 'hsl(var(--popover))',
'popover-foreground': 'hsl(var(--popover-foreground))',
primary: 'hsl(var(--primary))',
'primary-foreground': 'hsl(var(--primary-foreground))',
secondary: 'hsl(var(--secondary))',
'secondary-foreground': 'hsl(var(--secondary-foreground))',
muted: 'hsl(var(--muted))',
'muted-foreground': 'hsl(var(--muted-foreground))',
accent: 'hsl(var(--accent))',
'accent-foreground': 'hsl(var(--accent-foreground))',
destructive: 'hsl(var(--destructive))',
'destructive-foreground': 'hsl(var(--destructive-foreground))',
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
},
borderRadius: {
DEFAULT: 'var(--radius)',
},
},
}, },
plugins: [], plugins: [],
}; };