fix(ux): replace confirm() with ConfirmModal, fix dark-mode colors, add currency format

- Add reusable ConfirmModal component (themed, Escape key, focus trap)
- Replace all native confirm() calls in Orders, ProductLibrary, Materials, Admin, Billing
- Fix ValidationDialog (Upload.tsx) hardcoded bg-white/text-gray-* → semantic tokens
- Fix NewInvoiceModal (Billing.tsx) hardcoded colors → semantic tokens
- Add formatCurrency (Intl.NumberFormat de-DE/EUR) to NewProductOrder wizard
- Resolves audit issues C1, C3, M3

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 19:59:13 +01:00
parent 9f54bc3ab1
commit 915abe9d74
8 changed files with 307 additions and 45 deletions
+118
View File
@@ -0,0 +1,118 @@
import { useEffect, useRef } from 'react'
import { AlertTriangle } from 'lucide-react'
interface ConfirmModalProps {
open: boolean
title: string
message: string
confirmLabel?: string
confirmVariant?: 'danger' | 'primary'
onConfirm: () => void
onCancel: () => void
}
export default function ConfirmModal({
open,
title,
message,
confirmLabel = 'Delete',
confirmVariant = 'danger',
onConfirm,
onCancel,
}: ConfirmModalProps) {
const cancelRef = useRef<HTMLButtonElement>(null)
const dialogRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (!open) return
// Focus cancel button when modal opens
cancelRef.current?.focus()
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
onCancel()
return
}
// Trap focus inside the modal
if (e.key === 'Tab' && dialogRef.current) {
const focusable = dialogRef.current.querySelectorAll<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
)
const first = focusable[0]
const last = focusable[focusable.length - 1]
if (e.shiftKey) {
if (document.activeElement === first) {
e.preventDefault()
last?.focus()
}
} else {
if (document.activeElement === last) {
e.preventDefault()
first?.focus()
}
}
}
}
document.addEventListener('keydown', handleKeyDown)
return () => document.removeEventListener('keydown', handleKeyDown)
}, [open, onCancel])
if (!open) return null
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div
ref={dialogRef}
role="dialog"
aria-modal="true"
aria-labelledby="confirm-modal-title"
className="rounded-xl shadow-2xl w-full max-w-sm mx-4 border"
style={{
backgroundColor: 'var(--color-bg-surface)',
borderColor: 'var(--color-border-default)',
}}
>
<div className="px-6 py-5">
<div className="flex items-start gap-3 mb-4">
<div className="shrink-0 w-9 h-9 rounded-full bg-red-100 flex items-center justify-center">
<AlertTriangle size={18} className="text-red-600" />
</div>
<div>
<h2
id="confirm-modal-title"
className="text-base font-semibold text-content"
>
{title}
</h2>
<p className="text-sm text-content mt-1" style={{ opacity: 0.7 }}>
{message}
</p>
</div>
</div>
<div className="flex justify-end gap-3 mt-6">
<button
ref={cancelRef}
onClick={onCancel}
className="px-4 py-2 text-sm rounded-lg border transition-colors text-content-secondary hover:bg-surface-hover"
style={{ borderColor: 'var(--color-border-default)' }}
>
Cancel
</button>
<button
onClick={onConfirm}
className={`px-4 py-2 text-sm rounded-lg font-medium transition-colors ${
confirmVariant === 'danger'
? 'bg-red-600 hover:bg-red-700 text-white'
: 'bg-accent hover:bg-accent text-white'
}`}
>
{confirmLabel}
</button>
</div>
</div>
</div>
</div>
)
}
+32 -2
View File
@@ -4,6 +4,7 @@ import { toast } from 'sonner'
import { UserPlus, Trash2, Pencil, ChevronDown, ChevronUp, ChevronRight, Settings, RefreshCw, CheckCircle2, XCircle, Clock, DollarSign, Layers, AlertTriangle, Upload, FileBox, Plus, X, LayoutDashboard } from 'lucide-react'
import { Link } from 'react-router-dom'
import api from '../api/client'
import ConfirmModal from '../components/ConfirmModal'
import TemplateEditor from '../components/admin/TemplateEditor'
import PricingTierTable from '../components/admin/PricingTierTable'
import OutputTypeTable from '../components/admin/OutputTypeTable'
@@ -191,6 +192,7 @@ export default function AdminPage() {
const [smtpDraft, setSmtpDraft] = useState<Partial<Settings>>({})
const smtp = { ...settings, ...smtpDraft } as Settings
const [confirmState, setConfirmState] = useState<{ open: boolean; title: string; message: string; onConfirm: () => void }>({ open: false, title: '', message: '', onConfirm: () => {} })
const [showTenantDashboardModal, setShowTenantDashboardModal] = useState(false)
const { data: tenantDefaultWidgets } = useQuery<WidgetConfig[]>({
queryKey: ['tenant-default-dashboard'],
@@ -276,7 +278,17 @@ export default function AdminPage() {
{user.is_active ? 'active' : 'inactive'}
</span>
<button
onClick={() => { if (confirm('Delete user?')) deleteUserMut.mutate(user.id) }}
onClick={() => {
setConfirmState({
open: true,
title: 'Delete User',
message: `Delete user "${user.email}"? This cannot be undone.`,
onConfirm: () => {
deleteUserMut.mutate(user.id)
setConfirmState((s) => ({ ...s, open: false }))
},
})
}}
className="text-content-muted hover:text-red-500 transition-colors"
title="Delete user"
>
@@ -1440,7 +1452,17 @@ function AssetLibraryPanel() {
</button>
<button
className="btn-danger text-xs"
onClick={() => { if (confirm(`Delete "${lib.name}"?`)) deleteMut.mutate(lib.id) }}
onClick={() => {
setConfirmState({
open: true,
title: 'Delete Asset Library',
message: `Delete "${lib.name}"?`,
onConfirm: () => {
deleteMut.mutate(lib.id)
setConfirmState((s) => ({ ...s, open: false }))
},
})
}}
>
<Trash2 size={12} />
</button>
@@ -1484,6 +1506,14 @@ function AssetLibraryPanel() {
})}
</div>
)}
<ConfirmModal
open={confirmState.open}
title={confirmState.title}
message={confirmState.message}
onConfirm={confirmState.onConfirm}
onCancel={() => setConfirmState((s) => ({ ...s, open: false }))}
/>
</div>
)
}
+40 -12
View File
@@ -6,6 +6,7 @@ import {
getInvoices, createInvoice, updateInvoiceStatus, deleteInvoice, downloadInvoicePdf,
type Invoice, type InvoiceCreate,
} from '../api/billing'
import ConfirmModal from '../components/ConfirmModal'
// ── Helpers ───────────────────────────────────────────────────────────────
@@ -37,36 +38,46 @@ function NewInvoiceModal({ onClose, onCreate }: { onClose: () => void; onCreate:
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="bg-white rounded-xl shadow-2xl w-full max-w-md">
<div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
<h2 className="text-lg font-semibold text-gray-900">New Invoice</h2>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600"><X size={18} /></button>
<div
className="rounded-xl shadow-2xl w-full max-w-md border"
style={{ backgroundColor: 'var(--color-bg-surface)', borderColor: 'var(--color-border-default)' }}
>
<div
className="px-6 py-4 border-b flex items-center justify-between"
style={{ borderColor: 'var(--color-border-default)' }}
>
<h2 className="text-lg font-semibold text-content">New Invoice</h2>
<button onClick={onClose} className="text-content-muted hover:text-content"><X size={18} /></button>
</div>
<form onSubmit={handleSubmit} className="px-6 py-4 space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Issue Date</label>
<label className="block text-sm font-medium text-content mb-1">Issue Date</label>
<input
type="date"
value={issuedAt}
onChange={e => setIssuedAt(e.target.value)}
className="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-1 focus:ring-blue-500"
className="w-full px-3 py-2 text-sm border border-border-default rounded-lg focus:outline-none focus:ring-1 focus:ring-accent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Notes</label>
<label className="block text-sm font-medium text-content mb-1">Notes</label>
<textarea
value={notes}
onChange={e => setNotes(e.target.value)}
rows={3}
placeholder="Optional notes..."
className="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-1 focus:ring-blue-500 resize-none"
className="w-full px-3 py-2 text-sm border border-border-default rounded-lg focus:outline-none focus:ring-1 focus:ring-accent resize-none"
/>
</div>
<div className="flex justify-end gap-3 pt-2">
<button type="button" onClick={onClose} className="px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg transition-colors">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-sm text-content-secondary rounded-lg border border-border-default hover:bg-surface-hover transition-colors"
>
Cancel
</button>
<button type="submit" className="px-4 py-2 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
<button type="submit" className="px-4 py-2 text-sm bg-accent text-white rounded-lg transition-colors">
Create Invoice
</button>
</div>
@@ -81,6 +92,7 @@ function NewInvoiceModal({ onClose, onCreate }: { onClose: () => void; onCreate:
export default function BillingPage() {
const qc = useQueryClient()
const [showModal, setShowModal] = useState(false)
const [confirmState, setConfirmState] = useState<{ open: boolean; title: string; message: string; onConfirm: () => void }>({ open: false, title: '', message: '', onConfirm: () => {} })
const { data: invoices = [], isLoading } = useQuery({
queryKey: ['invoices'],
@@ -206,9 +218,17 @@ export default function BillingPage() {
{inv.status === 'draft' && (
<button
onClick={() => {
if (confirm('Delete this draft invoice?')) deleteMutation.mutate(inv.id)
setConfirmState({
open: true,
title: 'Delete Invoice',
message: `Delete draft invoice ${inv.invoice_number}?`,
onConfirm: () => {
deleteMutation.mutate(inv.id)
setConfirmState((s) => ({ ...s, open: false }))
},
})
}}
className="p-1.5 rounded hover:bg-red-100 text-gray-500 hover:text-red-600 transition-colors"
className="p-1.5 rounded hover:bg-red-100 text-content-muted hover:text-red-600 transition-colors"
title="Delete draft"
>
<Trash2 size={15} />
@@ -227,6 +247,14 @@ export default function BillingPage() {
onCreate={data => createMutation.mutate(data)}
/>
)}
<ConfirmModal
open={confirmState.open}
title={confirmState.title}
message={confirmState.message}
onConfirm={confirmState.onConfirm}
onCancel={() => setConfirmState((s) => ({ ...s, open: false }))}
/>
</div>
)
}
+37 -5
View File
@@ -11,6 +11,7 @@ import {
} from '../api/materials'
import type { Material } from '../api/materials'
import MaterialWizard from '../components/MaterialWizard'
import ConfirmModal from '../components/ConfirmModal'
const TYPE_GROUPS = [
{ code: '01', label: 'Metals', icon: Wrench, bg: 'bg-slate-50', border: 'border-slate-200', text: 'text-slate-700' },
@@ -48,6 +49,7 @@ export default function MaterialsPage() {
const [collapsed, setCollapsed] = useState<Set<string | null>>(new Set())
const [expandedAliases, setExpandedAliases] = useState<Set<string>>(new Set())
const [aliasInput, setAliasInput] = useState<Record<string, string>>({})
const [confirmState, setConfirmState] = useState<{ open: boolean; title: string; message: string; onConfirm: () => void }>({ open: false, title: '', message: '', onConfirm: () => {} })
const { data: materials = [], isLoading } = useQuery({
queryKey: ['materials'],
@@ -213,8 +215,15 @@ export default function MaterialsPage() {
</div>
<button
onClick={() => {
if (confirm('Import 35 Schaeffler standard materials? Existing entries will be skipped.'))
seedMut.mutate()
setConfirmState({
open: true,
title: 'Import Standard Materials',
message: 'Import 35 Schaeffler standard materials? Existing entries will be skipped.',
onConfirm: () => {
seedMut.mutate()
setConfirmState((s) => ({ ...s, open: false }))
},
})
}}
disabled={seedMut.isPending}
className="btn-secondary text-sm flex items-center gap-1.5"
@@ -224,8 +233,15 @@ export default function MaterialsPage() {
</button>
<button
onClick={() => {
if (confirm('Seed material aliases from naming scheme mappings? Existing aliases will be skipped.'))
seedAliasMut.mutate()
setConfirmState({
open: true,
title: 'Seed Material Aliases',
message: 'Seed material aliases from naming scheme mappings? Existing aliases will be skipped.',
onConfirm: () => {
seedAliasMut.mutate()
setConfirmState((s) => ({ ...s, open: false }))
},
})
}}
disabled={seedAliasMut.isPending}
className="btn-secondary text-sm flex items-center gap-1.5"
@@ -401,7 +417,15 @@ export default function MaterialsPage() {
</button>
<button
onClick={() => {
if (confirm(`Delete material "${mat.name}"?`)) deleteMut.mutate(mat.id)
setConfirmState({
open: true,
title: 'Delete Material',
message: `Delete material "${mat.name}"?`,
onConfirm: () => {
deleteMut.mutate(mat.id)
setConfirmState((s) => ({ ...s, open: false }))
},
})
}}
className="text-content-muted hover:text-red-500"
title="Delete"
@@ -471,6 +495,14 @@ export default function MaterialsPage() {
</div>
)}
<ConfirmModal
open={confirmState.open}
title={confirmState.title}
message={confirmState.message}
onConfirm={confirmState.onConfirm}
onCancel={() => setConfirmState((s) => ({ ...s, open: false }))}
/>
{/* Wizard modal */}
<MaterialWizard open={showWizard} onClose={() => setShowWizard(false)} />
</div>
+7 -4
View File
@@ -13,6 +13,9 @@ import { estimatePrice } from '../api/pricing'
import type { Product, RenderPosition } from '../api/products'
import type { OutputType } from '../api/outputTypes'
const formatCurrency = (amount: number) =>
new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(amount)
const CATEGORIES = [
{ key: 'TRB', label: 'TRB' },
{ key: 'Kugellager', label: 'Kugellager' },
@@ -620,7 +623,7 @@ export default function NewProductOrderPage() {
<span className="text-sm text-content-muted">
{selectedProducts.size} product{selectedProducts.size !== 1 ? 's' : ''} &middot; {orderLines.length} render job{orderLines.length !== 1 ? 's' : ''}
{priceEstimate && priceEstimate.total > 0 && (
<> &middot; Estimated: <span className="font-semibold text-content-secondary">{priceEstimate.total.toFixed(2)}</span></>
<> &middot; Estimated: <span className="font-semibold text-content-secondary">{formatCurrency(priceEstimate.total)}</span></>
)}
</span>
</div>
@@ -695,7 +698,7 @@ export default function NewProductOrderPage() {
{(() => {
const price = getLinePrice(line.product.id, line.outputType.id)
return price != null ? (
<span className="font-medium text-content-secondary">{price.toFixed(2)}</span>
<span className="font-medium text-content-secondary">{formatCurrency(price)}</span>
) : (
<span className="text-content-muted"></span>
)
@@ -741,7 +744,7 @@ export default function NewProductOrderPage() {
<span className="text-sm text-content-muted">
{orderLines.length} render job{orderLines.length !== 1 ? 's' : ''}
{priceEstimate && priceEstimate.total > 0 && (
<> &middot; Estimated: <span className="font-semibold text-content-secondary">{priceEstimate.total.toFixed(2)}</span></>
<> &middot; Estimated: <span className="font-semibold text-content-secondary">{formatCurrency(priceEstimate.total)}</span></>
)}
</span>
<button
@@ -839,7 +842,7 @@ function ProductOutputRow({
<p className="text-xs text-content-muted">
{ot.renderer} &middot; {ot.output_format.toUpperCase()}
{ot.price_per_item != null && (
<> &middot; <span className="text-emerald-600 font-medium">{ot.price_per_item.toFixed(2)}</span></>
<> &middot; <span className="text-emerald-600 font-medium">{formatCurrency(ot.price_per_item)}</span></>
)}
</p>
</div>
+19 -2
View File
@@ -11,6 +11,7 @@ import { toast } from 'sonner'
import { listOrders, searchOrders, deleteOrder } from '../api/orders'
import { fetchThumbnailBlob } from '../api/cad'
import type { Order, OrderDetail, OrderItem } from '../api/orders'
import ConfirmModal from '../components/ConfirmModal'
// ── Constants ────────────────────────────────────────────────────────────────
const STATUSES = ['draft', 'submitted', 'processing', 'completed', 'rejected'] as const
@@ -49,6 +50,7 @@ export default function OrdersPage() {
const [dateTo, setDateTo] = useState('')
const [showFilters, setShowFilters] = useState(false)
const [selected, setSelected] = useState<Set<string>>(new Set())
const [confirmState, setConfirmState] = useState<{ open: boolean; title: string; message: string; onConfirm: () => void }>({ open: false, title: '', message: '', onConfirm: () => {} })
// Debounce the search input (400 ms)
useEffect(() => {
@@ -161,8 +163,15 @@ export default function OrdersPage() {
const handleDeleteSelected = () => {
const ids = [...selected]
if (!ids.length) return
if (!confirm(`Delete ${ids.length} order${ids.length > 1 ? 's' : ''}? This cannot be undone.`)) return
deleteMut.mutate(ids)
setConfirmState({
open: true,
title: `Delete ${ids.length} order${ids.length > 1 ? 's' : ''}`,
message: 'This cannot be undone.',
onConfirm: () => {
deleteMut.mutate(ids)
setConfirmState((s) => ({ ...s, open: false }))
},
})
}
// ── Render ───────────────────────────────────────────────────────────────
@@ -351,6 +360,14 @@ export default function OrdersPage() {
/>
)}
<ConfirmModal
open={confirmState.open}
title={confirmState.title}
message={confirmState.message}
onConfirm={confirmState.onConfirm}
onCancel={() => setConfirmState((s) => ({ ...s, open: false }))}
/>
{/* ── Bulk delete bar ───────────────────────────────────────────────── */}
{selected.size > 0 && (
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 ml-[120px] z-50
+19 -2
View File
@@ -8,6 +8,7 @@ import {
import { toast } from 'sonner'
import { listProducts, deleteProduct } from '../api/products'
import type { Product } from '../api/products'
import ConfirmModal from '../components/ConfirmModal'
const CATEGORY_LABELS: Record<string, string> = {
TRB: 'TRB',
@@ -119,6 +120,7 @@ export default function ProductLibraryPage() {
const [materialsFilter, setMaterialsFilter] = useState('')
const [view, setView] = useState<'grid' | 'table'>('grid')
const [selected, setSelected] = useState<Set<string>>(new Set())
const [confirmState, setConfirmState] = useState<{ open: boolean; title: string; message: string; onConfirm: () => void }>({ open: false, title: '', message: '', onConfirm: () => {} })
const { data: products, isLoading } = useQuery({
queryKey: ['products', { search, categoryFilter, hasCadFilter, materialsFilter }],
@@ -164,8 +166,15 @@ export default function ProductLibraryPage() {
const handleDeleteSelected = () => {
const ids = [...selected]
if (!ids.length) return
if (!confirm(`Delete ${ids.length} product${ids.length > 1 ? 's' : ''}? This cannot be undone.`)) return
deleteMut.mutate(ids)
setConfirmState({
open: true,
title: `Delete ${ids.length} product${ids.length > 1 ? 's' : ''}`,
message: 'This cannot be undone.',
onConfirm: () => {
deleteMut.mutate(ids)
setConfirmState((s) => ({ ...s, open: false }))
},
})
}
return (
@@ -360,6 +369,14 @@ export default function ProductLibraryPage() {
</div>
)}
<ConfirmModal
open={confirmState.open}
title={confirmState.title}
message={confirmState.message}
onConfirm={confirmState.onConfirm}
onCancel={() => setConfirmState((s) => ({ ...s, open: false }))}
/>
{/* ── Floating action bar ───────────────────────────────────────── */}
{selected.size > 0 && (
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 ml-[120px] bg-gray-900 text-white rounded-lg shadow-xl px-5 py-3 flex items-center gap-4 z-50">
+35 -18
View File
@@ -707,13 +707,19 @@ function ValidationDialog({
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="bg-white rounded-xl shadow-2xl w-full max-w-2xl max-h-[80vh] flex flex-col">
<div
className="rounded-xl shadow-2xl w-full max-w-2xl max-h-[80vh] flex flex-col border"
style={{ backgroundColor: 'var(--color-bg-surface)', borderColor: 'var(--color-border-default)' }}
>
{/* Header */}
<div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
<h2 className="text-lg font-semibold text-gray-900">Import Validation</h2>
<div
className="px-6 py-4 border-b flex items-center justify-between"
style={{ borderColor: 'var(--color-border-default)' }}
>
<h2 className="text-lg font-semibold text-content">Import Validation</h2>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 text-xl leading-none"
className="text-content-muted hover:text-content text-xl leading-none"
>
×
</button>
@@ -721,8 +727,8 @@ function ValidationDialog({
<div className="flex-1 overflow-y-auto px-6 py-4 space-y-4">
{isLoading ? (
<div className="flex items-center gap-3 text-gray-500">
<div className="w-5 h-5 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
<div className="flex items-center gap-3 text-content-muted">
<div className="w-5 h-5 border-2 border-accent border-t-transparent rounded-full animate-spin" />
<span>Validating import...</span>
</div>
) : (
@@ -756,16 +762,20 @@ function ValidationDialog({
.map((row) => (
<div
key={row.row_index}
className="border border-gray-200 rounded-lg overflow-hidden"
className="rounded-lg overflow-hidden border"
style={{ borderColor: 'var(--color-border-default)' }}
>
<button
className={`w-full flex items-center justify-between px-4 py-3 text-left hover:bg-gray-50 transition-colors ${
className={`w-full flex items-center justify-between px-4 py-3 text-left transition-colors ${
row.status === 'error'
? 'bg-red-50'
: row.status === 'warning'
? 'bg-yellow-50'
: 'bg-white'
: ''
}`}
style={row.status !== 'error' && row.status !== 'warning'
? { backgroundColor: 'var(--color-bg-surface-alt)' }
: undefined}
onClick={() =>
setExpandedRows((prev) => {
const next = new Set(prev)
@@ -786,30 +796,34 @@ function ValidationDialog({
: 'bg-green-500'
}`}
/>
<span className="text-sm font-medium text-gray-700">
<span className="text-sm font-medium text-content">
Row {row.row_index + 1}
{row.pim_id ? ` — ${row.pim_id}` : ''}
{row.produkt_baureihe ? ` (${row.produkt_baureihe})` : ''}
</span>
<span className="text-xs text-gray-400">
<span className="text-xs text-content-muted">
{row.issues.length} issue{row.issues.length !== 1 ? 's' : ''}
</span>
</div>
<span className="text-gray-400 text-xs">
<span className="text-content-muted text-xs">
{expandedRows.has(row.row_index) ? '' : ''}
</span>
</button>
{expandedRows.has(row.row_index) && (
<div className="border-t border-gray-100 divide-y divide-gray-50">
<div
className="border-t divide-y"
style={{ borderColor: 'var(--color-border-default)' }}
>
{row.issues.map((issue: ValidationIssue, i: number) => (
<div
key={i}
className="px-4 py-3 flex items-start justify-between gap-4"
style={{ borderColor: 'var(--color-border-default)' }}
>
<div>
<p className="text-sm text-gray-700">{issue.message}</p>
<p className="text-sm text-content">{issue.message}</p>
{issue.suggestion && (
<p className="text-xs text-gray-500 mt-0.5">
<p className="text-xs text-content-muted mt-0.5">
Suggestion: <em>{issue.suggestion}</em>
</p>
)}
@@ -821,7 +835,7 @@ function ValidationDialog({
onClick={() =>
onSaveAlias(issue.value!, issue.suggestion!)
}
className="text-xs bg-blue-600 text-white px-3 py-1 rounded hover:bg-blue-700 whitespace-nowrap"
className="text-xs bg-accent text-white px-3 py-1 rounded whitespace-nowrap"
>
Save as alias
</button>
@@ -843,10 +857,13 @@ function ValidationDialog({
)}
</div>
<div className="px-6 py-4 border-t border-gray-200 flex justify-end">
<div
className="px-6 py-4 border-t flex justify-end"
style={{ borderColor: 'var(--color-border-default)' }}
>
<button
onClick={onClose}
className="px-4 py-2 text-sm bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors"
className="px-4 py-2 text-sm text-content-secondary rounded-lg border border-border-default hover:bg-surface-hover transition-colors"
>
Close
</button>