feat(ui): migrate more components to shadcn/ui

Continue migrating components to use shadcn/ui for better consistency
and dark mode support.

## Components Migrated

### ConfirmDialog
- Replace Modal with shadcn AlertDialog component
- Use AlertDialogAction and AlertDialogCancel with proper asChild prop
- Update icon colors for dark mode support

### UpdateDialog
- Replace buttons with shadcn Button component
- Use shadcn Card for version info display
- Add shadcn ScrollArea for changelog view
- Update styling with shadcn color tokens

### PlaywrightDownloadDialog
- Replace progress bar with shadcn Progress component
- Use shadcn Card for stats display
- Update colors for dark mode support
- Use shadcn Button throughout

### CleanerSidebar
- Replace card divs with shadcn Card component
- Add shadcn ScrollArea for manager list
- Update radio button styling with shadcn tokens
- Improve dark mode support

### CleanerToolbar
- Replace all buttons with shadcn Button component
- Use shadcn Separator for visual separation
- Update toolbar styling with shadcn tokens

### Button Component Enhancement
- Add React.forwardRef for ref forwarding support
- Enables proper ref usage with parent components

## Improvements
- Full dark mode support across all migrated components
- Consistent styling using shadcn design tokens
- Better accessibility with semantic components
- Ref forwarding support for Button component

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Misaka_Company
2026-04-01 10:48:54 +08:00
parent 8ef08be649
commit c99907c0ff
6 changed files with 346 additions and 266 deletions

View File

@@ -1,6 +1,10 @@
import React, { useEffect, useState, useCallback } from 'react'
import { DownloadCloud, LoaderCircle, X } from 'lucide-react'
import Modal from './ui/Modal'
import { Button } from './ui/Button'
import { Progress } from './ui/shadcn/progress'
import { Card, CardContent } from './ui/shadcn/card'
interface DownloadProgress {
percent: number // 0-100
downloadedBytes: number
@@ -137,23 +141,18 @@ export default function PlaywrightDownloadDialog({
{/* Progress Bar */}
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-slate-600"></span>
<span className="font-semibold text-blue-600">{progress?.percent ?? 0}%</span>
</div>
<div className="h-3 w-full overflow-hidden rounded-full bg-slate-200">
<div
className="h-full bg-gradient-to-r from-blue-500 to-blue-600 transition-all duration-300 ease-out"
style={{ width: `${progress?.percent ?? 0}%` }}
/>
<span className="text-muted-foreground"></span>
<span className="font-semibold text-primary">{progress?.percent ?? 0}%</span>
</div>
<Progress value={progress?.percent ?? 0} className="h-3" />
</div>
{/* Current File */}
{progress?.currentFile && (
<div className="rounded-lg bg-slate-50 px-4 py-3">
<div className="text-xs font-medium text-slate-500"></div>
<div className="rounded-lg bg-muted/50 px-4 py-3">
<div className="text-xs font-medium text-muted-foreground"></div>
<div
className="mt-1 truncate text-sm text-slate-700"
className="mt-1 truncate text-sm text-foreground"
title={progress.currentFile}
>
{progress.currentFile}
@@ -163,54 +162,63 @@ export default function PlaywrightDownloadDialog({
{/* Stats Grid */}
<div className="grid grid-cols-2 gap-3">
<div className="rounded-lg border border-slate-200 px-4 py-3">
<div className="text-xs text-slate-500"></div>
<div className="mt-1 text-lg font-semibold text-slate-900">
<Card>
<CardContent className="p-4">
<div className="text-xs text-muted-foreground"></div>
<div className="mt-1 text-lg font-semibold text-foreground">
{progress ? formatBytes(progress.downloadedBytes) : '0 B'}
</div>
</div>
<div className="rounded-lg border border-slate-200 px-4 py-3">
<div className="text-xs text-slate-500"></div>
<div className="mt-1 text-lg font-semibold text-slate-900">
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="text-xs text-muted-foreground"></div>
<div className="mt-1 text-lg font-semibold text-foreground">
{progress && progress.totalBytes > 0
? formatBytes(progress.totalBytes)
: '计算中...'}
</div>
</div>
<div className="rounded-lg border border-slate-200 px-4 py-3">
<div className="text-xs text-slate-500"></div>
<div className="mt-1 text-lg font-semibold text-slate-900">
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="text-xs text-muted-foreground"></div>
<div className="mt-1 text-lg font-semibold text-foreground">
{progress && progress.speed >= 0
? `${formatBytes(progress.speed)}/秒`
: '计算中...'}
</div>
</div>
<div className="rounded-lg border border-slate-200 px-4 py-3">
<div className="text-xs text-slate-500"></div>
<div className="mt-1 text-lg font-semibold text-slate-900">
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="text-xs text-muted-foreground"></div>
<div className="mt-1 text-lg font-semibold text-foreground">
{progress?.eta !== undefined && progress.eta >= 0
? formatETA(progress.eta)
: '计算中...'}
</div>
</div>
</CardContent>
</Card>
</div>
{/* Cancel Button */}
<div className="flex justify-center pt-2">
<button
<Button
variant="danger"
onClick={handleCancelDownloadClick}
className="inline-flex items-center gap-2 rounded-md border border-rose-200 bg-rose-50 px-4 py-2 text-sm text-rose-700 hover:bg-rose-100"
className="gap-2"
>
<X size={16} />
</button>
</Button>
</div>
</div>
)}
{/* Error Section */}
{error && (
<div className="rounded-lg border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700">
<div className="rounded-lg border border-destructive/20 bg-destructive/10 px-4 py-3 text-sm text-destructive dark:bg-destructive/20">
<div className="font-medium"></div>
<div className="mt-1">{error}</div>
</div>
@@ -218,7 +226,7 @@ export default function PlaywrightDownloadDialog({
{/* Success Section (shown briefly before closing) */}
{!isDownloading && !error && progress?.percent === 100 && (
<div className="rounded-lg border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-700">
<div className="rounded-lg border border-green-200 bg-green-50 dark:border-green-800/30 dark:bg-green-900/20 px-4 py-3 text-sm text-green-700 dark:text-green-400">
<div className="flex items-center gap-2">
<DownloadCloud size={18} />
<span className="font-medium"></span>
@@ -230,8 +238,8 @@ export default function PlaywrightDownloadDialog({
{/* Initial Loading State */}
{isDownloading && !progress && (
<div className="flex flex-col items-center justify-center py-8">
<LoaderCircle size={48} className="animate-spin text-blue-500" />
<div className="mt-4 text-sm text-slate-600">...</div>
<LoaderCircle size={48} className="animate-spin text-primary" />
<div className="mt-4 text-sm text-muted-foreground">...</div>
</div>
)}
</div>
@@ -246,24 +254,25 @@ export default function PlaywrightDownloadDialog({
isAlertDialog
>
<div className="space-y-4">
<div className="rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800">
<div className="rounded-lg border border-amber-200 bg-amber-50 dark:border-amber-800/30 dark:bg-amber-900/20 px-4 py-3 text-sm text-amber-800 dark:text-amber-400">
<div className="font-medium"></div>
<div className="mt-1">退</div>
</div>
<div className="flex justify-end gap-3">
<button
<Button
variant="secondary"
onClick={handleCloseConfirm}
className="rounded-md border border-slate-200 px-4 py-2 text-sm text-slate-600 hover:bg-slate-50"
>
</button>
<button
</Button>
<Button
variant="danger"
onClick={handleConfirmCancel}
className="inline-flex items-center gap-2 rounded-md bg-rose-600 px-4 py-2 text-sm font-medium text-white hover:bg-rose-700"
className="gap-2"
>
<X size={16} />
退
</button>
</Button>
</div>
</div>
</Modal>

View File

@@ -3,6 +3,9 @@ import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import { DownloadCloud, LoaderCircle, RefreshCw } from 'lucide-react'
import Modal from './ui/Modal'
import { Button } from './ui/Button'
import { ScrollArea } from './ui/shadcn/scroll-area'
import { Card, CardContent, CardHeader, CardTitle } from './ui/shadcn/card'
import { useUpdateDialogState } from '../hooks/useUpdateDialogState'
import type { UserType } from '../../../main/types/user.types'
import type {
@@ -44,7 +47,7 @@ export default function UpdateDialog({
const renderReleaseList = (title: string, releases: UpdateRelease[]) => {
if (releases.length === 0) {
return (
<div className="rounded-lg border border-dashed border-slate-200 px-3 py-4 text-sm text-slate-500">
<div className="rounded-lg border border-dashed border-border px-3 py-4 text-sm text-muted-foreground">
</div>
)
@@ -52,7 +55,7 @@ export default function UpdateDialog({
return (
<div className="space-y-2">
<div className="text-xs font-semibold uppercase tracking-wide text-slate-500">{title}</div>
<div className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">{title}</div>
{releases.map((release) => {
const selected = isSelectedRelease(release)
return (
@@ -65,19 +68,19 @@ export default function UpdateDialog({
}}
className={`w-full rounded-lg border px-3 py-3 text-left transition ${
selected
? 'border-blue-500 bg-blue-50 text-blue-900'
: 'border-slate-200 bg-white text-slate-700 hover:border-slate-300'
? 'border-primary bg-primary/10 text-primary'
: 'border-border bg-background text-foreground hover:bg-accent'
}`}
>
<div className="flex items-center justify-between gap-3">
<span className="font-medium">V{release.version}</span>
<span className="rounded-full bg-slate-100 px-2 py-0.5 text-xs uppercase text-slate-600">
<span className="rounded-full bg-muted px-2 py-0.5 text-xs uppercase text-muted-foreground">
{release.channel}
</span>
</div>
<div className="mt-1 text-xs text-slate-500">{release.publishedAt}</div>
<div className="mt-1 text-xs text-muted-foreground">{release.publishedAt}</div>
{release.notesSummary && (
<div className="mt-2 text-xs text-slate-600">{release.notesSummary}</div>
<div className="mt-2 text-xs text-foreground/80">{release.notesSummary}</div>
)}
</button>
)
@@ -96,21 +99,23 @@ export default function UpdateDialog({
disableEscapeKey={isBusy}
>
<div className="space-y-4">
<div className="flex items-center justify-between rounded-lg bg-slate-50 px-4 py-3 text-sm">
<div className="text-slate-600">
<span className="font-semibold text-slate-900">V{status?.currentVersion}</span>
<span className="ml-2 rounded-full bg-slate-200 px-2 py-0.5 text-xs uppercase text-slate-700">
<div className="flex items-center justify-between rounded-lg bg-muted/50 px-4 py-3 text-sm">
<div className="text-muted-foreground">
<span className="font-semibold text-foreground">V{status?.currentVersion}</span>
<span className="ml-2 rounded-full bg-muted px-2 py-0.5 text-xs uppercase text-muted-foreground">
{status?.currentChannel ?? __APP_CHANNEL__}
</span>
</div>
<button
<Button
variant="secondary"
size="sm"
onClick={() => void onRefreshCatalog()}
className="inline-flex items-center gap-2 rounded-md border border-slate-200 px-3 py-1.5 text-xs text-slate-600 hover:bg-slate-100"
disabled={isBusy}
className="gap-2"
>
<RefreshCw size={14} />
</button>
</Button>
</div>
{catalog?.mode === 'admin' ? (
@@ -119,104 +124,106 @@ export default function UpdateDialog({
{renderReleaseList('Stable', catalog.channels?.stable ?? [])}
{renderReleaseList('Preview', catalog.channels?.preview ?? [])}
</div>
<div className="rounded-xl border border-slate-200 bg-white">
<div className="border-b border-slate-200 px-4 py-3">
<div className="text-sm font-semibold text-slate-900">
<Card>
<CardHeader>
<CardTitle className="text-sm">
{selectedRelease
? `${selectedRelease.channel.toUpperCase()} V${selectedRelease.version}`
: '请选择一个版本'}
</div>
<div className="mt-1 text-xs text-slate-500">
</CardTitle>
<div className="mt-1 text-xs text-muted-foreground">
{selectedRelease?.notesSummary ?? '选择版本后可查看详细更新说明。'}
</div>
</div>
<div className="max-h-[420px] overflow-auto px-4 py-4 prose prose-slate max-w-none prose-headings:mb-2 prose-p:my-2 prose-li:my-1">
</CardHeader>
<CardContent className="max-h-[420px]">
<ScrollArea className="h-[340px]">
{isLoadingChangelog ? (
<div className="flex items-center gap-2 text-sm text-slate-500">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<LoaderCircle size={16} className="animate-spin" />
...
</div>
) : (
<div className="prose prose-slate dark:prose-invert max-w-none prose-headings:mb-2 prose-p:my-2 prose-li:my-1">
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{changelog || '暂无更新说明。'}
</ReactMarkdown>
</div>
)}
</div>
</div>
</ScrollArea>
</CardContent>
</Card>
</div>
) : (
<div className="rounded-xl border border-slate-200 bg-white">
<div className="border-b border-slate-200 px-4 py-3">
<div className="text-sm font-semibold text-slate-900">
<Card>
<CardHeader>
<CardTitle className="text-sm">
{selectedRelease ? `发现稳定版 V${selectedRelease.version}` : '暂无可用更新'}
</div>
<div className="mt-1 text-xs text-slate-500">
</CardTitle>
<div className="mt-1 text-xs text-muted-foreground">
{status?.currentChannel === 'preview'
? '当前设备正在运行预览版,普通用户将更新回稳定版。'
: '更新包已在后台准备完成,确认后将自动关闭并重启应用。'}
</div>
</div>
<div className="max-h-[420px] overflow-auto px-4 py-4 prose prose-slate max-w-none prose-headings:mb-2 prose-p:my-2 prose-li:my-1">
</CardHeader>
<CardContent className="max-h-[420px]">
<ScrollArea className="h-[340px]">
{isLoadingChangelog ? (
<div className="flex items-center gap-2 text-sm text-slate-500">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<LoaderCircle size={16} className="animate-spin" />
...
</div>
) : (
<div className="prose prose-slate dark:prose-invert max-w-none prose-headings:mb-2 prose-p:my-2 prose-li:my-1">
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{changelog || '暂无更新说明。'}
</ReactMarkdown>
</div>
)}
</div>
</div>
</ScrollArea>
</CardContent>
</Card>
)}
{status?.error && (
<div className="rounded-lg border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700">
<div className="rounded-lg border border-destructive/20 bg-destructive/10 px-4 py-3 text-sm text-destructive dark:bg-destructive/20">
{status.error}
</div>
)}
<div className="flex items-center justify-between border-t border-slate-200 pt-4">
<div className="text-sm text-slate-500">{status?.message ?? ' '}</div>
<div className="flex items-center justify-between border-t border-border pt-4">
<div className="text-sm text-muted-foreground">{status?.message ?? ' '}</div>
<div className="flex items-center gap-3">
<button
<Button
variant="secondary"
onClick={onClose}
className="rounded-md border border-slate-200 px-4 py-2 text-sm text-slate-600 hover:bg-slate-50"
disabled={isBusy}
>
</button>
</Button>
{userType === 'Admin' ? (
<button
<Button
onClick={() =>
selectedRelease
? void onDownloadAndInstallAdminRelease(selectedRelease)
: undefined
}
className="inline-flex items-center gap-2 rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:cursor-not-allowed disabled:bg-blue-300"
disabled={!selectedRelease || isBusy}
loading={isBusy}
className="gap-2"
>
{isBusy ? (
<LoaderCircle size={16} className="animate-spin" />
) : (
<DownloadCloud size={16} />
)}
{isBusy ? null : <DownloadCloud size={16} />}
</button>
</Button>
) : (
<button
<Button
onClick={() => void onInstallUserRelease()}
className="inline-flex items-center gap-2 rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:cursor-not-allowed disabled:bg-blue-300"
disabled={!selectedRelease || isBusy}
loading={isBusy}
className="gap-2"
>
{isBusy ? (
<LoaderCircle size={16} className="animate-spin" />
) : (
<DownloadCloud size={16} />
)}
{isBusy ? null : <DownloadCloud size={16} />}
</button>
</Button>
)}
</div>
</div>

View File

@@ -1,5 +1,8 @@
import React from 'react'
import { DatabaseZap, Layers, Users } from 'lucide-react'
import { Card } from '../ui/shadcn/card'
import { ScrollArea } from '../ui/shadcn/scroll-area'
import { cn } from '@renderer/lib/utils'
interface CleanerSidebarProps {
valMode: 'full' | 'filtered'
@@ -18,15 +21,20 @@ export function CleanerSidebar({
}: CleanerSidebarProps): React.JSX.Element {
return (
<div className="w-full xl:w-[380px] flex-shrink-0 flex flex-col gap-5 xl:self-start overflow-auto">
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-5">
<h3 className="text-base font-semibold mb-4 flex items-center gap-2 text-slate-800">
<Card className="p-5">
<h3 className="text-base font-semibold mb-4 flex items-center gap-2">
<DatabaseZap size={18} className="text-blue-500" />
</h3>
<div className="space-y-3">
<label
className={`flex items-start gap-3 p-3 rounded-lg border cursor-pointer transition-colors ${valMode === 'full' ? 'bg-blue-50 border-blue-200' : 'hover:bg-slate-50 border-slate-200'}`}
className={cn(
"flex items-start gap-3 p-3 rounded-lg border cursor-pointer transition-colors",
valMode === 'full'
? 'bg-primary/10 border-primary'
: 'hover:bg-muted/50 border-border'
)}
>
<input
type="radio"
@@ -36,13 +44,18 @@ export function CleanerSidebar({
onChange={() => setValMode('full')}
/>
<div>
<div className="text-sm font-medium text-slate-800"> - </div>
<div className="text-xs text-slate-500 mt-0.5"></div>
<div className="text-sm font-medium"> - </div>
<div className="text-xs text-muted-foreground mt-0.5"></div>
</div>
</label>
<label
className={`flex items-start gap-3 p-3 rounded-lg border cursor-pointer transition-colors ${valMode === 'filtered' ? 'bg-blue-50 border-blue-200' : 'hover:bg-slate-50 border-slate-200'}`}
className={cn(
"flex items-start gap-3 p-3 rounded-lg border cursor-pointer transition-colors",
valMode === 'filtered'
? 'bg-primary/10 border-primary'
: 'hover:bg-muted/50 border-border'
)}
>
<input
type="radio"
@@ -52,10 +65,10 @@ export function CleanerSidebar({
onChange={() => setValMode('filtered')}
/>
<div className="w-full">
<div className="text-sm font-medium text-slate-800"> - ProductionID </div>
<div className="text-xs text-slate-500 mt-0.5"></div>
<div className="text-sm font-medium"> - ProductionID </div>
<div className="text-xs text-muted-foreground mt-0.5"></div>
{valMode === 'filtered' && (
<div className="mt-3 text-xs text-blue-700 bg-blue-100/50 rounded p-2 flex items-start gap-1.5">
<div className="mt-3 text-xs text-primary bg-primary/10 rounded p-2 flex items-start gap-1.5">
<Layers size={14} className="mt-0.5 flex-shrink-0" />
<span>
使<strong></strong>
@@ -65,39 +78,40 @@ export function CleanerSidebar({
</div>
</label>
</div>
</div>
</Card>
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-5">
<Card className="p-5">
<div className="flex items-center justify-between mb-3">
<h3 className="text-base font-semibold flex items-center gap-2 text-slate-800">
<h3 className="text-base font-semibold flex items-center gap-2">
<Users size={18} className="text-indigo-500" />
()
</h3>
<div className="flex gap-2">
<button
onClick={() => setSelectedManagers(new Set(managers))}
className="text-xs text-blue-600 hover:underline"
className="text-xs text-primary hover:underline"
>
</button>
<button
onClick={() => setSelectedManagers(new Set())}
className="text-xs text-slate-500 hover:underline"
className="text-xs text-muted-foreground hover:underline"
>
</button>
</div>
</div>
<div className="grid grid-cols-2 gap-2 mt-3 max-h-[120px] overflow-y-auto pr-1">
<ScrollArea className="h-[120px] pr-1">
<div className="grid grid-cols-2 gap-2">
{managers.map((manager) => (
<label
key={manager}
className="flex items-center gap-2 text-sm text-slate-700 cursor-pointer hover:bg-slate-50 p-1.5 rounded"
className="flex items-center gap-2 text-sm cursor-pointer hover:bg-muted/50 p-1.5 rounded"
>
<input
type="checkbox"
className="rounded text-blue-600"
className="rounded text-primary"
checked={selectedManagers.has(manager)}
onChange={(e) => {
setSelectedManagers((prev) => {
@@ -111,9 +125,10 @@ export function CleanerSidebar({
{manager || '未分配'}
</label>
))}
{managers.length === 0 && <div className="text-sm text-slate-400"></div>}
</div>
{managers.length === 0 && <div className="text-sm text-muted-foreground"></div>}
</div>
</ScrollArea>
</Card>
</div>
)
}

View File

@@ -10,6 +10,8 @@ import {
Settings2,
Square
} from 'lucide-react'
import { Button } from '../ui/Button'
import { Separator } from '../ui/shadcn/separator'
import type { ValidationResult } from '../../hooks/cleaner/types'
interface CleanerToolbarProps {
@@ -48,25 +50,29 @@ export function CleanerToolbar({
}: CleanerToolbarProps): React.JSX.Element {
return (
<>
<div className="p-4 border-b border-slate-200 flex-shrink-0">
<button
<div className="p-4 border-b border-border flex-shrink-0">
<Button
onClick={() => void handleValidation()}
disabled={isValidationRunning}
className="w-full bg-slate-100 hover:bg-slate-200 text-slate-800 py-3 rounded-lg font-medium transition-colors shadow-sm flex justify-center items-center gap-2 border border-slate-200 disabled:opacity-50"
className="w-full gap-2"
loading={isValidationRunning}
>
<Search size={18} /> {isValidationRunning ? '正在获取...' : '获取并校验物料状态'}
</button>
</Button>
</div>
<div className="bg-slate-50 border-b border-slate-200 px-4 py-3 flex justify-between items-center flex-shrink-0">
<div className="bg-muted/30 border-b border-border px-4 py-3 flex justify-between items-center flex-shrink-0">
<div className="flex flex-wrap items-center gap-2">
<button
<Button
size="sm"
onClick={() => setSelectedItems(new Set(filteredResults.map((r) => r.materialCode)))}
className="text-xs bg-white border border-slate-300 text-slate-700 px-2.5 py-1.5 rounded shadow-sm hover:bg-slate-50 flex items-center gap-1"
variant="secondary"
className="gap-1"
>
<CheckSquare size={14} className="text-blue-600" />
</button>
<button
<CheckSquare size={14} className="text-primary" />
</Button>
<Button
size="sm"
onClick={() => {
const visibleCodes = new Set(filteredResults.map((r) => r.materialCode))
setSelectedItems((prev) => {
@@ -77,14 +83,16 @@ export function CleanerToolbar({
return next
})
}}
className="text-xs bg-white border border-slate-300 text-slate-700 px-2.5 py-1.5 rounded shadow-sm hover:bg-slate-50 flex items-center gap-1"
variant="secondary"
className="gap-1"
>
<Square size={14} className="text-slate-400" />
</button>
<Square size={14} className="text-muted-foreground" />
</Button>
<div className="w-px h-4 bg-slate-300 mx-1"></div>
<Separator orientation="vertical" className="h-4 mx-1" />
<button
<Button
size="sm"
onClick={() => {
const checkedCodes = filteredResults
.filter((r) => selectedItems.has(r.materialCode))
@@ -93,49 +101,58 @@ export function CleanerToolbar({
setHiddenItems((prev) => new Set([...prev, ...checkedCodes]))
}
}}
className="text-xs bg-white border border-slate-300 text-slate-700 px-2.5 py-1.5 rounded shadow-sm hover:bg-slate-50 flex items-center gap-1"
variant="secondary"
className="gap-1"
>
<EyeOff size={14} />
</button>
<button
</Button>
<Button
size="sm"
onClick={() => setHiddenItems(new Set())}
className="text-xs bg-white border border-slate-300 text-slate-700 px-2.5 py-1.5 rounded shadow-sm hover:bg-slate-50 flex items-center gap-1"
variant="secondary"
className="gap-1"
>
<Eye size={14} />
</button>
</Button>
<div className="w-px h-4 bg-slate-300 mx-1"></div>
<Separator orientation="vertical" className="h-4 mx-1" />
<button
<Button
size="sm"
onClick={() => void handleConfirmDeletion()}
disabled={isRunning || validationResults.length === 0}
className="text-xs bg-indigo-50 border border-indigo-200 text-indigo-700 px-3 py-1.5 rounded shadow-sm hover:bg-indigo-100 flex items-center gap-1.5 font-medium transition-colors disabled:opacity-50"
className="gap-1.5 bg-indigo-50 text-indigo-700 border-indigo-200 hover:bg-indigo-100 dark:bg-indigo-900/20 dark:text-indigo-400 dark:border-indigo-800"
>
<HardDrive size={14} /> ()
</button>
</Button>
</div>
<div className="flex items-center gap-2">
<button
<Button
size="sm"
onClick={() => setIsTypeDialogOpen(true)}
ref={typeManagementButtonRef}
className="text-xs bg-white border border-slate-300 text-slate-700 px-3 py-1.5 rounded shadow-sm hover:bg-slate-50 flex items-center gap-1.5"
variant="secondary"
className="gap-1.5"
>
<Settings2 size={14} />
</button>
<button
</Button>
<Button
size="sm"
onClick={() => void handleExportResults()}
disabled={isExporting || filteredResults.length === 0}
className="text-xs bg-blue-50 border border-blue-200 text-blue-700 px-3 py-1.5 rounded shadow-sm hover:bg-blue-100 flex items-center gap-1.5 font-medium disabled:opacity-50"
loading={isExporting}
className="gap-1.5 bg-blue-50 text-blue-700 border-blue-200 hover:bg-blue-100 dark:bg-blue-900/20 dark:text-blue-400 dark:border-blue-800"
>
<FileSpreadsheet size={14} /> {isExporting ? '导出中...' : '导出结果'}
</button>
<button
</Button>
<Button
size="sm"
onClick={() => setIsReportViewerOpen(true)}
className="text-xs bg-emerald-50 border border-emerald-200 text-emerald-700 px-3 py-1.5 rounded shadow-sm hover:bg-emerald-100 flex items-center gap-1.5 font-medium transition-colors"
className="gap-1.5 bg-emerald-50 text-emerald-700 border-emerald-200 hover:bg-emerald-100 dark:bg-emerald-900/20 dark:text-emerald-400 dark:border-emerald-800"
>
<FileText size={14} />
</button>
</Button>
</div>
</div>
</>

View File

@@ -34,7 +34,8 @@ const sizeMap: Record<ButtonSize, 'default' | 'sm' | 'lg' | 'icon'> = {
lg: 'lg'
}
export function Button({
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({
variant = 'primary',
size = 'md',
loading = false,
@@ -43,12 +44,13 @@ export function Button({
className = '',
disabled,
...props
}: ButtonProps) {
}, ref) => {
const newVariant = variantMap[variant]
const newSize = sizeMap[size]
return (
<ShadcnButton
ref={ref}
variant={newVariant}
size={newSize}
loading={loading}
@@ -60,6 +62,9 @@ export function Button({
{children}
</ShadcnButton>
)
}
}
)
Button.displayName = 'Button'
export { Button }
export default Button

View File

@@ -2,12 +2,23 @@
* ConfirmDialog Component
*
* A confirmation dialog component for displaying confirmation prompts.
* Extends the Modal component with consistent styling and behavior.
* Now uses shadcn/ui AlertDialog component for better accessibility and styling.
*
* Migrated to use shadcn/ui AlertDialog
*/
import React, { useCallback, useEffect } from 'react'
import { AlertTriangle, Info, AlertCircle } from 'lucide-react'
import { Modal } from './Modal'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from './shadcn/alert-dialog'
import { Button } from './Button'
export type ConfirmDialogVariant = 'danger' | 'warning' | 'info'
@@ -29,18 +40,18 @@ const variantStyles: Record<
> = {
danger: {
icon: <AlertCircle className="w-6 h-6" />,
iconColor: 'text-red-500',
buttonVariant: 'danger'
iconColor: 'text-destructive',
buttonVariant: 'danger' as const
},
warning: {
icon: <AlertTriangle className="w-6 h-6" />,
iconColor: 'text-yellow-500',
buttonVariant: 'primary'
iconColor: 'text-amber-500 dark:text-amber-400',
buttonVariant: 'primary' as const
},
info: {
icon: <Info className="w-6 h-6" />,
iconColor: 'text-blue-500',
buttonVariant: 'primary'
iconColor: 'text-primary',
buttonVariant: 'primary' as const
}
}
@@ -78,40 +89,56 @@ export function ConfirmDialog({
const styles = variantStyles[variant]
// For AlertDialog, we use open/onOpenChange
const handleOpenChange = (open: boolean) => {
if (!open) {
onCancel()
}
}
return (
<Modal
isOpen={isOpen}
onClose={onCancel}
title={title}
size="md"
showCloseButton={false}
isAlertDialog={true}
initialFocusSelector="[data-autofocus]"
>
<AlertDialog open={isOpen} onOpenChange={handleOpenChange}>
<AlertDialogContent>
<AlertDialogHeader>
<div className="flex items-start gap-4">
{/* Icon */}
<div className={`flex-shrink-0 ${styles.iconColor}`}>{styles.icon}</div>
{/* Message */}
{/* Title and Description */}
<div className="flex-1">
<AlertDialogTitle>{title}</AlertDialogTitle>
<AlertDialogDescription asChild>
<div className="mt-2">
{typeof message === 'string' ? (
<p className="text-gray-700 whitespace-pre-wrap">{message}</p>
<p className="whitespace-pre-wrap text-foreground">{message}</p>
) : (
message
)}
</div>
</AlertDialogDescription>
</div>
</div>
</AlertDialogHeader>
{/* Buttons */}
<div className="flex justify-end gap-3 mt-6">
<AlertDialogFooter>
<AlertDialogCancel asChild>
<Button variant="secondary" onClick={onCancel}>
{cancelText}
</Button>
<Button data-autofocus="true" variant={styles.buttonVariant} onClick={onConfirm}>
</AlertDialogCancel>
<AlertDialogAction asChild>
<Button
data-autofocus="true"
variant={styles.buttonVariant}
onClick={onConfirm}
>
{confirmText}
</Button>
</div>
</Modal>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}