style(web): 全站 font-mono → font-body (DM Mono 設計系統套用)
45 個 component + 6 個 page 統一從舊 font-mono 遷移到 font-body (DM Mono),確保設計系統一致性。 font-body = DM Mono (等寬),視覺效果相同但走新設計 token。 保留: font-heading (Syne)、font-dot-matrix (VT323/DSEG7) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -139,12 +139,12 @@ function StatCard({
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-nothing-gray-500 font-mono uppercase">
|
||||
<p className="text-xs text-nothing-gray-500 font-body uppercase">
|
||||
{label}
|
||||
</p>
|
||||
<p className="text-xl font-bold text-nothing-black">{value}</p>
|
||||
{subValue && (
|
||||
<p className="text-[10px] text-nothing-gray-400 font-mono">
|
||||
<p className="text-[10px] text-nothing-gray-400 font-body">
|
||||
{subValue}
|
||||
</p>
|
||||
)}
|
||||
@@ -349,7 +349,7 @@ export default function ActionLogPage({
|
||||
<DataPincerPanel title={t('actionLog.title')} status="healthy">
|
||||
{/* Toolbar */}
|
||||
<div className="flex items-center justify-between mb-4 px-1">
|
||||
<div className="text-sm text-nothing-gray-500 font-mono">
|
||||
<div className="text-sm text-nothing-gray-500 font-body">
|
||||
{totalCount > 0
|
||||
? `${totalCount} ${t('actionLog.columns.operation').toLowerCase()}s`
|
||||
: ''}
|
||||
@@ -361,7 +361,7 @@ export default function ActionLogPage({
|
||||
}}
|
||||
disabled={isLoading}
|
||||
className={cn(
|
||||
'flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs font-mono',
|
||||
'flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs font-body',
|
||||
'bg-nothing-gray-100 text-nothing-gray-600',
|
||||
'hover:bg-nothing-gray-200 transition-colors',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
@@ -378,7 +378,7 @@ export default function ActionLogPage({
|
||||
{error && (
|
||||
<div className="flex items-center gap-2 p-4 mb-4 rounded-lg bg-status-critical/10 border border-status-critical/20">
|
||||
<AlertCircle className="w-4 h-4 text-status-critical" />
|
||||
<span className="text-sm text-status-critical font-mono">
|
||||
<span className="text-sm text-status-critical font-body">
|
||||
{t('actionLog.fetchError')}: {error}
|
||||
</span>
|
||||
</div>
|
||||
@@ -388,7 +388,7 @@ export default function ActionLogPage({
|
||||
{isLoading && logs.length === 0 && (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<RefreshCw className="w-6 h-6 text-nothing-gray-400 animate-spin" />
|
||||
<span className="ml-2 text-nothing-gray-400 font-mono">
|
||||
<span className="ml-2 text-nothing-gray-400 font-body">
|
||||
{t('actionLog.loading')}
|
||||
</span>
|
||||
</div>
|
||||
@@ -398,7 +398,7 @@ export default function ActionLogPage({
|
||||
{!isLoading && logs.length === 0 && !error && (
|
||||
<div className="text-center py-12">
|
||||
<FileText className="w-12 h-12 mx-auto mb-3 text-nothing-gray-300" />
|
||||
<p className="text-nothing-gray-400 font-mono">
|
||||
<p className="text-nothing-gray-400 font-body">
|
||||
{t('actionLog.noLogs')}
|
||||
</p>
|
||||
</div>
|
||||
@@ -410,25 +410,25 @@ export default function ActionLogPage({
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-nothing-gray-200">
|
||||
<th className="px-3 py-2 text-left font-mono text-[10px] uppercase text-nothing-gray-500">
|
||||
<th className="px-3 py-2 text-left font-body text-[10px] uppercase text-nothing-gray-500">
|
||||
{t('actionLog.columns.time')}
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left font-mono text-[10px] uppercase text-nothing-gray-500">
|
||||
<th className="px-3 py-2 text-left font-body text-[10px] uppercase text-nothing-gray-500">
|
||||
{t('actionLog.columns.operation')}
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left font-mono text-[10px] uppercase text-nothing-gray-500">
|
||||
<th className="px-3 py-2 text-left font-body text-[10px] uppercase text-nothing-gray-500">
|
||||
{t('actionLog.columns.target')}
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left font-mono text-[10px] uppercase text-nothing-gray-500">
|
||||
<th className="px-3 py-2 text-left font-body text-[10px] uppercase text-nothing-gray-500">
|
||||
{t('actionLog.columns.namespace')}
|
||||
</th>
|
||||
<th className="px-3 py-2 text-center font-mono text-[10px] uppercase text-nothing-gray-500">
|
||||
<th className="px-3 py-2 text-center font-body text-[10px] uppercase text-nothing-gray-500">
|
||||
{t('actionLog.columns.status')}
|
||||
</th>
|
||||
<th className="px-3 py-2 text-right font-mono text-[10px] uppercase text-nothing-gray-500">
|
||||
<th className="px-3 py-2 text-right font-body text-[10px] uppercase text-nothing-gray-500">
|
||||
{t('actionLog.columns.duration')}
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left font-mono text-[10px] uppercase text-nothing-gray-500">
|
||||
<th className="px-3 py-2 text-left font-body text-[10px] uppercase text-nothing-gray-500">
|
||||
{t('actionLog.columns.executor')}
|
||||
</th>
|
||||
</tr>
|
||||
@@ -439,13 +439,13 @@ export default function ActionLogPage({
|
||||
key={log.id}
|
||||
className="border-b border-nothing-gray-100 hover:bg-nothing-gray-50/50 transition-colors"
|
||||
>
|
||||
<td className="px-3 py-3 font-mono text-xs text-nothing-gray-600">
|
||||
<td className="px-3 py-3 font-body text-xs text-nothing-gray-600">
|
||||
{formatDate(log.created_at)}
|
||||
</td>
|
||||
<td className="px-3 py-3">
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex items-center px-2 py-0.5 rounded text-[10px] font-mono font-bold uppercase',
|
||||
'inline-flex items-center px-2 py-0.5 rounded text-[10px] font-body font-bold uppercase',
|
||||
log.operation_type === 'DELETE_POD' &&
|
||||
'bg-status-critical/10 text-status-critical',
|
||||
log.operation_type === 'RESTART_DEPLOYMENT' &&
|
||||
@@ -458,33 +458,33 @@ export default function ActionLogPage({
|
||||
log.operation_type}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-3 py-3 font-mono text-xs text-nothing-black max-w-[200px] truncate">
|
||||
<td className="px-3 py-3 font-body text-xs text-nothing-black max-w-[200px] truncate">
|
||||
{log.target_resource}
|
||||
</td>
|
||||
<td className="px-3 py-3 font-mono text-xs text-nothing-gray-600">
|
||||
<td className="px-3 py-3 font-body text-xs text-nothing-gray-600">
|
||||
{log.namespace}
|
||||
</td>
|
||||
<td className="px-3 py-3 text-center">
|
||||
{log.success ? (
|
||||
<span className="inline-flex items-center gap-1 text-status-healthy">
|
||||
<CheckCircle2 className="w-4 h-4" />
|
||||
<span className="text-[10px] font-mono uppercase">
|
||||
<span className="text-[10px] font-body uppercase">
|
||||
{t('actionLog.status.success')}
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1 text-status-critical">
|
||||
<XCircle className="w-4 h-4" />
|
||||
<span className="text-[10px] font-mono uppercase">
|
||||
<span className="text-[10px] font-body uppercase">
|
||||
{t('actionLog.status.failure')}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-3 py-3 text-right font-mono text-xs text-nothing-gray-600">
|
||||
<td className="px-3 py-3 text-right font-body text-xs text-nothing-gray-600">
|
||||
{formatDuration(log.execution_duration_ms)}
|
||||
</td>
|
||||
<td className="px-3 py-3 font-mono text-xs text-nothing-gray-600">
|
||||
<td className="px-3 py-3 font-body text-xs text-nothing-gray-600">
|
||||
{log.executed_by}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -497,7 +497,7 @@ export default function ActionLogPage({
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-between mt-4 px-1">
|
||||
<span className="text-xs text-nothing-gray-500 font-mono">
|
||||
<span className="text-xs text-nothing-gray-500 font-body">
|
||||
{t('actionLog.pagination.page', {
|
||||
current: page,
|
||||
total: totalPages,
|
||||
@@ -508,7 +508,7 @@ export default function ActionLogPage({
|
||||
onClick={handlePrevPage}
|
||||
disabled={page <= 1 || isLoading}
|
||||
className={cn(
|
||||
'flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs font-mono',
|
||||
'flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs font-body',
|
||||
'border border-nothing-gray-200',
|
||||
'hover:bg-nothing-gray-50 transition-colors',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
@@ -521,7 +521,7 @@ export default function ActionLogPage({
|
||||
onClick={handleNextPage}
|
||||
disabled={page >= totalPages || isLoading}
|
||||
className={cn(
|
||||
'flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs font-mono',
|
||||
'flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs font-body',
|
||||
'border border-nothing-gray-200',
|
||||
'hover:bg-nothing-gray-50 transition-colors',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
@@ -541,7 +541,7 @@ export default function ActionLogPage({
|
||||
<p className="text-sm text-nothing-gray-500">
|
||||
{t('footer.copyright')}
|
||||
</p>
|
||||
<p className="text-sm text-nothing-gray-400 font-mono">
|
||||
<p className="text-sm text-nothing-gray-400 font-body">
|
||||
{t('footer.poweredBy')} v1.0.0
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -33,7 +33,7 @@ function SeverityBadge({ severity }: { severity: string }) {
|
||||
const style = SEVERITY_STYLE[severity] ?? SEVERITY_STYLE['P3']
|
||||
return (
|
||||
<span className={cn(
|
||||
'inline-flex items-center gap-1 px-2 py-0.5 rounded font-mono text-[11px] font-bold uppercase',
|
||||
'inline-flex items-center gap-1 px-2 py-0.5 rounded font-body text-[11px] font-bold uppercase',
|
||||
style.bg, style.text
|
||||
)}>
|
||||
{severity === 'P0' && <AlertTriangle className="w-3 h-3" />}
|
||||
@@ -57,12 +57,12 @@ function StatPill({ label, value, highlight }: { label: string; value: number; h
|
||||
: 'bg-white border-nothing-gray-200'
|
||||
)}>
|
||||
<span className={cn(
|
||||
'text-2xl font-bold font-mono tabular-nums',
|
||||
'text-2xl font-bold font-body tabular-nums',
|
||||
highlight && value > 0 ? 'text-status-critical' : 'text-nothing-black'
|
||||
)}>
|
||||
{value}
|
||||
</span>
|
||||
<span className="text-[11px] font-mono text-nothing-gray-500 uppercase tracking-wider mt-0.5">
|
||||
<span className="text-[11px] font-body text-nothing-gray-500 uppercase tracking-wider mt-0.5">
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
@@ -100,7 +100,7 @@ export default function AlertsPage({ params }: { params: { locale: string } }) {
|
||||
<Bell className="w-6 h-6" />
|
||||
{t('nav.alerts')}
|
||||
</h2>
|
||||
<p className="mt-1 text-sm text-nothing-gray-500 font-mono">
|
||||
<p className="mt-1 text-sm text-nothing-gray-500 font-body">
|
||||
{t('incident.activeIncidents')} · {t('alerts.autoRefresh', { seconds: 15 })}
|
||||
</p>
|
||||
</div>
|
||||
@@ -109,7 +109,7 @@ export default function AlertsPage({ params }: { params: { locale: string } }) {
|
||||
disabled={isLoading}
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 px-3 py-1.5 rounded-lg',
|
||||
'text-xs font-mono bg-nothing-gray-100 text-nothing-gray-600',
|
||||
'text-xs font-body bg-nothing-gray-100 text-nothing-gray-600',
|
||||
'hover:bg-nothing-gray-200 transition-colors',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
)}
|
||||
@@ -131,7 +131,7 @@ export default function AlertsPage({ params }: { params: { locale: string } }) {
|
||||
{error && (
|
||||
<div className="mb-4 p-4 rounded-lg bg-status-critical/10 border border-status-critical/20 flex items-center gap-2">
|
||||
<AlertCircle className="w-4 h-4 text-status-critical" />
|
||||
<span className="text-sm font-mono text-status-critical">{error}</span>
|
||||
<span className="text-sm font-body text-status-critical">{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -139,7 +139,7 @@ export default function AlertsPage({ params }: { params: { locale: string } }) {
|
||||
{isLoading && sorted.length === 0 && (
|
||||
<div className="flex items-center justify-center py-16">
|
||||
<RefreshCw className="w-6 h-6 animate-spin text-nothing-gray-400" />
|
||||
<span className="ml-2 text-nothing-gray-400 font-mono text-sm">{t('common.loading')}</span>
|
||||
<span className="ml-2 text-nothing-gray-400 font-body text-sm">{t('common.loading')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -147,8 +147,8 @@ export default function AlertsPage({ params }: { params: { locale: string } }) {
|
||||
{!isLoading && sorted.length === 0 && !error && (
|
||||
<div className="flex flex-col items-center justify-center py-20 text-center">
|
||||
<BellOff className="w-12 h-12 text-nothing-gray-300 mb-4" />
|
||||
<p className="font-mono text-nothing-gray-500 text-sm">{t('incident.noActiveIncidents')}</p>
|
||||
<p className="font-mono text-nothing-gray-400 text-xs mt-1">{t('incident.systemStable')}</p>
|
||||
<p className="font-body text-nothing-gray-500 text-sm">{t('incident.noActiveIncidents')}</p>
|
||||
<p className="font-body text-nothing-gray-400 text-xs mt-1">{t('incident.systemStable')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -163,7 +163,7 @@ export default function AlertsPage({ params }: { params: { locale: string } }) {
|
||||
{/* Section header */}
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<SeverityBadge severity={sev} />
|
||||
<span className="text-[11px] font-mono text-nothing-gray-400">
|
||||
<span className="text-[11px] font-body text-nothing-gray-400">
|
||||
{tAlerts('incidentCount', { count: group.length })}
|
||||
</span>
|
||||
<div className="flex-1 h-px bg-nothing-gray-200" />
|
||||
|
||||
@@ -86,11 +86,11 @@ function StatCard({
|
||||
'rounded-lg border p-4',
|
||||
highlight ? 'bg-claw-blue/5 border-claw-blue/20' : 'bg-white border-nothing-gray-200'
|
||||
)}>
|
||||
<p className="text-[11px] font-mono text-nothing-gray-500 uppercase tracking-wider mb-1">{label}</p>
|
||||
<p className={cn('text-2xl font-bold font-mono tabular-nums', highlight ? 'text-claw-blue' : 'text-nothing-black')}>
|
||||
<p className="text-[11px] font-body text-nothing-gray-500 uppercase tracking-wider mb-1">{label}</p>
|
||||
<p className={cn('text-2xl font-bold font-body tabular-nums', highlight ? 'text-claw-blue' : 'text-nothing-black')}>
|
||||
{value}
|
||||
</p>
|
||||
{sub && <p className="text-[11px] font-mono text-nothing-gray-400 mt-0.5">{sub}</p>}
|
||||
{sub && <p className="text-[11px] font-body text-nothing-gray-400 mt-0.5">{sub}</p>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -147,18 +147,18 @@ function IncidentEvalRow({
|
||||
onClick={() => setExpanded(v => !v)}
|
||||
>
|
||||
<span className={cn(
|
||||
'px-2 py-0.5 rounded font-mono text-[11px] font-bold',
|
||||
'px-2 py-0.5 rounded font-body text-[11px] font-bold',
|
||||
severity === 'P0' ? 'bg-status-critical/10 text-status-critical' :
|
||||
severity === 'P1' ? 'bg-status-warning/10 text-status-warning' :
|
||||
'bg-nothing-gray-100 text-nothing-gray-600'
|
||||
)}>{severity}</span>
|
||||
<span className="font-mono text-sm text-nothing-black flex-1 truncate">{incidentId}</span>
|
||||
<span className="font-body text-sm text-nothing-black flex-1 truncate">{incidentId}</span>
|
||||
|
||||
{loading && <RefreshCw className="w-4 h-4 animate-spin text-nothing-gray-400" />}
|
||||
{!loading && eval_ && (
|
||||
eval_.can_auto_repair
|
||||
? <span className="flex items-center gap-1 text-[11px] font-mono text-status-healthy"><CheckCircle2 className="w-3.5 h-3.5" />{t('canAutoRepair')}</span>
|
||||
: <span className="flex items-center gap-1 text-[11px] font-mono text-nothing-gray-400"><XCircle className="w-3.5 h-3.5" />{t('notEligibleShort')}</span>
|
||||
? <span className="flex items-center gap-1 text-[11px] font-body text-status-healthy"><CheckCircle2 className="w-3.5 h-3.5" />{t('canAutoRepair')}</span>
|
||||
: <span className="flex items-center gap-1 text-[11px] font-body text-nothing-gray-400"><XCircle className="w-3.5 h-3.5" />{t('notEligibleShort')}</span>
|
||||
)}
|
||||
{expanded ? <ChevronUp className="w-4 h-4 text-nothing-gray-400" /> : <ChevronDown className="w-4 h-4 text-nothing-gray-400" />}
|
||||
</div>
|
||||
@@ -168,34 +168,34 @@ function IncidentEvalRow({
|
||||
<div className="px-4 py-4 bg-white space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||
<div>
|
||||
<span className="text-[10px] font-mono text-nothing-gray-500 uppercase">Playbook</span>
|
||||
<p className="font-mono text-nothing-black mt-0.5">{eval_.playbook_name ?? '—'}</p>
|
||||
<span className="text-[10px] font-body text-nothing-gray-500 uppercase">Playbook</span>
|
||||
<p className="font-body text-nothing-black mt-0.5">{eval_.playbook_name ?? '—'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] font-mono text-nothing-gray-500 uppercase">{t('riskLevel')}</span>
|
||||
<span className="text-[10px] font-body text-nothing-gray-500 uppercase">{t('riskLevel')}</span>
|
||||
<p className="mt-0.5">
|
||||
<span className={cn('px-2 py-0.5 rounded border text-[11px] font-mono font-bold', RISK_STYLE[eval_.risk_level] ?? RISK_STYLE.MEDIUM)}>
|
||||
<span className={cn('px-2 py-0.5 rounded border text-[11px] font-body font-bold', RISK_STYLE[eval_.risk_level] ?? RISK_STYLE.MEDIUM)}>
|
||||
{eval_.risk_level}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
{eval_.success_rate != null && (
|
||||
<div>
|
||||
<span className="text-[10px] font-mono text-nothing-gray-500 uppercase">{t('successRate')}</span>
|
||||
<p className="font-mono text-status-healthy font-bold mt-0.5">{(eval_.success_rate * 100).toFixed(1)}%</p>
|
||||
<span className="text-[10px] font-body text-nothing-gray-500 uppercase">{t('successRate')}</span>
|
||||
<p className="font-body text-status-healthy font-bold mt-0.5">{(eval_.success_rate * 100).toFixed(1)}%</p>
|
||||
</div>
|
||||
)}
|
||||
{eval_.total_executions != null && (
|
||||
<div>
|
||||
<span className="text-[10px] font-mono text-nothing-gray-500 uppercase">{t('execCount')}</span>
|
||||
<p className="font-mono text-nothing-black mt-0.5">{eval_.total_executions}</p>
|
||||
<span className="text-[10px] font-body text-nothing-gray-500 uppercase">{t('execCount')}</span>
|
||||
<p className="font-body text-nothing-black mt-0.5">{eval_.total_executions}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-3 bg-nothing-gray-50 rounded-lg">
|
||||
<span className="text-[10px] font-mono text-nothing-gray-500 uppercase">{t('decisionReason')}</span>
|
||||
<p className="text-sm font-mono text-nothing-gray-700 mt-1">{eval_.reason}</p>
|
||||
<span className="text-[10px] font-body text-nothing-gray-500 uppercase">{t('decisionReason')}</span>
|
||||
<p className="text-sm font-body text-nothing-gray-700 mt-1">{eval_.reason}</p>
|
||||
</div>
|
||||
|
||||
{/* Execute result */}
|
||||
@@ -208,14 +208,14 @@ function IncidentEvalRow({
|
||||
{result.success
|
||||
? <CheckCircle2 className="w-4 h-4 text-status-healthy" />
|
||||
: <XCircle className="w-4 h-4 text-status-critical" />}
|
||||
<span className={cn('text-sm font-mono font-bold', result.success ? 'text-status-healthy' : 'text-status-critical')}>
|
||||
<span className={cn('text-sm font-body font-bold', result.success ? 'text-status-healthy' : 'text-status-critical')}>
|
||||
{result.success ? t('execSuccess', { ms: result.execution_time_ms }) : t('execFailed', { error: result.error })}
|
||||
</span>
|
||||
</div>
|
||||
{result.executed_steps.length > 0 && (
|
||||
<ul className="space-y-1">
|
||||
{result.executed_steps.map((step, i) => (
|
||||
<li key={i} className="text-xs font-mono text-nothing-gray-600 flex items-start gap-1.5">
|
||||
<li key={i} className="text-xs font-body text-nothing-gray-600 flex items-start gap-1.5">
|
||||
<Zap className="w-3 h-3 mt-0.5 text-claw-blue flex-shrink-0" />
|
||||
{step}
|
||||
</li>
|
||||
@@ -232,7 +232,7 @@ function IncidentEvalRow({
|
||||
disabled={executing}
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-4 py-2 rounded-lg',
|
||||
'bg-claw-blue text-white font-mono text-sm font-semibold',
|
||||
'bg-claw-blue text-white font-body text-sm font-semibold',
|
||||
'hover:bg-claw-blue/90 transition-colors',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
)}
|
||||
@@ -305,7 +305,7 @@ export default function AutoRepairPage({ params }: { params: { locale: string }
|
||||
<Wrench className="w-6 h-6" />
|
||||
{tNav('autoRepair')}
|
||||
</h2>
|
||||
<p className="mt-1 text-sm text-nothing-gray-500 font-mono">
|
||||
<p className="mt-1 text-sm text-nothing-gray-500 font-body">
|
||||
{t('subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
@@ -314,7 +314,7 @@ export default function AutoRepairPage({ params }: { params: { locale: string }
|
||||
disabled={statsLoading}
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 px-3 py-1.5 rounded-lg',
|
||||
'text-xs font-mono bg-nothing-gray-100 text-nothing-gray-600',
|
||||
'text-xs font-body bg-nothing-gray-100 text-nothing-gray-600',
|
||||
'hover:bg-nothing-gray-200 transition-colors',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
)}
|
||||
@@ -328,7 +328,7 @@ export default function AutoRepairPage({ params }: { params: { locale: string }
|
||||
{statsError && (
|
||||
<div className="mb-4 p-4 rounded-lg bg-status-critical/10 border border-status-critical/20 flex items-center gap-2">
|
||||
<AlertCircle className="w-4 h-4 text-status-critical" />
|
||||
<span className="text-sm font-mono text-status-critical">{statsError}</span>
|
||||
<span className="text-sm font-body text-status-critical">{statsError}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -369,10 +369,10 @@ export default function AutoRepairPage({ params }: { params: { locale: string }
|
||||
? <CheckCircle2 className="w-5 h-5 text-status-healthy" />
|
||||
: <ShieldAlert className="w-5 h-5 text-nothing-gray-400" />}
|
||||
<div>
|
||||
<p className={cn('text-sm font-mono font-semibold', stats.auto_repair_eligible ? 'text-status-healthy' : 'text-nothing-gray-600')}>
|
||||
<p className={cn('text-sm font-body font-semibold', stats.auto_repair_eligible ? 'text-status-healthy' : 'text-nothing-gray-600')}>
|
||||
{stats.auto_repair_eligible ? t('ready') : t('notReady')}
|
||||
</p>
|
||||
<p className="text-xs font-mono text-nothing-gray-400">
|
||||
<p className="text-xs font-body text-nothing-gray-400">
|
||||
{stats.auto_repair_eligible
|
||||
? t('readyDesc', { count: stats.high_quality_playbooks })
|
||||
: t('notReadyDesc')}
|
||||
@@ -383,7 +383,7 @@ export default function AutoRepairPage({ params }: { params: { locale: string }
|
||||
|
||||
{/* Incident evaluation list */}
|
||||
<div>
|
||||
<h3 className="text-[11px] font-mono text-nothing-gray-500 uppercase tracking-widest mb-3">
|
||||
<h3 className="text-[11px] font-body text-nothing-gray-500 uppercase tracking-widest mb-3">
|
||||
{t('incidentEval')}
|
||||
</h3>
|
||||
|
||||
@@ -396,7 +396,7 @@ export default function AutoRepairPage({ params }: { params: { locale: string }
|
||||
{!incidentsLoading && eligibleIncidents.length === 0 && (
|
||||
<div className="text-center py-10 border border-dashed border-nothing-gray-200 rounded-lg">
|
||||
<CheckCircle2 className="w-8 h-8 text-status-healthy mx-auto mb-2" />
|
||||
<p className="font-mono text-sm text-nothing-gray-500">{t('noEligible')}</p>
|
||||
<p className="font-body text-sm text-nothing-gray-500">{t('noEligible')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -171,7 +171,7 @@ export default function DemoPage({ params }: { params: { locale: string } }) {
|
||||
{/* Error Display */}
|
||||
{createError && (
|
||||
<div className="p-3 bg-status-critical/10 border border-status-critical/30 rounded-lg">
|
||||
<p className="text-sm text-status-critical font-mono">{createError}</p>
|
||||
<p className="text-sm text-status-critical font-body">{createError}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -84,10 +84,10 @@ function HealthSummary({ data }: { data: DashboardResponse }) {
|
||||
{ label: '異常', count: data.critical_count, color: 'text-status-critical', bg: 'bg-status-critical/10' },
|
||||
].map(item => (
|
||||
<div key={item.label} className={cn('rounded-lg border p-4 text-center', item.bg, 'border-transparent')}>
|
||||
<div className={cn('text-3xl font-bold font-mono tabular-nums', item.color)}>
|
||||
<div className={cn('text-3xl font-bold font-body tabular-nums', item.color)}>
|
||||
{item.count}
|
||||
</div>
|
||||
<div className="text-[11px] font-mono text-nothing-gray-500 uppercase tracking-wider mt-1">
|
||||
<div className="text-[11px] font-body text-nothing-gray-500 uppercase tracking-wider mt-1">
|
||||
{item.label} · {Math.round(item.count / total * 100)}%
|
||||
</div>
|
||||
</div>
|
||||
@@ -155,7 +155,7 @@ export default function MonitoringPage({ params }: { params: { locale: string }
|
||||
<Monitor className="w-6 h-6" />
|
||||
{t('nav.monitoring')}
|
||||
</h2>
|
||||
<p className="mt-1 text-sm text-nothing-gray-500 font-mono">
|
||||
<p className="mt-1 text-sm text-nothing-gray-500 font-body">
|
||||
{t('alerts.autoRefresh', { seconds: 30 })}
|
||||
</p>
|
||||
</div>
|
||||
@@ -164,7 +164,7 @@ export default function MonitoringPage({ params }: { params: { locale: string }
|
||||
disabled={isLoading}
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 px-3 py-1.5 rounded-lg',
|
||||
'text-xs font-mono bg-nothing-gray-100 text-nothing-gray-600',
|
||||
'text-xs font-body bg-nothing-gray-100 text-nothing-gray-600',
|
||||
'hover:bg-nothing-gray-200 transition-colors',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
)}
|
||||
@@ -178,7 +178,7 @@ export default function MonitoringPage({ params }: { params: { locale: string }
|
||||
{dashError && (
|
||||
<div className="mb-4 p-4 rounded-lg bg-status-critical/10 border border-status-critical/20 flex items-center gap-2">
|
||||
<AlertCircle className="w-4 h-4 text-status-critical" />
|
||||
<span className="text-sm font-mono text-status-critical">{dashError}</span>
|
||||
<span className="text-sm font-body text-status-critical">{dashError}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -187,7 +187,7 @@ export default function MonitoringPage({ params }: { params: { locale: string }
|
||||
|
||||
{/* Gold Metrics */}
|
||||
<div className="mb-6">
|
||||
<h3 className="text-[11px] font-mono text-nothing-gray-500 uppercase tracking-widest mb-3">
|
||||
<h3 className="text-[11px] font-body text-nothing-gray-500 uppercase tracking-widest mb-3">
|
||||
黃金指標 (Gold Metrics)
|
||||
</h3>
|
||||
{metricsLoading && metrics.length === 0 ? (
|
||||
@@ -201,7 +201,7 @@ export default function MonitoringPage({ params }: { params: { locale: string }
|
||||
|
||||
{/* Host Grid */}
|
||||
<div className="mb-6">
|
||||
<h3 className="text-[11px] font-mono text-nothing-gray-500 uppercase tracking-widest mb-3">
|
||||
<h3 className="text-[11px] font-body text-nothing-gray-500 uppercase tracking-widest mb-3">
|
||||
主機狀態 (Four-Host Architecture)
|
||||
</h3>
|
||||
<HostGrid hosts={[]} />
|
||||
@@ -210,7 +210,7 @@ export default function MonitoringPage({ params }: { params: { locale: string }
|
||||
{/* Service Table */}
|
||||
{dashboard && dashboard.services && dashboard.services.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-[11px] font-mono text-nothing-gray-500 uppercase tracking-widest mb-3">
|
||||
<h3 className="text-[11px] font-body text-nothing-gray-500 uppercase tracking-widest mb-3">
|
||||
服務清單
|
||||
</h3>
|
||||
<div className="rounded-lg border border-nothing-gray-200 overflow-hidden">
|
||||
@@ -218,7 +218,7 @@ export default function MonitoringPage({ params }: { params: { locale: string }
|
||||
<thead>
|
||||
<tr className="bg-nothing-gray-50 border-b border-nothing-gray-200">
|
||||
{['服務名稱', '狀態', '延遲', '可用率', '最後檢查'].map(h => (
|
||||
<th key={h} className="px-4 py-2.5 text-left font-mono text-[10px] uppercase text-nothing-gray-500 tracking-wider">
|
||||
<th key={h} className="px-4 py-2.5 text-left font-body text-[10px] uppercase text-nothing-gray-500 tracking-wider">
|
||||
{h}
|
||||
</th>
|
||||
))}
|
||||
@@ -230,25 +230,25 @@ export default function MonitoringPage({ params }: { params: { locale: string }
|
||||
'border-b border-nothing-gray-100 last:border-0',
|
||||
i % 2 === 0 ? 'bg-white' : 'bg-nothing-gray-50/50'
|
||||
)}>
|
||||
<td className="px-4 py-3 font-mono text-sm text-nothing-black font-medium">
|
||||
<td className="px-4 py-3 font-body text-sm text-nothing-black font-medium">
|
||||
{svc.name}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={cn(
|
||||
'inline-flex items-center gap-1.5 px-2 py-0.5 rounded border text-[11px] font-mono font-bold uppercase',
|
||||
'inline-flex items-center gap-1.5 px-2 py-0.5 rounded border text-[11px] font-body font-bold uppercase',
|
||||
STATUS_BADGE[svc.status]
|
||||
)}>
|
||||
{STATUS_ICON[svc.status]}
|
||||
{svc.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 font-mono text-sm text-nothing-gray-700">
|
||||
<td className="px-4 py-3 font-body text-sm text-nothing-gray-700">
|
||||
{svc.latency_ms != null ? `${svc.latency_ms}ms` : '--'}
|
||||
</td>
|
||||
<td className="px-4 py-3 font-mono text-sm text-nothing-gray-700">
|
||||
<td className="px-4 py-3 font-body text-sm text-nothing-gray-700">
|
||||
{svc.uptime_pct != null ? `${svc.uptime_pct.toFixed(2)}%` : '--'}
|
||||
</td>
|
||||
<td className="px-4 py-3 font-mono text-xs text-nothing-gray-500">
|
||||
<td className="px-4 py-3 font-body text-xs text-nothing-gray-500">
|
||||
{svc.last_checked
|
||||
? new Date(svc.last_checked).toLocaleTimeString()
|
||||
: '--'}
|
||||
|
||||
@@ -42,7 +42,7 @@ function SettingRow({
|
||||
<div className="flex items-center justify-between py-4 border-b border-nothing-gray-100 last:border-0">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-nothing-black">{label}</p>
|
||||
{description && <p className="text-xs font-mono text-nothing-gray-400 mt-0.5">{description}</p>}
|
||||
{description && <p className="text-xs font-body text-nothing-gray-400 mt-0.5">{description}</p>}
|
||||
</div>
|
||||
<div className="flex-shrink-0 ml-6">{children}</div>
|
||||
</div>
|
||||
@@ -131,7 +131,7 @@ export default function SettingsPage({ params }: { params: { locale: string } })
|
||||
)}
|
||||
>
|
||||
<section.icon className="w-4 h-4 flex-shrink-0" />
|
||||
<span className="text-sm font-mono">{t(section.titleKey)}</span>
|
||||
<span className="text-sm font-body">{t(section.titleKey)}</span>
|
||||
{activeSection !== section.id && <ChevronRight className="w-3 h-3 ml-auto text-nothing-gray-400" />}
|
||||
</button>
|
||||
))}
|
||||
@@ -144,12 +144,12 @@ export default function SettingsPage({ params }: { params: { locale: string } })
|
||||
{/* Appearance */}
|
||||
{activeSection === 'appearance' && (
|
||||
<div>
|
||||
<h3 className="font-mono text-sm font-bold text-nothing-black mb-4 uppercase tracking-wider">{t('appearanceSettings')}</h3>
|
||||
<h3 className="font-body text-sm font-bold text-nothing-black mb-4 uppercase tracking-wider">{t('appearanceSettings')}</h3>
|
||||
<SettingRow label={t('compactMode')} description={t('compactModeDesc')}>
|
||||
<Toggle value={compactMode} onChange={setCompactMode} />
|
||||
</SettingRow>
|
||||
<SettingRow label={t('designSystem')} description={t('designSystemValue')}>
|
||||
<span className="text-xs font-mono text-nothing-gray-400 bg-nothing-gray-100 px-2 py-1 rounded">v7.0</span>
|
||||
<span className="text-xs font-body text-nothing-gray-400 bg-nothing-gray-100 px-2 py-1 rounded">v7.0</span>
|
||||
</SettingRow>
|
||||
<SettingRow label={t('themeColor')} description={t('themeColorValue')}>
|
||||
<div className="flex gap-1.5">
|
||||
@@ -163,7 +163,7 @@ export default function SettingsPage({ params }: { params: { locale: string } })
|
||||
{/* Language */}
|
||||
{activeSection === 'language' && (
|
||||
<div>
|
||||
<h3 className="font-mono text-sm font-bold text-nothing-black mb-4 uppercase tracking-wider">{t('languageSettings')}</h3>
|
||||
<h3 className="font-body text-sm font-bold text-nothing-black mb-4 uppercase tracking-wider">{t('languageSettings')}</h3>
|
||||
{[
|
||||
{ code: 'zh-TW', labelKey: 'zhTW', subKey: 'zhTWSub' },
|
||||
{ code: 'en', labelKey: 'en', subKey: 'enSub' },
|
||||
@@ -180,7 +180,7 @@ export default function SettingsPage({ params }: { params: { locale: string } })
|
||||
>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-nothing-black">{t(lang.labelKey)}</p>
|
||||
<p className="text-xs font-mono text-nothing-gray-400">{t(lang.subKey)}</p>
|
||||
<p className="text-xs font-body text-nothing-gray-400">{t(lang.subKey)}</p>
|
||||
</div>
|
||||
{params.locale === lang.code && <CheckCircle2 className="w-5 h-5 text-claw-blue" />}
|
||||
</div>
|
||||
@@ -191,7 +191,7 @@ export default function SettingsPage({ params }: { params: { locale: string } })
|
||||
{/* Notifications */}
|
||||
{activeSection === 'notify' && (
|
||||
<div>
|
||||
<h3 className="font-mono text-sm font-bold text-nothing-black mb-4 uppercase tracking-wider">{t('notifySettings')}</h3>
|
||||
<h3 className="font-body text-sm font-bold text-nothing-black mb-4 uppercase tracking-wider">{t('notifySettings')}</h3>
|
||||
<SettingRow label={t('browserNotify')} description={t('browserNotifyDesc')}>
|
||||
<Toggle value={notifyBrowser} onChange={setNotifyBrowser} />
|
||||
</SettingRow>
|
||||
@@ -199,7 +199,7 @@ export default function SettingsPage({ params }: { params: { locale: string } })
|
||||
<Toggle value={notifyP0Only} onChange={setNotifyP0Only} />
|
||||
</SettingRow>
|
||||
<SettingRow label={t('telegramNotify')} description={t('telegramNotifyDesc')}>
|
||||
<span className="text-xs font-mono text-nothing-gray-400 bg-nothing-gray-100 px-2 py-1 rounded">{t('backendConfig')}</span>
|
||||
<span className="text-xs font-body text-nothing-gray-400 bg-nothing-gray-100 px-2 py-1 rounded">{t('backendConfig')}</span>
|
||||
</SettingRow>
|
||||
</div>
|
||||
)}
|
||||
@@ -207,7 +207,7 @@ export default function SettingsPage({ params }: { params: { locale: string } })
|
||||
{/* System */}
|
||||
{activeSection === 'system' && (
|
||||
<div>
|
||||
<h3 className="font-mono text-sm font-bold text-nothing-black mb-4 uppercase tracking-wider">{t('systemSettings')}</h3>
|
||||
<h3 className="font-body text-sm font-bold text-nothing-black mb-4 uppercase tracking-wider">{t('systemSettings')}</h3>
|
||||
{[
|
||||
{ labelKey: 'frontendVersion', value: 'v1.0.0' },
|
||||
{ labelKey: 'apiEndpoint', value: process.env.NEXT_PUBLIC_API_URL ?? t('notConfigured') },
|
||||
@@ -215,7 +215,7 @@ export default function SettingsPage({ params }: { params: { locale: string } })
|
||||
{ labelKey: 'phase', value: 'Phase 23 (2026-04-01)' },
|
||||
].map(item => (
|
||||
<SettingRow key={item.labelKey} label={t(item.labelKey)}>
|
||||
<span className="text-sm font-mono text-nothing-gray-600">{item.value}</span>
|
||||
<span className="text-sm font-body text-nothing-gray-600">{item.value}</span>
|
||||
</SettingRow>
|
||||
))}
|
||||
</div>
|
||||
@@ -226,12 +226,12 @@ export default function SettingsPage({ params }: { params: { locale: string } })
|
||||
<div className="mt-6 flex items-center gap-3">
|
||||
<button
|
||||
onClick={saveSettings}
|
||||
className="px-5 py-2.5 rounded-lg bg-nothing-black text-white font-mono text-sm font-semibold hover:bg-nothing-black/90 transition-colors"
|
||||
className="px-5 py-2.5 rounded-lg bg-nothing-black text-white font-body text-sm font-semibold hover:bg-nothing-black/90 transition-colors"
|
||||
>
|
||||
{t('save')}
|
||||
</button>
|
||||
{saved && (
|
||||
<span className="flex items-center gap-1.5 text-sm font-mono text-status-healthy">
|
||||
<span className="flex items-center gap-1.5 text-sm font-body text-status-healthy">
|
||||
<CheckCircle2 className="w-4 h-4" />
|
||||
{t('saved')}
|
||||
</span>
|
||||
|
||||
@@ -145,7 +145,7 @@ export function ApprovalCard({
|
||||
{/* Risk Badge */}
|
||||
<div
|
||||
className={cn(
|
||||
'inline-flex items-center gap-2 px-3 py-1 rounded-full border text-xs font-mono tracking-wider mb-3',
|
||||
'inline-flex items-center gap-2 px-3 py-1 rounded-full border text-xs font-body tracking-wider mb-3',
|
||||
riskStyle.bgColor,
|
||||
riskStyle.borderColor,
|
||||
riskStyle.color
|
||||
@@ -174,12 +174,12 @@ export function ApprovalCard({
|
||||
|
||||
{/* Signature Counter */}
|
||||
<div className="text-right shrink-0">
|
||||
<div className="font-mono text-xs text-nothing-gray-500 mb-1">
|
||||
<div className="font-body text-xs text-nothing-gray-500 mb-1">
|
||||
{t('signatures')}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'font-mono text-lg',
|
||||
'font-body text-lg',
|
||||
needsMoreSignatures ? 'text-status-warning' : 'text-status-healthy'
|
||||
)}
|
||||
>
|
||||
@@ -193,40 +193,40 @@ export function ApprovalCard({
|
||||
|
||||
{/* Blast Radius Section */}
|
||||
<div className="space-y-3">
|
||||
<h4 className="font-mono text-xs text-nothing-gray-500 tracking-wider">
|
||||
<h4 className="font-body text-xs text-nothing-gray-500 tracking-wider">
|
||||
{tBlast('title')}
|
||||
</h4>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{/* Affected Pods */}
|
||||
<div className="bg-nothing-gray-900 rounded-card p-3">
|
||||
<div className="font-mono text-xs text-nothing-gray-500 mb-1">
|
||||
<div className="font-body text-xs text-nothing-gray-500 mb-1">
|
||||
{tBlast('affectedPods')}
|
||||
</div>
|
||||
<div className="font-mono text-lg text-nothing-white">
|
||||
<div className="font-body text-lg text-nothing-white">
|
||||
{request.blastRadius?.affectedPods ?? 0}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Estimated Downtime */}
|
||||
<div className="bg-nothing-gray-900 rounded-card p-3">
|
||||
<div className="font-mono text-xs text-nothing-gray-500 mb-1">
|
||||
<div className="font-body text-xs text-nothing-gray-500 mb-1">
|
||||
{tBlast('estimatedDowntime')}
|
||||
</div>
|
||||
<div className="font-mono text-lg text-nothing-white">
|
||||
<div className="font-body text-lg text-nothing-white">
|
||||
{request.blastRadius?.estimatedDowntime ?? '0'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Related Services */}
|
||||
<div className="bg-nothing-gray-900 rounded-card p-3">
|
||||
<div className="font-mono text-xs text-nothing-gray-500 mb-1">
|
||||
<div className="font-body text-xs text-nothing-gray-500 mb-1">
|
||||
{tBlast('relatedServices')}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{(request.blastRadius?.relatedServices ?? []).map((service) => (
|
||||
<span
|
||||
key={service}
|
||||
className="px-2 py-0.5 bg-nothing-gray-800 rounded text-xs font-mono text-nothing-gray-300"
|
||||
className="px-2 py-0.5 bg-nothing-gray-800 rounded text-xs font-body text-nothing-gray-300"
|
||||
>
|
||||
{service}
|
||||
</span>
|
||||
@@ -236,10 +236,10 @@ export function ApprovalCard({
|
||||
|
||||
{/* Data Impact */}
|
||||
<div className="bg-nothing-gray-900 rounded-card p-3">
|
||||
<div className="font-mono text-xs text-nothing-gray-500 mb-1">
|
||||
<div className="font-body text-xs text-nothing-gray-500 mb-1">
|
||||
{tBlast('dataImpact')}
|
||||
</div>
|
||||
<div className={cn('font-mono text-lg', dataImpactStyle.color)}>
|
||||
<div className={cn('font-body text-lg', dataImpactStyle.color)}>
|
||||
{dataImpactLabel}
|
||||
</div>
|
||||
</div>
|
||||
@@ -251,7 +251,7 @@ export function ApprovalCard({
|
||||
|
||||
{/* Dry-Run Checks */}
|
||||
<div className="space-y-3">
|
||||
<h4 className="font-mono text-xs text-nothing-gray-500 tracking-wider">
|
||||
<h4 className="font-body text-xs text-nothing-gray-500 tracking-wider">
|
||||
{tDryRun('validation')}
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
@@ -271,12 +271,12 @@ export function ApprovalCard({
|
||||
>
|
||||
{check.passed ? '✓' : '✗'}
|
||||
</div>
|
||||
<span className="font-mono text-sm text-nothing-white">
|
||||
<span className="font-body text-sm text-nothing-white">
|
||||
{check.name}
|
||||
</span>
|
||||
</div>
|
||||
{check.message && (
|
||||
<span className="font-mono text-xs text-nothing-gray-500">
|
||||
<span className="font-body text-xs text-nothing-gray-500">
|
||||
{check.message}
|
||||
</span>
|
||||
)}
|
||||
@@ -291,7 +291,7 @@ export function ApprovalCard({
|
||||
{/* Footer: Meta + Actions */}
|
||||
<div className="flex items-center justify-between">
|
||||
{/* Meta */}
|
||||
<div className="font-mono text-xs text-nothing-gray-600">
|
||||
<div className="font-body text-xs text-nothing-gray-600">
|
||||
<span>{t('requestedBy')} </span>
|
||||
<span className="text-nothing-gray-400">{request.requestedBy}</span>
|
||||
<span className="mx-2">|</span>
|
||||
@@ -304,7 +304,7 @@ export function ApprovalCard({
|
||||
<button
|
||||
onClick={handleReject}
|
||||
className={cn(
|
||||
'px-5 py-2 rounded-button font-mono text-sm transition-all',
|
||||
'px-5 py-2 rounded-button font-body text-sm transition-all',
|
||||
'border border-nothing-gray-700 text-nothing-gray-400',
|
||||
'hover:border-nothing-red hover:text-nothing-red hover:bg-nothing-red/10'
|
||||
)}
|
||||
@@ -318,7 +318,7 @@ export function ApprovalCard({
|
||||
onClick={handleApprove}
|
||||
disabled={!allChecksPassed}
|
||||
className={cn(
|
||||
'px-5 py-2 rounded-button font-mono text-sm transition-all animate-pulse',
|
||||
'px-5 py-2 rounded-button font-body text-sm transition-all animate-pulse',
|
||||
'bg-nothing-red text-nothing-white border-2 border-nothing-red',
|
||||
'hover:bg-nothing-red/80'
|
||||
)}
|
||||
@@ -330,7 +330,7 @@ export function ApprovalCard({
|
||||
onClick={handleApprove}
|
||||
disabled={!allChecksPassed}
|
||||
className={cn(
|
||||
'px-5 py-2 rounded-button font-mono text-sm transition-all',
|
||||
'px-5 py-2 rounded-button font-body text-sm transition-all',
|
||||
allChecksPassed
|
||||
? isDestructive
|
||||
? 'bg-nothing-red/20 text-nothing-red border border-nothing-red/50 hover:bg-nothing-red/30'
|
||||
|
||||
@@ -190,7 +190,7 @@ export function DataPincer({
|
||||
{showLabel && (
|
||||
<div
|
||||
className={cn(
|
||||
'mt-6 font-mono tracking-wider uppercase',
|
||||
'mt-6 font-body tracking-wider uppercase',
|
||||
sizeClass.label,
|
||||
hasError ? 'text-status-critical' : 'text-nothing-gray-400',
|
||||
config.animate && 'animate-pulse'
|
||||
|
||||
@@ -266,7 +266,7 @@ export function ThinkingTerminalOptimized({
|
||||
</div>
|
||||
<h2 className="font-display text-display-sm">{t('title')}</h2>
|
||||
</div>
|
||||
<span className="font-mono text-xs text-nothing-gray-500">
|
||||
<span className="font-body text-xs text-nothing-gray-500">
|
||||
v1.0 | DOM Bypass | {lineCount} lines
|
||||
</span>
|
||||
</div>
|
||||
@@ -277,7 +277,7 @@ export function ThinkingTerminalOptimized({
|
||||
onClick={startStream}
|
||||
disabled={isStreaming}
|
||||
className={cn(
|
||||
'flex-1 py-3 px-4 rounded-button font-mono text-sm transition-all border',
|
||||
'flex-1 py-3 px-4 rounded-button font-body text-sm transition-all border',
|
||||
isStreaming
|
||||
? 'bg-nothing-gray-800 text-nothing-gray-500 border-nothing-gray-700 cursor-not-allowed'
|
||||
: 'bg-nothing-white text-nothing-black border-transparent hover:bg-nothing-gray-200'
|
||||
@@ -289,7 +289,7 @@ export function ThinkingTerminalOptimized({
|
||||
{isStreaming && (
|
||||
<button
|
||||
onClick={stopStream}
|
||||
className="py-3 px-4 rounded-button font-mono text-sm bg-status-critical/20 text-status-critical border border-status-critical/50 hover:bg-status-critical/30"
|
||||
className="py-3 px-4 rounded-button font-body text-sm bg-status-critical/20 text-status-critical border border-status-critical/50 hover:bg-status-critical/30"
|
||||
>
|
||||
{t('stop')}
|
||||
</button>
|
||||
@@ -299,7 +299,7 @@ export function ThinkingTerminalOptimized({
|
||||
onClick={clearTerminal}
|
||||
disabled={isStreaming || lineCount === 0}
|
||||
className={cn(
|
||||
'py-3 px-4 rounded-button font-mono text-sm border',
|
||||
'py-3 px-4 rounded-button font-body text-sm border',
|
||||
isStreaming || lineCount === 0
|
||||
? 'bg-nothing-gray-800 text-nothing-gray-500 border-nothing-gray-700 cursor-not-allowed'
|
||||
: 'bg-nothing-gray-800 text-nothing-gray-300 border-nothing-gray-700 hover:bg-nothing-gray-700'
|
||||
@@ -312,7 +312,7 @@ export function ThinkingTerminalOptimized({
|
||||
{/* Terminal Output - DOM Bypass Container */}
|
||||
<div
|
||||
ref={terminalRef}
|
||||
className="bg-nothing-gray-900 rounded-card p-4 overflow-y-auto font-mono text-sm"
|
||||
className="bg-nothing-gray-900 rounded-card p-4 overflow-y-auto font-body text-sm"
|
||||
style={{ maxHeight, minHeight: '150px' }}
|
||||
>
|
||||
{/* Content appended directly via DOM manipulation */}
|
||||
@@ -340,10 +340,10 @@ export function ThinkingTerminalOptimized({
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between pt-2 border-t border-nothing-gray-800">
|
||||
<p className="font-mono text-xs text-nothing-gray-600">
|
||||
<p className="font-body text-xs text-nothing-gray-600">
|
||||
{t('stream')}
|
||||
</p>
|
||||
<p className="font-mono text-xs text-nothing-gray-600">
|
||||
<p className="font-body text-xs text-nothing-gray-600">
|
||||
{status === 'streaming' ? (
|
||||
<span className="text-status-thinking animate-pulse">● LIVE</span>
|
||||
) : (
|
||||
|
||||
@@ -59,7 +59,7 @@ function DependencyPathVisualizer({
|
||||
<div className="text-xs text-nothing-gray-500 mb-2">
|
||||
{direction === 'upstream' ? t('blastRadius') : t('rootCauseChain')}
|
||||
</div>
|
||||
<div className="font-mono text-sm space-y-1">
|
||||
<div className="font-body text-sm space-y-1">
|
||||
{paths.map((path, i) => (
|
||||
<div key={i} className="flex items-center gap-2">
|
||||
<span className="text-nothing-gray-600">{i === 0 ? '>' : '|'}</span>
|
||||
@@ -97,7 +97,7 @@ function ServiceChainVisualizer({
|
||||
</div>
|
||||
|
||||
{/* ASCII Art 風格圖形 */}
|
||||
<div className="font-mono text-xs leading-relaxed">
|
||||
<div className="font-body text-xs leading-relaxed">
|
||||
{isBlastRadius ? (
|
||||
// Blast Radius: 向上展示誰會受影響
|
||||
<>
|
||||
@@ -200,7 +200,7 @@ function FinOpsVisualizer({ data }: { data: NonNullable<ThinkingStep['finopsData
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
'font-mono',
|
||||
'font-body',
|
||||
action.risk === 'low' && 'text-status-healthy',
|
||||
action.risk === 'medium' && 'text-status-warning',
|
||||
action.risk === 'high' && 'text-status-critical'
|
||||
@@ -338,7 +338,7 @@ export function ThinkingTerminal({
|
||||
</div>
|
||||
<h2 className="font-display text-display-sm">{t('title')}</h2>
|
||||
</div>
|
||||
<span className="font-mono text-xs text-nothing-gray-500">
|
||||
<span className="font-body text-xs text-nothing-gray-500">
|
||||
v0.1.0 | SSE
|
||||
</span>
|
||||
</div>
|
||||
@@ -348,7 +348,7 @@ export function ThinkingTerminal({
|
||||
onClick={() => startThinkingStream()}
|
||||
disabled={isStreaming}
|
||||
className={cn(
|
||||
'w-full py-3 px-4 rounded-button font-mono text-sm transition-all border',
|
||||
'w-full py-3 px-4 rounded-button font-body text-sm transition-all border',
|
||||
isStreaming
|
||||
? 'bg-nothing-gray-800 text-nothing-gray-500 border-nothing-gray-700 cursor-not-allowed'
|
||||
: 'bg-nothing-white text-nothing-black border-transparent hover:bg-nothing-gray-200'
|
||||
@@ -359,7 +359,7 @@ export function ThinkingTerminal({
|
||||
|
||||
{/* Terminal Output */}
|
||||
<div
|
||||
className="bg-nothing-gray-900 rounded-card p-4 overflow-y-auto font-mono text-sm"
|
||||
className="bg-nothing-gray-900 rounded-card p-4 overflow-y-auto font-body text-sm"
|
||||
style={{ maxHeight, minHeight: '150px' }}
|
||||
>
|
||||
{thinkingStream.length === 0 && !isStreaming && !error && (
|
||||
@@ -383,10 +383,10 @@ export function ThinkingTerminal({
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between pt-2 border-t border-nothing-gray-800">
|
||||
<p className="font-mono text-xs text-nothing-gray-600">
|
||||
<p className="font-body text-xs text-nothing-gray-600">
|
||||
{t('stream')}
|
||||
</p>
|
||||
<p className="font-mono text-xs text-nothing-gray-600">
|
||||
<p className="font-body text-xs text-nothing-gray-600">
|
||||
{t('events', { count: thinkingStream.length })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -73,13 +73,13 @@ export function AICommandPanel({ className }: AICommandPanelProps) {
|
||||
<div className="flex items-center justify-between px-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<ShieldCheck className="w-4 h-4 text-claw-blue" />
|
||||
<span className="font-mono text-xs text-nothing-gray-600">
|
||||
<span className="font-body text-xs text-nothing-gray-600">
|
||||
{tApproval('pendingApprovals')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Bell className="w-3 h-3 text-status-warning animate-pulse" />
|
||||
<span className="font-mono text-xs font-bold text-status-warning">
|
||||
<span className="font-body text-xs font-bold text-status-warning">
|
||||
{pendingApprovals.length}
|
||||
</span>
|
||||
</div>
|
||||
@@ -107,7 +107,7 @@ export function AICommandPanel({ className }: AICommandPanelProps) {
|
||||
{pendingApprovals.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center py-6 text-center">
|
||||
<ShieldCheck className="w-8 h-8 text-nothing-gray-300 mb-2" />
|
||||
<p className="font-mono text-xs text-nothing-gray-400">
|
||||
<p className="font-body text-xs text-nothing-gray-400">
|
||||
{tApproval('noApprovals')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -179,18 +179,18 @@ export function AIThinkingPanel({
|
||||
<div className="flex-1 min-w-0">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className="text-xs font-mono text-status-thinking uppercase tracking-widest">
|
||||
<span className="text-xs font-body text-status-thinking uppercase tracking-widest">
|
||||
OpenClaw AI
|
||||
</span>
|
||||
{alertType && (
|
||||
<span className="px-2 py-0.5 rounded text-[10px] font-mono bg-status-thinking/10 text-status-thinking border border-status-thinking/20">
|
||||
<span className="px-2 py-0.5 rounded text-[10px] font-body bg-status-thinking/10 text-status-thinking border border-status-thinking/20">
|
||||
{alertType}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Typewriter text - Dot Matrix Style (Phase 8.0 #16: CSS cursor blink) */}
|
||||
<div className="font-mono text-lg text-nothing-gray-800 tracking-wide">
|
||||
<div className="font-body text-lg text-nothing-gray-800 tracking-wide">
|
||||
<span className="whitespace-pre-wrap">{displayText}</span>
|
||||
<span className="inline-block w-2 h-5 bg-status-thinking ml-1 -mb-0.5 animate-pulse" />
|
||||
</div>
|
||||
@@ -216,7 +216,7 @@ export function AIThinkingPanel({
|
||||
|
||||
{/* Bottom status bar */}
|
||||
<div className="mt-4 pt-4 border-t border-status-thinking/10">
|
||||
<div className="flex items-center justify-between text-xs font-mono text-nothing-gray-500">
|
||||
<div className="flex items-center justify-between text-xs font-body text-nothing-gray-500">
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-status-thinking animate-pulse" />
|
||||
{currentPhase === 'complete' ? t('analysisComplete') : t('processingAlert')}
|
||||
|
||||
@@ -280,7 +280,7 @@ export function HITLSection({ locale: _locale, className }: HITLSectionProps) {
|
||||
disabled={isSimulating}
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 px-3 py-1.5 rounded-lg',
|
||||
'font-mono text-xs font-medium',
|
||||
'font-body text-xs font-medium',
|
||||
'bg-status-critical/10 text-status-critical border border-status-critical/20',
|
||||
'hover:bg-status-critical/20 transition-colors',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
@@ -298,7 +298,7 @@ export function HITLSection({ locale: _locale, className }: HITLSectionProps) {
|
||||
disabled={isSimulating}
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 px-3 py-1.5 rounded-lg',
|
||||
'font-mono text-xs font-medium',
|
||||
'font-body text-xs font-medium',
|
||||
'bg-status-warning/10 text-status-warning border border-status-warning/20',
|
||||
'hover:bg-status-warning/20 transition-colors',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
@@ -332,7 +332,7 @@ export function HITLSection({ locale: _locale, className }: HITLSectionProps) {
|
||||
<GlassCardTitle className="text-nothing-gray-500">
|
||||
{tApproval('noApprovals')}
|
||||
</GlassCardTitle>
|
||||
<p className="text-sm text-nothing-gray-400 mt-2 font-mono">
|
||||
<p className="text-sm text-nothing-gray-400 mt-2 font-body">
|
||||
[SYS] All clear. Awaiting alerts...
|
||||
</p>
|
||||
</GlassCard>
|
||||
@@ -356,7 +356,7 @@ export function HITLSection({ locale: _locale, className }: HITLSectionProps) {
|
||||
|
||||
{/* Phase 3: Permission Warning Badge */}
|
||||
{!hasPermission && (
|
||||
<div className="absolute top-3 right-3 flex items-center gap-1 px-2 py-1 bg-status-critical/10 border border-status-critical/30 rounded text-[10px] font-mono text-status-critical z-10">
|
||||
<div className="absolute top-3 right-3 flex items-center gap-1 px-2 py-1 bg-status-critical/10 border border-status-critical/30 rounded text-[10px] font-body text-status-critical z-10">
|
||||
<Lock className="w-3 h-3" />
|
||||
需要 {getRequiredRolesDisplay(frontendApproval.riskLevel)}
|
||||
</div>
|
||||
@@ -382,11 +382,11 @@ export function HITLSection({ locale: _locale, className }: HITLSectionProps) {
|
||||
{/* Current User */}
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 bg-nothing-gray-100 rounded-lg">
|
||||
<Lock className="w-3.5 h-3.5 text-nothing-gray-500" />
|
||||
<span className="font-mono text-xs text-nothing-gray-600">
|
||||
<span className="font-body text-xs text-nothing-gray-600">
|
||||
登入身份: <span className="font-bold text-nothing-gray-800">{currentUserName}</span>
|
||||
</span>
|
||||
<span className={cn(
|
||||
'px-2 py-0.5 rounded text-[10px] font-mono font-bold uppercase',
|
||||
'px-2 py-0.5 rounded text-[10px] font-body font-bold uppercase',
|
||||
currentUserRole === 'cto' || currentUserRole === 'ciso' || currentUserRole === 'ceo'
|
||||
? 'bg-status-healthy/10 text-status-healthy border border-status-healthy/30'
|
||||
: currentUserRole === 'admin'
|
||||
@@ -399,11 +399,11 @@ export function HITLSection({ locale: _locale, className }: HITLSectionProps) {
|
||||
|
||||
{/* Role Switcher (Demo Only) */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="font-mono text-[10px] text-nothing-gray-400">切換:</span>
|
||||
<span className="font-body text-[10px] text-nothing-gray-400">切換:</span>
|
||||
<button
|
||||
onClick={() => setCurrentUserRole('devops')}
|
||||
className={cn(
|
||||
'px-2 py-1 rounded text-[10px] font-mono font-medium transition-colors',
|
||||
'px-2 py-1 rounded text-[10px] font-body font-medium transition-colors',
|
||||
currentUserRole === 'devops'
|
||||
? 'bg-status-warning/20 text-status-warning'
|
||||
: 'bg-nothing-gray-100 text-nothing-gray-500 hover:bg-nothing-gray-200'
|
||||
@@ -414,7 +414,7 @@ export function HITLSection({ locale: _locale, className }: HITLSectionProps) {
|
||||
<button
|
||||
onClick={() => setCurrentUserRole('cto')}
|
||||
className={cn(
|
||||
'px-2 py-1 rounded text-[10px] font-mono font-medium transition-colors',
|
||||
'px-2 py-1 rounded text-[10px] font-body font-medium transition-colors',
|
||||
currentUserRole === 'cto'
|
||||
? 'bg-status-healthy/20 text-status-healthy'
|
||||
: 'bg-nothing-gray-100 text-nothing-gray-500 hover:bg-nothing-gray-200'
|
||||
@@ -426,7 +426,7 @@ export function HITLSection({ locale: _locale, className }: HITLSectionProps) {
|
||||
</div>
|
||||
|
||||
{/* Debug Info */}
|
||||
<div className="text-[10px] font-mono text-nothing-gray-400">
|
||||
<div className="text-[10px] font-body text-nothing-gray-400">
|
||||
Pending: {pendingApprovals.length} | Status: {openclawStatus}
|
||||
</div>
|
||||
</div>
|
||||
@@ -452,29 +452,29 @@ export function HITLSection({ locale: _locale, className }: HITLSectionProps) {
|
||||
{/* Risk Level Badge */}
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1.5 rounded-lg bg-status-critical/10 border border-status-critical/30 mb-4">
|
||||
<AlertTriangle className="w-4 h-4 text-status-critical" />
|
||||
<span className="font-mono text-sm text-status-critical font-semibold uppercase">
|
||||
<span className="font-body text-sm text-status-critical font-semibold uppercase">
|
||||
{accessDeniedModal.riskLevel} RISK
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Message */}
|
||||
<p className="text-nothing-gray-600 font-mono text-sm mb-2">
|
||||
<p className="text-nothing-gray-600 font-body text-sm mb-2">
|
||||
此操作需要更高權限簽核
|
||||
</p>
|
||||
<p className="text-nothing-gray-500 font-mono text-xs mb-6">
|
||||
<p className="text-nothing-gray-500 font-body text-xs mb-6">
|
||||
您的角色: <span className="text-status-warning font-bold">{currentUserRole.toUpperCase()}</span>
|
||||
</p>
|
||||
|
||||
{/* Required Roles */}
|
||||
<div className="bg-nothing-gray-50 rounded-lg p-4 mb-6">
|
||||
<p className="text-[10px] text-nothing-gray-500 font-mono uppercase tracking-wider mb-2">
|
||||
<p className="text-[10px] text-nothing-gray-500 font-body uppercase tracking-wider mb-2">
|
||||
需要以下角色之一
|
||||
</p>
|
||||
<div className="flex flex-wrap justify-center gap-2">
|
||||
{accessDeniedModal.requiredRoles.split(' / ').map((role) => (
|
||||
<span
|
||||
key={role}
|
||||
className="px-2 py-1 rounded bg-status-healthy/10 text-status-healthy font-mono text-xs font-bold border border-status-healthy/30"
|
||||
className="px-2 py-1 rounded bg-status-healthy/10 text-status-healthy font-body text-xs font-bold border border-status-healthy/30"
|
||||
>
|
||||
{role}
|
||||
</span>
|
||||
@@ -485,7 +485,7 @@ export function HITLSection({ locale: _locale, className }: HITLSectionProps) {
|
||||
{/* Action Button */}
|
||||
<button
|
||||
onClick={() => setAccessDeniedModal(null)}
|
||||
className="w-full px-6 py-3 rounded-xl font-mono text-sm font-semibold bg-nothing-gray-100 text-nothing-gray-700 hover:bg-nothing-gray-200 transition-colors"
|
||||
className="w-full px-6 py-3 rounded-xl font-body text-sm font-semibold bg-nothing-gray-100 text-nothing-gray-700 hover:bg-nothing-gray-200 transition-colors"
|
||||
>
|
||||
了解,返回
|
||||
</button>
|
||||
|
||||
@@ -279,7 +279,7 @@ export function OpenClawPanel({
|
||||
{/* Complete indicator */}
|
||||
{status === 'complete' && (
|
||||
<div className="mt-3 flex justify-center">
|
||||
<span className="px-2 py-0.5 rounded text-[9px] font-mono bg-status-healthy/10 text-status-healthy border border-status-healthy/20">
|
||||
<span className="px-2 py-0.5 rounded text-[9px] font-body bg-status-healthy/10 text-status-healthy border border-status-healthy/20">
|
||||
READY
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -292,12 +292,12 @@ export function OpenClawStateMachine({
|
||||
{/* Status Bar */}
|
||||
<div className="flex items-center justify-between px-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono text-xs text-nothing-gray-500">
|
||||
<span className="font-body text-xs text-nothing-gray-500">
|
||||
STATE:
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
'px-2 py-0.5 rounded text-[10px] font-mono font-bold uppercase',
|
||||
'px-2 py-0.5 rounded text-[10px] font-body font-bold uppercase',
|
||||
machineState === 'idle' && 'bg-nothing-gray-100 text-nothing-gray-600',
|
||||
machineState === 'thinking' && 'bg-status-thinking/10 text-status-thinking',
|
||||
machineState === 'awaiting_approval' && 'bg-status-warning/10 text-status-warning'
|
||||
@@ -317,7 +317,7 @@ export function OpenClawStateMachine({
|
||||
{error && (
|
||||
<div className="flex items-center gap-1 text-status-critical">
|
||||
<AlertCircle className="w-3 h-3" />
|
||||
<span className="text-[10px] font-mono">{error}</span>
|
||||
<span className="text-[10px] font-body">{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -326,7 +326,7 @@ export function OpenClawStateMachine({
|
||||
onClick={fetchPendingApprovals}
|
||||
disabled={isLoading}
|
||||
className={cn(
|
||||
'flex items-center gap-1 px-2 py-1 rounded text-[10px] font-mono',
|
||||
'flex items-center gap-1 px-2 py-1 rounded text-[10px] font-body',
|
||||
'bg-nothing-gray-100 text-nothing-gray-600',
|
||||
'hover:bg-nothing-gray-200 transition-colors',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
@@ -349,10 +349,10 @@ export function OpenClawStateMachine({
|
||||
<div className="space-y-2">
|
||||
{/* 批次操作提示 */}
|
||||
<div className="flex items-center justify-between px-1 mb-2">
|
||||
<span className="text-[10px] font-mono text-nothing-gray-400">
|
||||
<span className="text-[10px] font-body text-nothing-gray-400">
|
||||
{pendingApprovals.length} 筆待簽核
|
||||
</span>
|
||||
<span className="text-[10px] font-mono text-nothing-gray-400">
|
||||
<span className="text-[10px] font-body text-nothing-gray-400">
|
||||
點擊查看詳情
|
||||
</span>
|
||||
</div>
|
||||
@@ -400,13 +400,13 @@ export function OpenClawStateMachine({
|
||||
{/* 標題 */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono text-sm font-medium text-nothing-gray-900 truncate">
|
||||
<span className="font-body text-sm font-medium text-nothing-gray-900 truncate">
|
||||
{title}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-0.5">
|
||||
<span className={cn(
|
||||
'text-[10px] font-mono font-bold uppercase px-1.5 py-0.5 rounded',
|
||||
'text-[10px] font-body font-bold uppercase px-1.5 py-0.5 rounded',
|
||||
isCritical && 'bg-status-critical/10 text-status-critical',
|
||||
isHigh && 'bg-status-warning/10 text-status-warning',
|
||||
approval.riskLevel === 'medium' && 'bg-status-warning/10 text-status-warning',
|
||||
@@ -414,7 +414,7 @@ export function OpenClawStateMachine({
|
||||
)}>
|
||||
{approval.riskLevel}
|
||||
</span>
|
||||
<span className="text-[10px] font-mono text-nothing-gray-400">
|
||||
<span className="text-[10px] font-body text-nothing-gray-400">
|
||||
{approval.currentSignatures}/{approval.requiredSignatures}
|
||||
</span>
|
||||
</div>
|
||||
@@ -530,10 +530,10 @@ export function OpenClawStateMachine({
|
||||
{machineState === 'idle' && pendingApprovals.length === 0 && !isLoading && (
|
||||
<div className="text-center py-6 border border-dashed border-nothing-gray-200 rounded-lg">
|
||||
<CheckCircle2 className="w-8 h-8 mx-auto mb-2 text-status-healthy/50" />
|
||||
<p className="font-mono text-xs text-nothing-gray-400">
|
||||
<p className="font-body text-xs text-nothing-gray-400">
|
||||
{t('ai.standby')}
|
||||
</p>
|
||||
<p className="font-mono text-[10px] text-nothing-gray-300 mt-1">
|
||||
<p className="font-body text-[10px] text-nothing-gray-300 mt-1">
|
||||
{lastFetch
|
||||
? `Last check: ${lastFetch.toLocaleTimeString()}`
|
||||
: 'Waiting for first fetch...'}
|
||||
@@ -544,7 +544,7 @@ export function OpenClawStateMachine({
|
||||
{/* Demo mode warning */}
|
||||
{demoMode && (
|
||||
<div className="bg-status-warning/10 border border-status-warning/20 rounded-lg p-3">
|
||||
<p className="font-mono text-xs text-status-warning">
|
||||
<p className="font-body text-xs text-status-warning">
|
||||
Demo mode is enabled. Real API polling is disabled.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -134,7 +134,7 @@ function ThinkingLine({
|
||||
// 減少高頻 setState 造成的 DOM 重繪
|
||||
|
||||
return (
|
||||
<div className="flex items-start gap-2 font-mono text-sm leading-relaxed">
|
||||
<div className="flex items-start gap-2 font-body text-sm leading-relaxed">
|
||||
{/* Prefix */}
|
||||
<span
|
||||
className={cn(
|
||||
@@ -232,7 +232,7 @@ export function ThinkingStream({
|
||||
{messages.slice(0, currentIndex).map((msg) => (
|
||||
<div
|
||||
key={msg.id}
|
||||
className="flex items-start gap-2 font-mono text-sm leading-relaxed opacity-60"
|
||||
className="flex items-start gap-2 font-body text-sm leading-relaxed opacity-60"
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
|
||||
@@ -193,7 +193,7 @@ function LongPressButton({
|
||||
onPointerCancel={handlePointerUp}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
'relative overflow-hidden px-6 py-3.5 rounded-xl font-mono text-sm font-semibold',
|
||||
'relative overflow-hidden px-6 py-3.5 rounded-xl font-body text-sm font-semibold',
|
||||
'transition-all duration-300 ease-out select-none touch-none',
|
||||
'active:scale-[0.97]',
|
||||
disabled
|
||||
@@ -360,7 +360,7 @@ export function ApprovalCard({
|
||||
{readOnly && finalStatus && (
|
||||
<div className={cn(
|
||||
'absolute top-3 right-3 flex items-center gap-1.5 px-2.5 py-1 rounded-lg',
|
||||
'font-mono text-xs font-bold uppercase',
|
||||
'font-body text-xs font-bold uppercase',
|
||||
finalStatus === 'executed' && 'bg-status-healthy/10 text-status-healthy border border-status-healthy/20',
|
||||
finalStatus === 'approved' && 'bg-claw-blue/10 text-claw-blue border border-claw-blue/20',
|
||||
finalStatus === 'rejected' && 'bg-nothing-gray-100 text-nothing-gray-500 border border-nothing-gray-200',
|
||||
@@ -389,7 +389,7 @@ export function ApprovalCard({
|
||||
<div
|
||||
className={cn(
|
||||
'inline-flex items-center gap-2 px-3 py-1.5 rounded-lg',
|
||||
'font-mono text-xs font-semibold tracking-wide uppercase',
|
||||
'font-body text-xs font-semibold tracking-wide uppercase',
|
||||
isCritical
|
||||
? 'bg-status-critical/10 text-status-critical border border-status-critical/30'
|
||||
: request.riskLevel === 'high'
|
||||
@@ -430,12 +430,12 @@ export function ApprovalCard({
|
||||
|
||||
{/* Multi-Sig Counter + Progress - Enhanced */}
|
||||
<div className="text-right">
|
||||
<div className="text-[10px] text-nothing-gray-500 font-mono uppercase tracking-wider mb-1">
|
||||
<div className="text-[10px] text-nothing-gray-500 font-body uppercase tracking-wider mb-1">
|
||||
{t('signatures')}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'text-2xl font-mono font-bold tabular-nums',
|
||||
'text-2xl font-body font-bold tabular-nums',
|
||||
'transition-colors duration-200',
|
||||
needsMoreSignatures ? 'text-status-warning' : 'text-status-healthy'
|
||||
)}
|
||||
@@ -460,7 +460,7 @@ export function ApprovalCard({
|
||||
{/* 已簽核人員列表 (有簽名時顯示) */}
|
||||
{signatures && signatures.length > 0 && (
|
||||
<div className="px-6 -mt-2 mb-4">
|
||||
<div className="flex items-center gap-2 text-[10px] text-nothing-gray-500 font-mono">
|
||||
<div className="flex items-center gap-2 text-[10px] text-nothing-gray-500 font-body">
|
||||
<Shield className="w-3 h-3" />
|
||||
<span>已簽核:</span>
|
||||
{signatures.map((sig, idx) => (
|
||||
@@ -486,7 +486,7 @@ export function ApprovalCard({
|
||||
|
||||
{/* kubectl 命令: 獨立顯示,可複製 */}
|
||||
{request.action.includes('|') && (
|
||||
<code className="block mb-3 px-3 py-2 bg-nothing-gray-100 rounded-lg text-xs font-mono text-nothing-gray-700 break-all border border-nothing-gray-200">
|
||||
<code className="block mb-3 px-3 py-2 bg-nothing-gray-100 rounded-lg text-xs font-body text-nothing-gray-700 break-all border border-nothing-gray-200">
|
||||
{request.action.split('|')[1].trim()}
|
||||
</code>
|
||||
)}
|
||||
@@ -504,7 +504,7 @@ export function ApprovalCard({
|
||||
{/* Blast Radius - 可折疊面板 */}
|
||||
<details className="mb-4 group" open={isCritical || isDestructive}>
|
||||
<summary className="flex items-center justify-between cursor-pointer select-none py-2 px-3 -mx-3 rounded-lg hover:bg-nothing-gray-50 transition-colors">
|
||||
<h4 className="text-[10px] font-mono text-nothing-gray-500 uppercase tracking-widest flex items-center gap-2">
|
||||
<h4 className="text-[10px] font-body text-nothing-gray-500 uppercase tracking-widest flex items-center gap-2">
|
||||
<Zap className="w-3 h-3" />
|
||||
{tBlast('title')}
|
||||
{/* 摘要預覽 */}
|
||||
@@ -528,14 +528,14 @@ export function ApprovalCard({
|
||||
/>
|
||||
<div className="col-span-2">
|
||||
<div className="bg-nothing-gray-50 rounded-lg p-3 border border-nothing-gray-200">
|
||||
<div className="text-[10px] text-nothing-gray-500 font-mono uppercase tracking-wider mb-2">
|
||||
<div className="text-[10px] text-nothing-gray-500 font-body uppercase tracking-wider mb-2">
|
||||
{tBlast('relatedServices')}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{(request.blastRadius.relatedServices ?? []).map((svc) => (
|
||||
<span
|
||||
key={svc}
|
||||
className="px-2 py-0.5 rounded text-xs font-mono bg-white border border-nothing-gray-200 text-nothing-gray-700"
|
||||
className="px-2 py-0.5 rounded text-xs font-body bg-white border border-nothing-gray-200 text-nothing-gray-700"
|
||||
>
|
||||
{svc}
|
||||
</span>
|
||||
@@ -552,7 +552,7 @@ export function ApprovalCard({
|
||||
<div className="mb-5 p-4 bg-status-critical/5 border border-status-critical/20 rounded-lg">
|
||||
<div className="flex items-center gap-2">
|
||||
<StatusOrb status="critical" size="sm" pulse />
|
||||
<span className="text-sm font-mono text-status-critical font-semibold">
|
||||
<span className="text-sm font-body text-status-critical font-semibold">
|
||||
{tBlast('dataImpact')}: {dataImpactLabel}
|
||||
</span>
|
||||
</div>
|
||||
@@ -562,12 +562,12 @@ export function ApprovalCard({
|
||||
{/* Dry-Run Checks - 可折疊面板 (預設收合) */}
|
||||
<details className="mb-4 group">
|
||||
<summary className="flex items-center justify-between cursor-pointer select-none py-2 px-3 -mx-3 rounded-lg hover:bg-nothing-gray-50 transition-colors">
|
||||
<h4 className="text-[10px] font-mono text-nothing-gray-500 uppercase tracking-widest flex items-center gap-2">
|
||||
<h4 className="text-[10px] font-body text-nothing-gray-500 uppercase tracking-widest flex items-center gap-2">
|
||||
<Clock className="w-3 h-3" />
|
||||
{tDryRun('validation')}
|
||||
{/* 通過/失敗摘要 */}
|
||||
<span className={cn(
|
||||
'text-[10px] font-mono px-1.5 py-0.5 rounded',
|
||||
'text-[10px] font-body px-1.5 py-0.5 rounded',
|
||||
allChecksPassed
|
||||
? 'bg-status-healthy/10 text-status-healthy'
|
||||
: 'bg-status-critical/10 text-status-critical'
|
||||
@@ -595,13 +595,13 @@ export function ApprovalCard({
|
||||
) : (
|
||||
<XCircle className="w-4 h-4 text-status-critical" />
|
||||
)}
|
||||
<span className="text-sm text-nothing-gray-800 font-mono font-medium">
|
||||
<span className="text-sm text-nothing-gray-800 font-body font-medium">
|
||||
{check.name}
|
||||
</span>
|
||||
</div>
|
||||
{check.message && (
|
||||
<span className={cn(
|
||||
'text-xs font-mono px-2 py-0.5 rounded',
|
||||
'text-xs font-body px-2 py-0.5 rounded',
|
||||
check.passed
|
||||
? 'bg-status-healthy/10 text-status-healthy'
|
||||
: 'bg-status-critical/10 text-status-critical'
|
||||
@@ -621,7 +621,7 @@ export function ApprovalCard({
|
||||
<GlassCardFooter className="sticky bottom-0 bg-white border-t border-nothing-gray-100 p-4 mt-auto">
|
||||
{/* Meta */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="text-xs text-nothing-gray-500 font-mono">
|
||||
<div className="text-xs text-nothing-gray-500 font-body">
|
||||
<span>{t('requestedBy')} </span>
|
||||
<span className="text-nothing-gray-700">{request.requestedBy}</span>
|
||||
<span className="mx-2">|</span>
|
||||
@@ -636,7 +636,7 @@ export function ApprovalCard({
|
||||
<svg className="w-5 h-5 text-claw-blue animate-spin" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeDasharray="60" strokeDashoffset="20" />
|
||||
</svg>
|
||||
<span className="text-sm font-mono text-claw-blue font-semibold">
|
||||
<span className="text-sm font-body text-claw-blue font-semibold">
|
||||
正在處理中...
|
||||
</span>
|
||||
</div>
|
||||
@@ -646,7 +646,7 @@ export function ApprovalCard({
|
||||
{/* 唯讀模式 - 不顯示按鈕 */}
|
||||
{readOnly ? (
|
||||
<div className="text-center py-2">
|
||||
<p className="text-xs text-nothing-gray-400 font-mono">
|
||||
<p className="text-xs text-nothing-gray-400 font-body">
|
||||
此紀錄為歷史存檔,僅供稽核參考
|
||||
</p>
|
||||
</div>
|
||||
@@ -657,7 +657,7 @@ export function ApprovalCard({
|
||||
<div className="mb-4 p-3 bg-status-warning/10 border border-status-warning/30 rounded-lg">
|
||||
<div className="flex items-center gap-2">
|
||||
<StatusOrb status="warning" size="sm" />
|
||||
<span className="text-sm font-mono text-status-warning">
|
||||
<span className="text-sm font-body text-status-warning">
|
||||
{t('waitingSecondSig')}
|
||||
</span>
|
||||
</div>
|
||||
@@ -673,7 +673,7 @@ export function ApprovalCard({
|
||||
disabled={isLoading || isProcessing}
|
||||
className={cn(
|
||||
'flex-1 flex items-center justify-center gap-2',
|
||||
'px-6 py-3.5 rounded-xl font-mono text-sm font-semibold',
|
||||
'px-6 py-3.5 rounded-xl font-body text-sm font-semibold',
|
||||
'bg-nothing-gray-50 border-2 border-nothing-gray-200 text-nothing-gray-600',
|
||||
'hover:border-nothing-gray-300 hover:bg-nothing-gray-100 hover:text-nothing-gray-800',
|
||||
'active:scale-[0.97]',
|
||||
@@ -707,7 +707,7 @@ export function ApprovalCard({
|
||||
</div>
|
||||
|
||||
{/* Long Press Hint - Subtle */}
|
||||
<p className="mt-4 text-[11px] text-nothing-gray-400 text-center font-mono tracking-wide">
|
||||
<p className="mt-4 text-[11px] text-nothing-gray-400 text-center font-body tracking-wide">
|
||||
{t('holdHint', {
|
||||
seconds: holdSeconds,
|
||||
action: hasPartialSignatures ? t('actionSign') : isDestructive ? t('actionConfirm') : t('actionApprove')
|
||||
@@ -740,9 +740,9 @@ function MetricBox({ label, value, className, highlight = false }: MetricBoxProp
|
||||
highlight ? 'border-status-warning/30' : 'border-nothing-gray-200',
|
||||
className
|
||||
)}>
|
||||
<div className="text-[10px] text-nothing-gray-500 font-mono uppercase tracking-wider mb-1">{label}</div>
|
||||
<div className="text-[10px] text-nothing-gray-500 font-body uppercase tracking-wider mb-1">{label}</div>
|
||||
<div className={cn(
|
||||
'text-lg font-mono font-bold tabular-nums',
|
||||
'text-lg font-body font-bold tabular-nums',
|
||||
highlight ? 'text-status-warning' : 'text-nothing-gray-900'
|
||||
)}>
|
||||
{value}
|
||||
|
||||
@@ -143,14 +143,14 @@ export function ApprovalThreadItem({
|
||||
{/* 頂部: 風險指示器 + 時間 */}
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className={cn(
|
||||
'flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs font-mono',
|
||||
'flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs font-body',
|
||||
riskConfig.bgColor,
|
||||
riskConfig.color
|
||||
)}>
|
||||
<RiskIcon className="w-3 h-3" />
|
||||
<span className="uppercase">{approval.riskLevel}</span>
|
||||
</div>
|
||||
<span className="text-xs text-nothing-gray-400 font-mono">
|
||||
<span className="text-xs text-nothing-gray-400 font-body">
|
||||
{relativeTime}
|
||||
</span>
|
||||
</div>
|
||||
@@ -167,7 +167,7 @@ export function ApprovalThreadItem({
|
||||
|
||||
{/* 底部: 動作類型 + 狀態指示 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-nothing-gray-500 font-mono truncate max-w-[70%]">
|
||||
<span className="text-xs text-nothing-gray-500 font-body truncate max-w-[70%]">
|
||||
{actionSummary}
|
||||
</span>
|
||||
<StatusOrb
|
||||
|
||||
@@ -183,7 +183,7 @@ export function BatchModeSelector({
|
||||
<h3 className="font-heading text-sm font-bold text-nothing-gray-900">
|
||||
{tBatch('title')}
|
||||
</h3>
|
||||
<div className="flex items-center gap-2 text-xs font-mono">
|
||||
<div className="flex items-center gap-2 text-xs font-body">
|
||||
<span className="text-nothing-gray-400">{stats.total} {t('pendingApprovals')}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -192,25 +192,25 @@ export function BatchModeSelector({
|
||||
<div className="grid grid-cols-4 gap-2 mb-4">
|
||||
<div className="flex items-center gap-1.5 px-2 py-1 rounded-lg bg-nothing-gray-50">
|
||||
<StatusOrb status="healthy" size="sm" />
|
||||
<span className="text-xs font-mono text-nothing-gray-600">
|
||||
<span className="text-xs font-body text-nothing-gray-600">
|
||||
LOW: {stats.low}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 px-2 py-1 rounded-lg bg-claw-blue/5">
|
||||
<StatusOrb status="healthy" size="sm" />
|
||||
<span className="text-xs font-mono text-claw-blue">
|
||||
<span className="text-xs font-body text-claw-blue">
|
||||
MED: {stats.medium}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 px-2 py-1 rounded-lg bg-status-warning/5">
|
||||
<StatusOrb status="warning" size="sm" />
|
||||
<span className="text-xs font-mono text-status-warning">
|
||||
<span className="text-xs font-body text-status-warning">
|
||||
HIGH: {stats.high}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 px-2 py-1 rounded-lg bg-status-critical/5">
|
||||
<StatusOrb status="critical" size="sm" pulse />
|
||||
<span className="text-xs font-mono text-status-critical">
|
||||
<span className="text-xs font-body text-status-critical">
|
||||
CRIT: {stats.critical}
|
||||
</span>
|
||||
</div>
|
||||
@@ -241,7 +241,7 @@ export function BatchModeSelector({
|
||||
{tBatch('bulkApprove')}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs font-mono text-nothing-gray-400">
|
||||
<span className="text-xs font-body text-nothing-gray-400">
|
||||
{stats.canBulkApprove} {tBatch('eligible')}
|
||||
</span>
|
||||
</button>
|
||||
@@ -263,7 +263,7 @@ export function BatchModeSelector({
|
||||
{tBatch('sequential')}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs font-mono text-nothing-gray-400">
|
||||
<span className="text-xs font-body text-nothing-gray-400">
|
||||
{stats.total} {tBatch('items')}
|
||||
</span>
|
||||
</button>
|
||||
@@ -287,7 +287,7 @@ export function BatchModeSelector({
|
||||
{tBatch('criticalOnly')}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs font-mono text-status-critical">
|
||||
<span className="text-xs font-body text-status-critical">
|
||||
{stats.critical} {tBatch('items')}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@@ -109,14 +109,14 @@ export function CompactApprovalItem({
|
||||
{/* 標題與摘要 */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono text-sm font-medium text-nothing-gray-900 truncate">
|
||||
<span className="font-body text-sm font-medium text-nothing-gray-900 truncate">
|
||||
{title}
|
||||
</span>
|
||||
<ChevronRight className="w-4 h-4 text-nothing-gray-400 flex-shrink-0 group-hover:translate-x-0.5 transition-transform" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-0.5">
|
||||
<span className={cn(
|
||||
'text-[10px] font-mono font-bold uppercase px-1.5 py-0.5 rounded',
|
||||
'text-[10px] font-body font-bold uppercase px-1.5 py-0.5 rounded',
|
||||
isCritical && 'bg-status-critical/10 text-status-critical',
|
||||
isHigh && 'bg-status-warning/10 text-status-warning',
|
||||
request.riskLevel === 'medium' && 'bg-status-warning/10 text-status-warning',
|
||||
@@ -124,7 +124,7 @@ export function CompactApprovalItem({
|
||||
)}>
|
||||
{tRisk(request.riskLevel)}
|
||||
</span>
|
||||
<span className="text-[10px] font-mono text-nothing-gray-400">
|
||||
<span className="text-[10px] font-body text-nothing-gray-400">
|
||||
{request.currentSignatures}/{request.requiredSignatures}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -236,7 +236,7 @@ export function ConversationalView({
|
||||
{/* SSE 連線狀態 */}
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-1 px-2 py-1 rounded-lg text-xs font-mono',
|
||||
'flex items-center gap-1 px-2 py-1 rounded-lg text-xs font-body',
|
||||
isConnected
|
||||
? 'bg-status-healthy/10 text-status-healthy'
|
||||
: 'bg-status-warning/10 text-status-warning'
|
||||
@@ -264,7 +264,7 @@ export function ConversationalView({
|
||||
{/* 導航提示 */}
|
||||
{approvals.length > 1 && (
|
||||
<div className="mt-2 flex items-center justify-between text-xs text-nothing-gray-400">
|
||||
<span className="font-mono">
|
||||
<span className="font-body">
|
||||
{selectedIndex + 1} / {approvals.length}
|
||||
</span>
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -302,7 +302,7 @@ export function ConversationalView({
|
||||
{approvals.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-nothing-gray-400">
|
||||
<StatusOrb status="healthy" size="lg" />
|
||||
<p className="mt-4 text-sm font-mono">{t('noPendingApprovals')}</p>
|
||||
<p className="mt-4 text-sm font-body">{t('noPendingApprovals')}</p>
|
||||
</div>
|
||||
) : (
|
||||
approvals.map((approval) => (
|
||||
@@ -353,7 +353,7 @@ export function ConversationalView({
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 分頁指示器 */}
|
||||
<div className="flex items-center gap-1 px-2 py-1 rounded-lg text-xs font-mono bg-nothing-gray-100">
|
||||
<div className="flex items-center gap-1 px-2 py-1 rounded-lg text-xs font-body bg-nothing-gray-100">
|
||||
<span>{selectedIndex + 1} / {approvals.length}</span>
|
||||
</div>
|
||||
{/* 快速導航按鈕 */}
|
||||
@@ -411,7 +411,7 @@ export function ConversationalView({
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<div className="text-center text-nothing-gray-400">
|
||||
<MessageSquare className="w-12 h-12 mx-auto mb-4 opacity-50" aria-hidden="true" />
|
||||
<p className="text-sm font-mono">{t('selectApproval')}</p>
|
||||
<p className="text-sm font-body">{t('selectApproval')}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -439,7 +439,7 @@ export function ConversationalView({
|
||||
<ShortcutRow keys={['?']} label={tCommon('showShortcuts')} />
|
||||
<ShortcutRow keys={['Esc']} label={tCommon('close')} />
|
||||
</div>
|
||||
<p className="mt-4 text-xs text-nothing-gray-400 font-mono">
|
||||
<p className="mt-4 text-xs text-nothing-gray-400 font-body">
|
||||
{t('holdToApproveHint')}
|
||||
</p>
|
||||
</div>
|
||||
@@ -469,7 +469,7 @@ function ShortcutRow({
|
||||
<kbd
|
||||
key={key}
|
||||
className={cn(
|
||||
'px-2 py-1 rounded text-xs font-mono border',
|
||||
'px-2 py-1 rounded text-xs font-body border',
|
||||
highlight
|
||||
? 'bg-status-healthy/10 text-status-healthy border-status-healthy/30'
|
||||
: 'bg-nothing-gray-100 text-nothing-gray-700 border-nothing-gray-200'
|
||||
|
||||
@@ -245,7 +245,7 @@ export function LiveApprovalPanel({
|
||||
<h2 className="font-heading text-2xl font-bold text-nothing-gray-900">
|
||||
{t('pendingApprovals')}
|
||||
</h2>
|
||||
<span className="px-3 py-1 rounded-full bg-nothing-gray-100 text-nothing-gray-600 font-mono text-sm">
|
||||
<span className="px-3 py-1 rounded-full bg-nothing-gray-100 text-nothing-gray-600 font-body text-sm">
|
||||
{approvals.length}
|
||||
</span>
|
||||
</div>
|
||||
@@ -254,14 +254,14 @@ export function LiveApprovalPanel({
|
||||
{/* Error State */}
|
||||
{error && (
|
||||
<div className="p-4 bg-status-critical/10 border border-status-critical/30 rounded-lg">
|
||||
<p className="text-sm text-status-critical font-mono">{t('fetchError')}: {error}</p>
|
||||
<p className="text-sm text-status-critical font-body">{t('fetchError')}: {error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Phase 20: CSRF Error State */}
|
||||
{csrfError && (
|
||||
<div className="p-4 bg-status-warning/10 border border-status-warning/30 rounded-lg">
|
||||
<p className="text-sm text-status-warning font-mono">
|
||||
<p className="text-sm text-status-warning font-body">
|
||||
⚠️ CSRF Token 載入失敗,簽核功能暫時無法使用
|
||||
</p>
|
||||
</div>
|
||||
@@ -270,7 +270,7 @@ export function LiveApprovalPanel({
|
||||
{/* Phase 20: CSRF Loading State */}
|
||||
{csrfLoading && (
|
||||
<div className="p-4 bg-claw-blue/10 border border-claw-blue/30 rounded-lg">
|
||||
<p className="text-sm text-claw-blue font-mono">
|
||||
<p className="text-sm text-claw-blue font-body">
|
||||
🔐 正在載入安全憑證...
|
||||
</p>
|
||||
</div>
|
||||
@@ -282,7 +282,7 @@ export function LiveApprovalPanel({
|
||||
<GlassCardContent>
|
||||
<div className="py-12 text-center">
|
||||
<StatusOrb status="healthy" size="lg" glow className="mx-auto mb-4" />
|
||||
<p className="text-nothing-gray-500 font-mono">{t('noApprovals')}</p>
|
||||
<p className="text-nothing-gray-500 font-body">{t('noApprovals')}</p>
|
||||
</div>
|
||||
</GlassCardContent>
|
||||
</GlassCard>
|
||||
@@ -291,11 +291,11 @@ export function LiveApprovalPanel({
|
||||
{/* Current User Role Display (Phase 3 Demo) */}
|
||||
<div className="flex items-center gap-2 px-4 py-2 bg-nothing-gray-100 rounded-lg w-fit">
|
||||
<Lock className="w-4 h-4 text-nothing-gray-500" />
|
||||
<span className="font-mono text-xs text-nothing-gray-600">
|
||||
<span className="font-body text-xs text-nothing-gray-600">
|
||||
登入身份: <span className="font-bold text-nothing-gray-800">{currentUser.name}</span>
|
||||
</span>
|
||||
<span className={cn(
|
||||
'px-2 py-0.5 rounded text-[10px] font-mono font-bold uppercase',
|
||||
'px-2 py-0.5 rounded text-[10px] font-body font-bold uppercase',
|
||||
currentUser.role === 'cto' || currentUser.role === 'ciso' || currentUser.role === 'ceo'
|
||||
? 'bg-status-healthy/10 text-status-healthy border border-status-healthy/30'
|
||||
: currentUser.role === 'admin'
|
||||
@@ -326,7 +326,7 @@ export function LiveApprovalPanel({
|
||||
{/* 🔴 已解決狀態橫幅 */}
|
||||
{isResolved && (
|
||||
<div className={cn(
|
||||
'absolute -top-3 left-1/2 -translate-x-1/2 z-10 px-4 py-1 rounded-full font-mono text-xs font-bold uppercase tracking-wider shadow-lg',
|
||||
'absolute -top-3 left-1/2 -translate-x-1/2 z-10 px-4 py-1 rounded-full font-body text-xs font-bold uppercase tracking-wider shadow-lg',
|
||||
resolvedStatus === 'approved'
|
||||
? 'bg-status-healthy text-white'
|
||||
: 'bg-status-critical text-white'
|
||||
@@ -345,7 +345,7 @@ export function LiveApprovalPanel({
|
||||
|
||||
{/* Permission Warning Badge (Phase 3) */}
|
||||
{!canSignApproval(currentUser.role, approval.riskLevel) && (
|
||||
<div className="absolute top-2 right-2 flex items-center gap-1 px-2 py-1 bg-status-critical/10 border border-status-critical/30 rounded text-[10px] font-mono text-status-critical">
|
||||
<div className="absolute top-2 right-2 flex items-center gap-1 px-2 py-1 bg-status-critical/10 border border-status-critical/30 rounded text-[10px] font-body text-status-critical">
|
||||
<Lock className="w-3 h-3" />
|
||||
需要 {getRequiredRolesDisplay(approval.riskLevel)}
|
||||
</div>
|
||||
@@ -356,7 +356,7 @@ export function LiveApprovalPanel({
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-status-healthy/20 rounded-xl">
|
||||
<div className="text-center">
|
||||
<StatusOrb status="healthy" size="lg" glow />
|
||||
<p className="mt-2 text-status-healthy font-mono font-semibold">
|
||||
<p className="mt-2 text-status-healthy font-body font-semibold">
|
||||
{t('signSuccess')}
|
||||
</p>
|
||||
</div>
|
||||
@@ -368,7 +368,7 @@ export function LiveApprovalPanel({
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-status-critical/20 rounded-xl">
|
||||
<div className="text-center">
|
||||
<StatusOrb status="critical" size="lg" glow />
|
||||
<p className="mt-2 text-status-critical font-mono font-semibold">
|
||||
<p className="mt-2 text-status-critical font-body font-semibold">
|
||||
{tCommon('error')}
|
||||
</p>
|
||||
</div>
|
||||
@@ -394,20 +394,20 @@ export function LiveApprovalPanel({
|
||||
value={rejectReason}
|
||||
onChange={(e) => setRejectReason(e.target.value)}
|
||||
placeholder={t('enterReason')}
|
||||
className="w-full h-32 px-4 py-3 rounded-lg border border-nothing-gray-200 bg-white/50 resize-none font-mono text-sm focus:outline-none focus:ring-2 focus:ring-status-critical/50"
|
||||
className="w-full h-32 px-4 py-3 rounded-lg border border-nothing-gray-200 bg-white/50 resize-none font-body text-sm focus:outline-none focus:ring-2 focus:ring-status-critical/50"
|
||||
autoFocus
|
||||
/>
|
||||
<div className="flex gap-3 mt-4">
|
||||
<button
|
||||
onClick={() => setRejectModalId(null)}
|
||||
className="flex-1 px-6 py-3 rounded-lg border border-nothing-gray-300 text-nothing-gray-600 font-mono text-sm hover:bg-nothing-gray-100 transition-colors"
|
||||
className="flex-1 px-6 py-3 rounded-lg border border-nothing-gray-300 text-nothing-gray-600 font-body text-sm hover:bg-nothing-gray-100 transition-colors"
|
||||
>
|
||||
{tCommon('cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={confirmReject}
|
||||
disabled={!rejectReason.trim()}
|
||||
className="flex-1 px-6 py-3 rounded-lg bg-status-critical text-white font-mono text-sm hover:bg-status-critical/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className="flex-1 px-6 py-3 rounded-lg bg-status-critical text-white font-body text-sm hover:bg-status-critical/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{t('reject')}
|
||||
</button>
|
||||
@@ -438,29 +438,29 @@ export function LiveApprovalPanel({
|
||||
{/* Risk Level Badge */}
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1.5 rounded-lg bg-status-critical/10 border border-status-critical/30 mb-4">
|
||||
<AlertTriangle className="w-4 h-4 text-status-critical" />
|
||||
<span className="font-mono text-sm text-status-critical font-semibold uppercase">
|
||||
<span className="font-body text-sm text-status-critical font-semibold uppercase">
|
||||
{accessDeniedModal.riskLevel} RISK
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Message */}
|
||||
<p className="text-nothing-gray-600 font-mono text-sm mb-2">
|
||||
<p className="text-nothing-gray-600 font-body text-sm mb-2">
|
||||
此操作需要更高權限簽核
|
||||
</p>
|
||||
<p className="text-nothing-gray-500 font-mono text-xs mb-6">
|
||||
<p className="text-nothing-gray-500 font-body text-xs mb-6">
|
||||
您的角色: <span className="text-status-warning font-bold">{currentUser.role.toUpperCase()}</span>
|
||||
</p>
|
||||
|
||||
{/* Required Roles */}
|
||||
<div className="bg-nothing-gray-50 rounded-lg p-4 mb-6">
|
||||
<p className="text-[10px] text-nothing-gray-500 font-mono uppercase tracking-wider mb-2">
|
||||
<p className="text-[10px] text-nothing-gray-500 font-body uppercase tracking-wider mb-2">
|
||||
需要以下角色之一
|
||||
</p>
|
||||
<div className="flex flex-wrap justify-center gap-2">
|
||||
{accessDeniedModal.requiredRoles.split(' / ').map((role) => (
|
||||
<span
|
||||
key={role}
|
||||
className="px-2 py-1 rounded bg-status-healthy/10 text-status-healthy font-mono text-xs font-bold border border-status-healthy/30"
|
||||
className="px-2 py-1 rounded bg-status-healthy/10 text-status-healthy font-body text-xs font-bold border border-status-healthy/30"
|
||||
>
|
||||
{role}
|
||||
</span>
|
||||
@@ -471,7 +471,7 @@ export function LiveApprovalPanel({
|
||||
{/* Action Button */}
|
||||
<button
|
||||
onClick={() => setAccessDeniedModal(null)}
|
||||
className="w-full px-6 py-3 rounded-xl font-mono text-sm font-semibold bg-nothing-gray-100 text-nothing-gray-700 hover:bg-nothing-gray-200 transition-colors"
|
||||
className="w-full px-6 py-3 rounded-xl font-body text-sm font-semibold bg-nothing-gray-100 text-nothing-gray-700 hover:bg-nothing-gray-200 transition-colors"
|
||||
>
|
||||
了解,返回
|
||||
</button>
|
||||
|
||||
@@ -155,7 +155,7 @@ function Step({ step, index, isLast }: StepProps) {
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={cn(
|
||||
'text-xs font-mono uppercase tracking-wider',
|
||||
'text-xs font-body uppercase tracking-wider',
|
||||
config.text
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -139,7 +139,7 @@ function MetricCard({ metric }: MetricCardProps) {
|
||||
<p className="dot-matrix-display">
|
||||
{metric.value}
|
||||
{metric.unit && (
|
||||
<span className="text-base font-mono font-normal text-ink-secondary ml-2">
|
||||
<span className="text-base font-body font-normal text-ink-secondary ml-2">
|
||||
{metric.unit}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -98,7 +98,7 @@ function CustomTooltip({ active, payload, unit }: CustomTooltipProps) {
|
||||
return (
|
||||
<div className="bg-nothing-gray-900/95 backdrop-blur-sm px-3 py-2 rounded-lg border border-nothing-gray-700">
|
||||
<p className="text-nothing-gray-400 text-xs mb-1">{time}</p>
|
||||
<p className="text-white font-mono text-sm font-semibold">
|
||||
<p className="text-white font-body text-sm font-semibold">
|
||||
{data.value.toFixed(2)}{unit && ` ${unit}`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -93,7 +93,7 @@ export function DataPincerCard({
|
||||
{/* 標題 (如有) */}
|
||||
{title && (
|
||||
<div className="mb-4 pb-3 border-b border-nothing-gray-200/40">
|
||||
<h3 className="font-mono text-xs uppercase tracking-wider text-ink-secondary">
|
||||
<h3 className="font-body text-xs uppercase tracking-wider text-ink-secondary">
|
||||
{title}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@@ -90,11 +90,11 @@ export function ConnectionStatus({
|
||||
|
||||
{showLabel && (
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-mono text-nothing-gray-600">
|
||||
<span className="text-xs font-body text-nothing-gray-600">
|
||||
{t(labelKey)}
|
||||
</span>
|
||||
{showLastUpdate && lastUpdate && (
|
||||
<span className="text-[10px] font-mono text-nothing-gray-400">
|
||||
<span className="text-[10px] font-body text-nothing-gray-400">
|
||||
{lastUpdateStr}
|
||||
</span>
|
||||
)}
|
||||
@@ -102,7 +102,7 @@ export function ConnectionStatus({
|
||||
)}
|
||||
|
||||
{mockMode && (
|
||||
<span className="px-1.5 py-0.5 text-[10px] font-mono bg-status-warning/10 text-status-warning rounded">
|
||||
<span className="px-1.5 py-0.5 text-[10px] font-body bg-status-warning/10 text-status-warning rounded">
|
||||
{t('mockMode')}
|
||||
</span>
|
||||
)}
|
||||
@@ -127,7 +127,7 @@ export function ConnectionStatusCompact({ className }: { className?: string }) {
|
||||
)}
|
||||
>
|
||||
<StatusOrb status={status} size="sm" pulse={connectionStatus !== 'connected'} />
|
||||
<span className="font-mono text-xs text-nothing-gray-600 uppercase">
|
||||
<span className="font-body text-xs text-nothing-gray-600 uppercase">
|
||||
{t(connectionLabelKeys[connectionStatus])}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -113,7 +113,7 @@ export function HostCard({
|
||||
<div className="flex items-center gap-3">
|
||||
<StatusOrb status={status} size="md" glow />
|
||||
<div>
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded-md bg-nothing-gray-100 text-nothing-gray-600 text-xs font-mono">
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded-md bg-nothing-gray-100 text-nothing-gray-600 text-xs font-body">
|
||||
{ip}
|
||||
</span>
|
||||
</div>
|
||||
@@ -140,18 +140,18 @@ export function HostCard({
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<StatusOrb status={service.status} size="sm" />
|
||||
<span className="font-mono text-sm text-nothing-gray-800">
|
||||
<span className="font-body text-sm text-nothing-gray-800">
|
||||
{service.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{service.port && (
|
||||
<span className="text-xs text-nothing-gray-400 font-mono">
|
||||
<span className="text-xs text-nothing-gray-400 font-body">
|
||||
:{service.port}
|
||||
</span>
|
||||
)}
|
||||
{service.latencyMs !== null && service.latencyMs !== undefined && (
|
||||
<span className="text-xs text-nothing-gray-500 font-mono">
|
||||
<span className="text-xs text-nothing-gray-500 font-body">
|
||||
{service.latencyMs.toFixed(1)}ms
|
||||
</span>
|
||||
)}
|
||||
@@ -168,7 +168,7 @@ export function HostCard({
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-xs text-nothing-gray-500">{tDashboard('cpu')}</span>
|
||||
<span className="text-xs font-mono text-nothing-gray-700">
|
||||
<span className="text-xs font-body text-nothing-gray-700">
|
||||
{formatPercent(metrics.cpuPercent)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -191,7 +191,7 @@ export function HostCard({
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-xs text-nothing-gray-500">{tDashboard('memory')}</span>
|
||||
<span className="text-xs font-mono text-nothing-gray-700">
|
||||
<span className="text-xs font-body text-nothing-gray-700">
|
||||
{formatPercent(metrics.memoryPercent)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -218,7 +218,7 @@ export function HostCard({
|
||||
<GlassCardFooter>
|
||||
<div className="flex items-center justify-between">
|
||||
<StatusBadge status={status} label={statusLabel} size="sm" />
|
||||
<span className="text-xs text-nothing-gray-400 font-mono">
|
||||
<span className="text-xs text-nothing-gray-400 font-body">
|
||||
{new Date().toLocaleTimeString('zh-TW', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
|
||||
@@ -225,12 +225,12 @@ function OpenClawPanel() {
|
||||
<h3 className="font-heading font-bold text-nothing-gray-900">
|
||||
{t('name')}
|
||||
</h3>
|
||||
<span className="text-[10px] text-status-thinking font-mono uppercase tracking-wider">
|
||||
<span className="text-[10px] text-status-thinking font-body uppercase tracking-wider">
|
||||
{t('monitoring')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-[10px] text-nothing-gray-400 font-mono bg-nothing-gray-100 px-2 py-1 rounded">v5.0.0</span>
|
||||
<span className="text-[10px] text-nothing-gray-400 font-body bg-nothing-gray-100 px-2 py-1 rounded">v5.0.0</span>
|
||||
</GlassCardHeader>
|
||||
|
||||
<GlassCardContent>
|
||||
@@ -243,7 +243,7 @@ function OpenClawPanel() {
|
||||
<div className="flex items-start gap-2 mb-2">
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-mono font-semibold',
|
||||
'inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-body font-semibold',
|
||||
warningHost
|
||||
? 'bg-status-warning/10 text-status-warning'
|
||||
: 'bg-status-healthy/10 text-status-healthy'
|
||||
@@ -365,7 +365,7 @@ function StatsPanel({
|
||||
</div>
|
||||
<span
|
||||
className={cn(
|
||||
'font-mono text-lg font-bold tabular-nums',
|
||||
'font-body text-lg font-bold tabular-nums',
|
||||
stat.color === 'healthy'
|
||||
? 'text-status-healthy'
|
||||
: stat.color === 'warning'
|
||||
|
||||
@@ -112,7 +112,7 @@ export function LiveHostCard({
|
||||
return (
|
||||
<GlassCard className={cn('h-full animate-pulse', className)}>
|
||||
<div className="h-40 flex items-center justify-center">
|
||||
<span className="text-nothing-gray-400 font-mono text-sm">
|
||||
<span className="text-nothing-gray-400 font-body text-sm">
|
||||
{tCommon('loading')} {ip}
|
||||
</span>
|
||||
</div>
|
||||
@@ -126,7 +126,7 @@ export function LiveHostCard({
|
||||
<GlassCardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<StatusOrb status="idle" size="md" />
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded-md bg-nothing-gray-100 text-nothing-gray-600 text-xs font-mono">
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded-md bg-nothing-gray-100 text-nothing-gray-600 text-xs font-body">
|
||||
{ip}
|
||||
</span>
|
||||
</div>
|
||||
@@ -137,7 +137,7 @@ export function LiveHostCard({
|
||||
{fallbackData.services.map((service) => (
|
||||
<div key={service.name} className="flex items-center gap-2 py-1.5 px-2">
|
||||
<StatusOrb status="idle" size="sm" />
|
||||
<span className="font-mono text-sm text-nothing-gray-500">
|
||||
<span className="font-body text-sm text-nothing-gray-500">
|
||||
{service.name}
|
||||
</span>
|
||||
</div>
|
||||
@@ -197,7 +197,7 @@ export function LiveHostCard({
|
||||
{getRoleIcon(host.role || '')}
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-nothing-gray-50 border border-nothing-gray-100 text-nothing-gray-700 text-xs font-mono">
|
||||
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-nothing-gray-50 border border-nothing-gray-100 text-nothing-gray-700 text-xs font-body">
|
||||
<Wifi className="w-3 h-3" />
|
||||
{host.ip}
|
||||
</span>
|
||||
@@ -210,7 +210,7 @@ export function LiveHostCard({
|
||||
<div className="mb-4">
|
||||
<GlassCardTitle className="text-lg">{host.name}</GlassCardTitle>
|
||||
{host.role && (
|
||||
<span className="text-[10px] text-nothing-gray-400 font-mono uppercase tracking-widest">
|
||||
<span className="text-[10px] text-nothing-gray-400 font-body uppercase tracking-widest">
|
||||
{host.role.replace('_', ' ')}
|
||||
</span>
|
||||
)}
|
||||
@@ -226,20 +226,20 @@ export function LiveHostCard({
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<StatusOrb status={mapServiceStatus(service.status)} size="sm" />
|
||||
<span className="font-mono text-sm text-nothing-gray-800">
|
||||
<span className="font-body text-sm text-nothing-gray-800">
|
||||
{service.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{service.port && (
|
||||
<span className="text-xs text-nothing-gray-400 font-mono">
|
||||
<span className="text-xs text-nothing-gray-400 font-body">
|
||||
:{service.port}
|
||||
</span>
|
||||
)}
|
||||
{service.latency_ms !== null && service.latency_ms !== undefined && (
|
||||
<span
|
||||
className={cn(
|
||||
'text-xs font-mono',
|
||||
'text-xs font-body',
|
||||
service.latency_ms > 100
|
||||
? 'text-status-warning'
|
||||
: 'text-nothing-gray-500'
|
||||
@@ -287,7 +287,7 @@ export function LiveHostCard({
|
||||
<GlassCardFooter>
|
||||
<div className="flex items-center justify-between">
|
||||
<StatusBadge status={overallStatus} label={statusLabel} size="sm" />
|
||||
<span className="text-xs text-nothing-gray-400 font-mono">
|
||||
<span className="text-xs text-nothing-gray-400 font-body">
|
||||
{new Date(host.last_check).toLocaleTimeString('zh-TW', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
@@ -330,8 +330,8 @@ function MetricBar({
|
||||
return (
|
||||
<div className="p-3 rounded-xl bg-nothing-gray-50/50 border border-nothing-gray-100">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-[10px] text-nothing-gray-500 uppercase tracking-wider font-mono">{label}</span>
|
||||
<span className="text-xs font-mono text-nothing-gray-400 animate-pulse">
|
||||
<span className="text-[10px] text-nothing-gray-500 uppercase tracking-wider font-body">{label}</span>
|
||||
<span className="text-xs font-body text-nothing-gray-400 animate-pulse">
|
||||
{collectingLabel}
|
||||
</span>
|
||||
</div>
|
||||
@@ -372,12 +372,12 @@ function MetricBar({
|
||||
) : (
|
||||
<Gauge className="w-3 h-3 text-nothing-gray-400" />
|
||||
)}
|
||||
<span className="text-[10px] text-nothing-gray-500 uppercase tracking-wider font-mono">{label}</span>
|
||||
<span className="text-[10px] text-nothing-gray-500 uppercase tracking-wider font-body">{label}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span
|
||||
className={cn(
|
||||
'text-sm font-mono font-bold tabular-nums',
|
||||
'text-sm font-body font-bold tabular-nums',
|
||||
value >= criticalThreshold
|
||||
? 'text-status-critical'
|
||||
: value >= warningThreshold
|
||||
@@ -389,7 +389,7 @@ function MetricBar({
|
||||
</span>
|
||||
{/* Dynamic Baseline Display */}
|
||||
{baselineValue !== null && baselineValue !== undefined && (
|
||||
<span className="text-[9px] font-mono text-nothing-gray-300">
|
||||
<span className="text-[9px] font-body text-nothing-gray-300">
|
||||
({formatPercent(baselineValue)})
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -84,7 +84,7 @@ export const ApprovalCard: React.FC<ApprovalCardProps> = ({ data }) => {
|
||||
<span className="hidden sm:inline">Approval Required // </span>{data.approvalId}
|
||||
</span>
|
||||
</div>
|
||||
<div className={`px-2 py-0.5 sm:py-1 text-xs font-mono font-bold border-2 self-start sm:self-auto shrink-0 ${
|
||||
<div className={`px-2 py-0.5 sm:py-1 text-xs font-body font-bold border-2 self-start sm:self-auto shrink-0 ${
|
||||
isCritical ? 'border-red-500 text-red-500' : 'border-blue-500 text-blue-500'
|
||||
}`}>
|
||||
{data.riskLevel}
|
||||
@@ -94,7 +94,7 @@ export const ApprovalCard: React.FC<ApprovalCardProps> = ({ data }) => {
|
||||
<div className="p-3 sm:p-4 space-y-3 sm:space-y-4">
|
||||
{/* Metadata */}
|
||||
{(data.requestedBy || data.requestedAt) && (
|
||||
<div className="flex flex-wrap gap-2 sm:gap-4 text-xs font-mono text-gray-500">
|
||||
<div className="flex flex-wrap gap-2 sm:gap-4 text-xs font-body text-gray-500">
|
||||
{data.requestedBy && (
|
||||
<div className="flex items-center gap-1">
|
||||
<User size={12} />
|
||||
@@ -132,13 +132,13 @@ export const ApprovalCard: React.FC<ApprovalCardProps> = ({ data }) => {
|
||||
{/* Nuclear Key Authorization */}
|
||||
<div className="pt-1 sm:pt-2">
|
||||
{executionResult === 'success' ? (
|
||||
<div className="w-full py-3 sm:py-4 bg-green-50 text-green-700 border-2 border-green-500 font-mono font-bold text-xs sm:text-sm uppercase flex justify-center items-center gap-2 rounded-sm">
|
||||
<div className="w-full py-3 sm:py-4 bg-green-50 text-green-700 border-2 border-green-500 font-body font-bold text-xs sm:text-sm uppercase flex justify-center items-center gap-2 rounded-sm">
|
||||
<span className="text-lg sm:text-xl">✓</span>
|
||||
<span className="hidden sm:inline">{t('executionAuthorized')}</span>
|
||||
<span className="sm:hidden">{t('authorized')}</span>
|
||||
</div>
|
||||
) : executionResult === 'error' ? (
|
||||
<div className="w-full py-3 sm:py-4 bg-red-50 text-red-700 border-2 border-red-500 font-mono font-bold text-xs sm:text-sm uppercase flex justify-center items-center gap-2 rounded-sm">
|
||||
<div className="w-full py-3 sm:py-4 bg-red-50 text-red-700 border-2 border-red-500 font-body font-bold text-xs sm:text-sm uppercase flex justify-center items-center gap-2 rounded-sm">
|
||||
<span className="text-lg sm:text-xl">✗</span>
|
||||
{t('executionFailed')}
|
||||
</div>
|
||||
@@ -158,7 +158,7 @@ export const ApprovalCard: React.FC<ApprovalCardProps> = ({ data }) => {
|
||||
}
|
||||
|
||||
const Label = ({ children }: { children: React.ReactNode }) => (
|
||||
<span className="text-xs uppercase font-mono font-bold text-gray-500 tracking-wider">
|
||||
<span className="text-xs uppercase font-body font-bold text-gray-500 tracking-wider">
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
|
||||
@@ -127,7 +127,7 @@ const LoadingCard: React.FC<{ componentName: string }> = ({ componentName }) =>
|
||||
<div className="w-full max-w-2xl bg-white border-2 border-gray-200 shadow-lg rounded-sm overflow-hidden mt-4 mb-2 animate-pulse">
|
||||
<div className="p-4 flex items-center gap-2">
|
||||
<Loader2 className="animate-spin text-gray-400" size={20} />
|
||||
<span className="font-mono text-sm text-gray-500">
|
||||
<span className="font-body text-sm text-gray-500">
|
||||
Loading {componentName}...
|
||||
</span>
|
||||
</div>
|
||||
@@ -139,8 +139,8 @@ const ErrorCard: React.FC<{ title: string; message: string }> = ({ title, messag
|
||||
<div className="p-4 flex items-start gap-3">
|
||||
<AlertTriangle className="text-red-500 shrink-0" size={20} />
|
||||
<div>
|
||||
<div className="font-mono font-bold text-red-700">{title}</div>
|
||||
<div className="font-mono text-sm text-red-600 mt-1">{message}</div>
|
||||
<div className="font-body font-bold text-red-700">{title}</div>
|
||||
<div className="font-body text-sm text-red-600 mt-1">{message}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -53,11 +53,11 @@ export const IncidentTimelineCard: React.FC<IncidentTimelineCardProps> = ({ data
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{data.severity && (
|
||||
<span className="px-2 py-1 text-xs font-mono font-bold border-2 border-nothing-black">
|
||||
<span className="px-2 py-1 text-xs font-body font-bold border-2 border-nothing-black">
|
||||
{data.severity}
|
||||
</span>
|
||||
)}
|
||||
<span className={`px-2 py-1 text-xs font-mono font-bold border-2 ${statusColors[data.status]} uppercase`}>
|
||||
<span className={`px-2 py-1 text-xs font-body font-bold border-2 ${statusColors[data.status]} uppercase`}>
|
||||
{data.status}
|
||||
</span>
|
||||
</div>
|
||||
@@ -67,18 +67,18 @@ export const IncidentTimelineCard: React.FC<IncidentTimelineCardProps> = ({ data
|
||||
<div className="space-y-3">
|
||||
{data.events.map((event, i) => (
|
||||
<div key={i} className="flex gap-3 text-sm">
|
||||
<div className="text-xs font-mono text-gray-400 w-20 shrink-0">
|
||||
<div className="text-xs font-body text-gray-400 w-20 shrink-0">
|
||||
{event.timestamp}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<span className={`text-xs font-mono uppercase px-1 ${
|
||||
<span className={`text-xs font-body uppercase px-1 ${
|
||||
event.type === 'resolved' ? 'bg-green-100 text-green-700' :
|
||||
event.type === 'escalated' ? 'bg-red-100 text-red-700' :
|
||||
'bg-gray-100 text-gray-700'
|
||||
}`}>
|
||||
{event.type}
|
||||
</span>
|
||||
<span className="ml-2 font-mono text-nothing-black">{event.message}</span>
|
||||
<span className="ml-2 font-body text-nothing-black">{event.message}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -77,7 +77,7 @@ export const K8sPodStatusCard: React.FC<K8sPodStatusCardProps> = ({ data }) => {
|
||||
K8s Pods // {data.namespace}
|
||||
</span>
|
||||
</div>
|
||||
<div className={`px-2 py-1 text-xs font-mono font-bold border-2 ${isHealthy ? 'border-green-500 text-green-600' : 'border-yellow-500 text-yellow-600'}`}>
|
||||
<div className={`px-2 py-1 text-xs font-body font-bold border-2 ${isHealthy ? 'border-green-500 text-green-600' : 'border-yellow-500 text-yellow-600'}`}>
|
||||
{summary.running}/{summary.total} RUNNING
|
||||
</div>
|
||||
</div>
|
||||
@@ -87,7 +87,7 @@ export const K8sPodStatusCard: React.FC<K8sPodStatusCardProps> = ({ data }) => {
|
||||
{data.pods.map((pod, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-center gap-3 p-2 bg-gray-50 rounded-sm text-sm font-mono"
|
||||
className="flex items-center gap-3 p-2 bg-gray-50 rounded-sm text-sm font-body"
|
||||
>
|
||||
{statusIcon(pod.status)}
|
||||
<span className="flex-1 truncate text-nothing-black">{pod.name}</span>
|
||||
|
||||
@@ -55,7 +55,7 @@ export const MetricsSummaryCard: React.FC<MetricsSummaryCardProps> = ({ data })
|
||||
SignOz Metrics // Real-time
|
||||
</span>
|
||||
</div>
|
||||
<div className={`px-2 py-1 text-xs font-mono font-bold border-2 ${statusTextColors[status]} border-current uppercase`}>
|
||||
<div className={`px-2 py-1 text-xs font-body font-bold border-2 ${statusTextColors[status]} border-current uppercase`}>
|
||||
{status}
|
||||
</div>
|
||||
</div>
|
||||
@@ -93,26 +93,26 @@ export const MetricsSummaryCard: React.FC<MetricsSummaryCardProps> = ({ data })
|
||||
<div className="p-4 pt-0 grid grid-cols-2 gap-4 bg-white/80">
|
||||
{data.cpu !== undefined && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-mono text-gray-500">CPU:</span>
|
||||
<span className="text-xs font-body text-gray-500">CPU:</span>
|
||||
<div className="flex-1 h-2 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full ${data.cpu > 80 ? 'bg-red-500' : data.cpu > 60 ? 'bg-yellow-500' : 'bg-green-500'}`}
|
||||
style={{ width: `${data.cpu}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs font-mono font-bold">{data.cpu}%</span>
|
||||
<span className="text-xs font-body font-bold">{data.cpu}%</span>
|
||||
</div>
|
||||
)}
|
||||
{data.memory !== undefined && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-mono text-gray-500">MEM:</span>
|
||||
<span className="text-xs font-body text-gray-500">MEM:</span>
|
||||
<div className="flex-1 h-2 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full ${data.memory > 80 ? 'bg-red-500' : data.memory > 60 ? 'bg-yellow-500' : 'bg-green-500'}`}
|
||||
style={{ width: `${data.memory}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs font-mono font-bold">{data.memory}%</span>
|
||||
<span className="text-xs font-body font-bold">{data.memory}%</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -121,7 +121,7 @@ export const MetricsSummaryCard: React.FC<MetricsSummaryCardProps> = ({ data })
|
||||
{/* Pods status */}
|
||||
{data.pods && (
|
||||
<div className="px-4 pb-4 bg-white/80">
|
||||
<div className="text-xs font-mono text-gray-500">
|
||||
<div className="text-xs font-body text-gray-500">
|
||||
Pods: <span className="font-bold text-nothing-black">{data.pods.running}/{data.pods.total}</span> running
|
||||
</div>
|
||||
</div>
|
||||
@@ -141,7 +141,7 @@ const MetricBox: React.FC<{
|
||||
<div className={`text-center p-3 rounded-sm border ${highlight ? 'border-red-300 bg-red-50' : 'border-gray-200 bg-gray-50'}`}>
|
||||
<div className="flex items-center justify-center gap-1 text-gray-500 mb-1">
|
||||
{icon}
|
||||
<span className="text-xs font-mono uppercase">{label}</span>
|
||||
<span className="text-xs font-body uppercase">{label}</span>
|
||||
</div>
|
||||
<div className={`font-['VT323'] text-2xl ${highlight ? 'text-red-600' : 'text-nothing-black'}`}>
|
||||
{value}
|
||||
|
||||
@@ -167,7 +167,7 @@ export const NuclearKeyButton: React.FC<NuclearKeyButtonProps> = ({
|
||||
w-full py-4 px-6 rounded-sm border-2
|
||||
bg-green-50 border-green-500
|
||||
flex items-center justify-center gap-2
|
||||
font-mono font-bold text-green-700
|
||||
font-body font-bold text-green-700
|
||||
transition-all duration-300
|
||||
`}>
|
||||
<CheckCircle size={20} />
|
||||
@@ -218,7 +218,7 @@ export const NuclearKeyButton: React.FC<NuclearKeyButtonProps> = ({
|
||||
className={`${config.textColor} ${isHolding ? 'animate-pulse' : ''} sm:w-5 sm:h-5`}
|
||||
/>
|
||||
|
||||
<span className={`font-mono font-bold uppercase tracking-wider text-sm sm:text-base ${config.textColor}`}>
|
||||
<span className={`font-body font-bold uppercase tracking-wider text-sm sm:text-base ${config.textColor}`}>
|
||||
{isHolding ? (
|
||||
<span className="tabular-nums">
|
||||
{label} ({Math.ceil((1 - progress) * (duration ?? 1500) / 1000)}s)
|
||||
@@ -230,7 +230,7 @@ export const NuclearKeyButton: React.FC<NuclearKeyButtonProps> = ({
|
||||
|
||||
{/* Risk Badge */}
|
||||
<span className={`
|
||||
px-1.5 sm:px-2 py-0.5 text-[10px] sm:text-xs font-mono font-bold
|
||||
px-1.5 sm:px-2 py-0.5 text-[10px] sm:text-xs font-body font-bold
|
||||
border ${config.borderColor} ${config.textColor}
|
||||
rounded-sm
|
||||
`}>
|
||||
@@ -241,7 +241,7 @@ export const NuclearKeyButton: React.FC<NuclearKeyButtonProps> = ({
|
||||
|
||||
{/* Shortcut Hint - Hidden on mobile touch devices */}
|
||||
{showShortcut && !disabled && (
|
||||
<div className="text-center text-xs font-mono text-gray-400">
|
||||
<div className="text-center text-xs font-body text-gray-400">
|
||||
{isHolding ? (
|
||||
<span className="text-gray-600">
|
||||
{t('keepHolding')}
|
||||
@@ -271,7 +271,7 @@ export const NuclearKeyButton: React.FC<NuclearKeyButtonProps> = ({
|
||||
{/* Critical Warning */}
|
||||
{isCritical && !disabled && !isHolding && (
|
||||
<div className={`
|
||||
text-center text-xs font-mono ${config.textColor}
|
||||
text-center text-xs font-body ${config.textColor}
|
||||
animate-pulse
|
||||
`}>
|
||||
<AlertTriangle size={12} className="inline mr-1" />
|
||||
|
||||
@@ -41,14 +41,14 @@ export const SentryErrorCard: React.FC<SentryErrorCardProps> = ({ data }) => {
|
||||
Sentry // {data.errorId}
|
||||
</span>
|
||||
</div>
|
||||
<div className={`px-2 py-1 text-xs font-mono font-bold border-2 ${levelColors[level]} uppercase`}>
|
||||
<div className={`px-2 py-1 text-xs font-body font-bold border-2 ${levelColors[level]} uppercase`}>
|
||||
{level}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 space-y-3">
|
||||
<div>
|
||||
<span className="text-xs uppercase font-mono font-bold text-gray-500 tracking-wider">
|
||||
<span className="text-xs uppercase font-body font-bold text-gray-500 tracking-wider">
|
||||
Error
|
||||
</span>
|
||||
<div className="mt-1 font-['VT323'] text-xl text-nothing-black">
|
||||
@@ -56,7 +56,7 @@ export const SentryErrorCard: React.FC<SentryErrorCardProps> = ({ data }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 text-sm font-mono">
|
||||
<div className="flex gap-4 text-sm font-body">
|
||||
{data.count !== undefined && (
|
||||
<div>
|
||||
<span className="text-gray-500">Count:</span>{' '}
|
||||
@@ -76,7 +76,7 @@ export const SentryErrorCard: React.FC<SentryErrorCardProps> = ({ data }) => {
|
||||
href={data.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 text-sm font-mono text-[#4A90D9] hover:underline"
|
||||
className="inline-flex items-center gap-1 text-sm font-body text-[#4A90D9] hover:underline"
|
||||
>
|
||||
View in Sentry <ExternalLink size={14} />
|
||||
</a>
|
||||
|
||||
@@ -49,7 +49,7 @@ export const TraceWaterfallCard: React.FC<TraceWaterfallCardProps> = ({ data })
|
||||
Trace // {data.traceId.slice(0, 16)}...
|
||||
</span>
|
||||
</div>
|
||||
<div className="px-2 py-1 text-xs font-mono font-bold border-2 border-[#4A90D9] text-[#4A90D9]">
|
||||
<div className="px-2 py-1 text-xs font-body font-bold border-2 border-[#4A90D9] text-[#4A90D9]">
|
||||
{data.duration}ms
|
||||
</div>
|
||||
</div>
|
||||
@@ -58,7 +58,7 @@ export const TraceWaterfallCard: React.FC<TraceWaterfallCardProps> = ({ data })
|
||||
{data.spans.map((span, i) => (
|
||||
<div key={i} className="flex items-center gap-2 text-sm">
|
||||
{/* Service name */}
|
||||
<div className="w-24 shrink-0 truncate text-xs font-mono text-gray-500">
|
||||
<div className="w-24 shrink-0 truncate text-xs font-body text-gray-500">
|
||||
{span.service}
|
||||
</div>
|
||||
|
||||
@@ -74,7 +74,7 @@ export const TraceWaterfallCard: React.FC<TraceWaterfallCardProps> = ({ data })
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
className="absolute text-xs font-mono text-white px-1 truncate"
|
||||
className="absolute text-xs font-body text-white px-1 truncate"
|
||||
style={{
|
||||
left: `${getBarOffset(span.startOffset)}%`,
|
||||
maxWidth: `${getBarWidth(span.duration)}%`,
|
||||
@@ -86,7 +86,7 @@ export const TraceWaterfallCard: React.FC<TraceWaterfallCardProps> = ({ data })
|
||||
</div>
|
||||
|
||||
{/* Duration */}
|
||||
<div className="w-16 shrink-0 text-right text-xs font-mono text-gray-600">
|
||||
<div className="w-16 shrink-0 text-right text-xs font-body text-gray-600">
|
||||
{span.duration}ms
|
||||
</div>
|
||||
</div>
|
||||
@@ -99,7 +99,7 @@ export const TraceWaterfallCard: React.FC<TraceWaterfallCardProps> = ({ data })
|
||||
href={data.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 text-sm font-mono text-[#4A90D9] hover:underline"
|
||||
className="inline-flex items-center gap-1 text-sm font-body text-[#4A90D9] hover:underline"
|
||||
>
|
||||
View in SignOz <ExternalLink size={14} />
|
||||
</a>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
*
|
||||
* Nothing.tech 視覺規範:
|
||||
* - 深色背景 (bg-black text-white) - 全站唯一深色區域
|
||||
* - 強制等寬字體 (font-mono)
|
||||
* - 強制等寬字體 (font-body)
|
||||
* - 打字機效果展示推論步驟
|
||||
*
|
||||
* 統帥鐵律: 禁止假數據!
|
||||
@@ -168,7 +168,7 @@ export function ThinkingTerminal({
|
||||
{/* Title */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Terminal className="w-4 h-4 text-gray-400" />
|
||||
<span className="font-mono text-xs text-gray-400">
|
||||
<span className="font-body text-xs text-gray-400">
|
||||
OpenClaw Terminal
|
||||
</span>
|
||||
</div>
|
||||
@@ -177,7 +177,7 @@ export function ThinkingTerminal({
|
||||
{/* Controls */}
|
||||
<div className="flex items-center gap-2">
|
||||
{incidentId && (
|
||||
<span className="font-mono text-xs text-gray-600">
|
||||
<span className="font-body text-xs text-gray-600">
|
||||
{incidentId}
|
||||
</span>
|
||||
)}
|
||||
@@ -214,7 +214,7 @@ export function ThinkingTerminal({
|
||||
{isExpanded && (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="p-4 overflow-y-auto font-mono text-sm"
|
||||
className="p-4 overflow-y-auto font-body text-sm"
|
||||
style={{ maxHeight, minHeight: '150px' }}
|
||||
>
|
||||
{/* No Data State */}
|
||||
@@ -291,10 +291,10 @@ export function ThinkingTerminal({
|
||||
|
||||
{/* Terminal Footer */}
|
||||
<div className="flex items-center justify-between px-4 py-2 border-t border-gray-800 bg-gray-900/50">
|
||||
<span className="font-mono text-xs text-gray-600">
|
||||
<span className="font-body text-xs text-gray-600">
|
||||
{t('steps')}: {displayedSteps}/{totalSteps}
|
||||
</span>
|
||||
<span className="font-mono text-xs text-gray-600">
|
||||
<span className="font-body text-xs text-gray-600">
|
||||
{isPlaying ? t('streaming') : t('paused')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -30,11 +30,11 @@ export function ComingSoon({ icon: Icon, title, description, className }: Coming
|
||||
<Icon className="w-8 h-8 text-nothing-gray-400" />
|
||||
</div>
|
||||
)}
|
||||
<h2 className="font-mono text-lg font-bold text-nothing-gray-700 tracking-tight mb-2">
|
||||
<h2 className="font-body text-lg font-bold text-nothing-gray-700 tracking-tight mb-2">
|
||||
{title}
|
||||
</h2>
|
||||
{description && (
|
||||
<p className="font-mono text-sm text-nothing-gray-400 max-w-xs">
|
||||
<p className="font-body text-sm text-nothing-gray-400 max-w-xs">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -95,7 +95,7 @@ export function Header({
|
||||
{pendingApprovals > 0 && (
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 rounded-full bg-status-critical/10 border border-status-critical/30">
|
||||
<StatusOrb status="critical" size="sm" pulse />
|
||||
<span className="text-sm font-mono text-status-critical">
|
||||
<span className="text-sm font-body text-status-critical">
|
||||
{pendingApprovals} {tDashboard('pendingApprovals')}
|
||||
</span>
|
||||
</div>
|
||||
@@ -105,13 +105,13 @@ export function Header({
|
||||
<div className="flex items-center gap-2">
|
||||
<StatusOrb status={connectionOrbStatus} size="md" glow />
|
||||
<span className={cn(
|
||||
'font-mono text-sm',
|
||||
'font-body text-sm',
|
||||
connectionStatus === 'connected' ? 'text-status-healthy' : 'text-nothing-gray-500'
|
||||
)}>
|
||||
{connectionLabel}
|
||||
</span>
|
||||
{mockMode && (
|
||||
<span className="px-2 py-0.5 text-xs font-mono rounded bg-status-warning/10 text-status-warning border border-status-warning/30">
|
||||
<span className="px-2 py-0.5 text-xs font-body rounded bg-status-warning/10 text-status-warning border border-status-warning/30">
|
||||
MOCK
|
||||
</span>
|
||||
)}
|
||||
@@ -122,7 +122,7 @@ export function Header({
|
||||
<button
|
||||
onClick={() => switchLocale('zh-TW')}
|
||||
className={cn(
|
||||
'px-3 py-1.5 rounded-md font-mono text-sm transition-all',
|
||||
'px-3 py-1.5 rounded-md font-body text-sm transition-all',
|
||||
locale === 'zh-TW'
|
||||
? 'bg-nothing-black text-white'
|
||||
: 'text-nothing-gray-600 hover:text-nothing-black'
|
||||
@@ -133,7 +133,7 @@ export function Header({
|
||||
<button
|
||||
onClick={() => switchLocale('en')}
|
||||
className={cn(
|
||||
'px-3 py-1.5 rounded-md font-mono text-sm transition-all',
|
||||
'px-3 py-1.5 rounded-md font-body text-sm transition-all',
|
||||
locale === 'en'
|
||||
? 'bg-nothing-black text-white'
|
||||
: 'text-nothing-gray-600 hover:text-nothing-black'
|
||||
|
||||
@@ -323,7 +323,7 @@ export function Sidebar({
|
||||
collapsed ? 'px-2 text-center' : 'px-4'
|
||||
)}>
|
||||
{!collapsed && (
|
||||
<span className="text-[10px] font-mono text-neutral-300 uppercase tracking-widest">
|
||||
<span className="text-[10px] font-body text-muted uppercase tracking-widest">
|
||||
v1.0.0
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -90,12 +90,12 @@ class AutoHealingErrorBoundaryInner extends Component<InnerProps, State> {
|
||||
if (this.state.retryCount >= 3) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center p-8 bg-[#111111] text-white border border-red-500/30 rounded-lg max-w-lg mx-auto mt-12">
|
||||
<h2 className="text-xl font-mono text-red-500 mb-4">{translations.systemFailure}</h2>
|
||||
<p className="font-mono text-sm text-gray-400 mb-4">{this.props.fallbackMessage || translations.criticalError}</p>
|
||||
<p className="font-mono text-xs text-purple-400 mb-6">{translations.escalating}</p>
|
||||
<h2 className="text-xl font-body text-red-500 mb-4">{translations.systemFailure}</h2>
|
||||
<p className="font-body text-sm text-gray-400 mb-4">{this.props.fallbackMessage || translations.criticalError}</p>
|
||||
<p className="font-body text-xs text-purple-400 mb-6">{translations.escalating}</p>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="px-6 py-2 bg-white text-black font-mono text-sm rounded-full hover:bg-gray-200 transition-colors"
|
||||
className="px-6 py-2 bg-white text-black font-body text-sm rounded-full hover:bg-gray-200 transition-colors"
|
||||
>
|
||||
{translations.forceRestart}
|
||||
</button>
|
||||
@@ -105,8 +105,8 @@ class AutoHealingErrorBoundaryInner extends Component<InnerProps, State> {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center p-12 space-y-4 animate-pulse">
|
||||
<div className="text-red-500 font-mono text-sm font-bold tracking-widest">{translations.detectingAnomaly}</div>
|
||||
<div className="text-gray-400 font-mono text-xs">{translations.autoHealingAttempt(this.state.retryCount + 1)}</div>
|
||||
<div className="text-red-500 font-body text-sm font-bold tracking-widest">{translations.detectingAnomaly}</div>
|
||||
<div className="text-gray-400 font-body text-xs">{translations.autoHealingAttempt(this.state.retryCount + 1)}</div>
|
||||
<div className="w-48 h-1 bg-gray-800 rounded overflow-hidden mt-4">
|
||||
<div className="h-full bg-white opacity-50 animate-ping"></div>
|
||||
</div>
|
||||
|
||||
@@ -41,7 +41,7 @@ export function LanguageSwitcher({ className }: LanguageSwitcherProps) {
|
||||
key={loc}
|
||||
onClick={() => handleSwitch(loc)}
|
||||
className={cn(
|
||||
'px-3 py-1.5 rounded-sm font-mono text-xs transition-all duration-200',
|
||||
'px-3 py-1.5 rounded-sm font-body text-xs transition-all duration-200',
|
||||
'focus:outline-none focus:ring-2 focus:ring-nothing-gray-400',
|
||||
isActive
|
||||
? 'bg-nothing-gray-900 text-nothing-white'
|
||||
@@ -81,7 +81,7 @@ export function LanguageSwitcherCompact({ className }: LanguageSwitcherProps) {
|
||||
className={cn(
|
||||
'px-3 py-1.5 rounded-button',
|
||||
'bg-nothing-gray-100 border border-nothing-gray-200',
|
||||
'font-mono text-xs text-nothing-gray-600',
|
||||
'font-body text-xs text-nothing-gray-600',
|
||||
'hover:text-nothing-gray-900 hover:bg-nothing-gray-200',
|
||||
'transition-all duration-200',
|
||||
'focus:outline-none focus:ring-2 focus:ring-nothing-gray-400',
|
||||
|
||||
@@ -83,7 +83,7 @@ export function StatusOrb({ status, size = 'md', className }: StatusOrbProps) {
|
||||
|
||||
{/* Label */}
|
||||
<div className="absolute -bottom-6 left-1/2 -translate-x-1/2 whitespace-nowrap">
|
||||
<span className="font-mono text-xs text-nothing-gray-500">
|
||||
<span className="font-body text-xs text-nothing-gray-500">
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -141,7 +141,7 @@ export const OmniTerminal = () => {
|
||||
const result = msg.payload.result as Record<string, unknown> | undefined
|
||||
|
||||
return (
|
||||
<div className={`font-mono text-sm ${status === 'completed' ? 'text-green-600' : status === 'failed' ? 'text-red-600' : 'text-yellow-600'}`}>
|
||||
<div className={`font-body text-sm ${status === 'completed' ? 'text-green-600' : status === 'failed' ? 'text-red-600' : 'text-yellow-600'}`}>
|
||||
<span className="opacity-70">[{tool ?? 'unknown'}]</span>{' '}
|
||||
<span className="font-bold">{status ?? 'executing'}</span>
|
||||
{result && (
|
||||
@@ -205,12 +205,12 @@ export const OmniTerminal = () => {
|
||||
</span>
|
||||
<div className="flex items-center gap-1 opacity-60">
|
||||
<span className={`w-2 h-2 rounded-full ${isConnected ? 'bg-green-500' : 'bg-gray-400'}`}></span>
|
||||
<span className="text-xs uppercase font-mono hidden sm:inline">{isConnected ? t('sseLive') : t('offline')}</span>
|
||||
<span className="text-xs uppercase font-body hidden sm:inline">{isConnected ? t('sseLive') : t('offline')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={toggleTerminal}
|
||||
className="text-gray-500 hover:text-black font-mono px-2 py-1"
|
||||
className="text-gray-500 hover:text-black font-body px-2 py-1"
|
||||
aria-label="Close terminal"
|
||||
>
|
||||
[x]
|
||||
|
||||
@@ -128,7 +128,7 @@ export function ThinkingStreamTest() {
|
||||
{/* Header - Nothing.tech 風格 */}
|
||||
<div className="flex items-center justify-between border-b border-nothing-gray-800 pb-4">
|
||||
<h2 className="font-display text-display-sm">AWOOOI Terminal</h2>
|
||||
<span className="font-mono text-xs text-nothing-gray-500">
|
||||
<span className="font-body text-xs text-nothing-gray-500">
|
||||
v0.1.0 | SSE
|
||||
</span>
|
||||
</div>
|
||||
@@ -138,7 +138,7 @@ export function ThinkingStreamTest() {
|
||||
onClick={startStream}
|
||||
disabled={isStreaming}
|
||||
className={cn(
|
||||
'w-full py-3 px-4 rounded-button font-mono text-sm transition-all border',
|
||||
'w-full py-3 px-4 rounded-button font-body text-sm transition-all border',
|
||||
isStreaming
|
||||
? 'bg-nothing-gray-800 text-nothing-gray-500 border-nothing-gray-700 cursor-not-allowed'
|
||||
: 'bg-nothing-white text-nothing-black border-transparent hover:bg-nothing-gray-200'
|
||||
@@ -150,7 +150,7 @@ export function ThinkingStreamTest() {
|
||||
{/* Terminal Output */}
|
||||
<div className="bg-nothing-gray-900 rounded-card p-4 min-h-[200px] max-h-[300px] overflow-y-auto">
|
||||
{thinkingStream.length === 0 && !isStreaming && (
|
||||
<div className="font-mono text-sm text-nothing-gray-600">
|
||||
<div className="font-body text-sm text-nothing-gray-600">
|
||||
{'>'} Waiting for command...
|
||||
</div>
|
||||
)}
|
||||
@@ -159,7 +159,7 @@ export function ThinkingStreamTest() {
|
||||
<div
|
||||
key={index}
|
||||
className={cn(
|
||||
'font-mono text-sm py-0.5 animate-fade-in',
|
||||
'font-body text-sm py-0.5 animate-fade-in',
|
||||
step.type === 'result' && 'text-status-healthy',
|
||||
step.type === 'thinking' && 'text-nothing-gray-500',
|
||||
step.type === 'error' && 'text-status-critical'
|
||||
@@ -172,7 +172,7 @@ export function ThinkingStreamTest() {
|
||||
{/* Cursor 動畫 */}
|
||||
{isStreaming && (
|
||||
<div className="flex items-center gap-1 mt-1">
|
||||
<span className="font-mono text-sm text-nothing-gray-500">{'>'}</span>
|
||||
<span className="font-body text-sm text-nothing-gray-500">{'>'}</span>
|
||||
<span className="w-2 h-4 bg-nothing-white animate-pulse" />
|
||||
</div>
|
||||
)}
|
||||
@@ -180,7 +180,7 @@ export function ThinkingStreamTest() {
|
||||
|
||||
{/* Connection Info */}
|
||||
<div className="pt-2 border-t border-nothing-gray-800">
|
||||
<p className="font-mono text-xs text-nothing-gray-600">
|
||||
<p className="font-body text-xs text-nothing-gray-600">
|
||||
ENDPOINT: {API_BASE_URL}/agent/thinking
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -135,7 +135,7 @@ function TimelineItem({ event, isLast, isNewest = false, isPulsingExec = false }
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<p className="font-mono text-[11px] text-nothing-gray-700 leading-tight">
|
||||
<p className="font-body text-[11px] text-nothing-gray-700 leading-tight">
|
||||
{event.title}
|
||||
</p>
|
||||
|
||||
@@ -143,7 +143,7 @@ function TimelineItem({ event, isLast, isNewest = false, isPulsingExec = false }
|
||||
{event.status === 'error' && event.description && (
|
||||
<div className="mt-1.5 flex items-start gap-1.5 p-2 rounded bg-nothing-red/5 border border-nothing-red/20">
|
||||
<span className="font-dot-matrix text-nothing-red text-[14px] leading-none animate-pulse">!</span>
|
||||
<pre className="font-mono text-[9px] text-nothing-red/90 leading-relaxed whitespace-pre-wrap break-all">
|
||||
<pre className="font-body text-[9px] text-nothing-red/90 leading-relaxed whitespace-pre-wrap break-all">
|
||||
{event.description}
|
||||
</pre>
|
||||
</div>
|
||||
@@ -153,12 +153,12 @@ function TimelineItem({ event, isLast, isNewest = false, isPulsingExec = false }
|
||||
{event.actor && (
|
||||
<div className="flex items-center gap-1.5 mt-0.5">
|
||||
<Icon className={cn('w-2.5 h-2.5', config.labelColor)} />
|
||||
<span className="font-mono text-[9px] text-nothing-gray-500">
|
||||
<span className="font-body text-[9px] text-nothing-gray-500">
|
||||
{event.actor}
|
||||
</span>
|
||||
{event.actorRole && (
|
||||
<span className={cn(
|
||||
'px-1 py-0.5 rounded text-[7px] font-mono font-bold uppercase',
|
||||
'px-1 py-0.5 rounded text-[7px] font-body font-bold uppercase',
|
||||
event.actorRole === 'cto' || event.actorRole === 'ciso'
|
||||
? 'bg-status-healthy/10 text-status-healthy'
|
||||
: event.actorRole === 'ai'
|
||||
@@ -173,7 +173,7 @@ function TimelineItem({ event, isLast, isNewest = false, isPulsingExec = false }
|
||||
|
||||
{/* Risk Badge */}
|
||||
{event.riskLevel === 'critical' && (
|
||||
<span className="inline-block mt-1 px-1 py-0.5 rounded text-[7px] font-mono font-bold uppercase bg-status-critical/10 text-status-critical">
|
||||
<span className="inline-block mt-1 px-1 py-0.5 rounded text-[7px] font-body font-bold uppercase bg-status-critical/10 text-status-critical">
|
||||
CRITICAL
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -104,7 +104,7 @@ export function ApprovalModal({
|
||||
<h2 className="font-heading text-lg font-bold text-nothing-gray-900">
|
||||
{displayTitle}
|
||||
</h2>
|
||||
<p className="text-xs text-nothing-gray-500 font-mono">
|
||||
<p className="text-xs text-nothing-gray-500 font-body">
|
||||
Multi-Sig Authorization Required
|
||||
</p>
|
||||
</div>
|
||||
@@ -127,7 +127,7 @@ export function ApprovalModal({
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</button>
|
||||
<span className="text-sm font-mono text-nothing-gray-600 tabular-nums min-w-[3rem] text-center">
|
||||
<span className="text-sm font-body text-nothing-gray-600 tabular-nums min-w-[3rem] text-center">
|
||||
{current} / {total}
|
||||
</span>
|
||||
<button
|
||||
|
||||
@@ -112,7 +112,7 @@ export function SlidePanel({
|
||||
{/* 導航控制 */}
|
||||
{(onPrev || onNext) && current && total && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-mono text-nothing-gray-400">
|
||||
<span className="text-xs font-body text-nothing-gray-400">
|
||||
{current} / {total}
|
||||
</span>
|
||||
<div className="flex items-center gap-1">
|
||||
|
||||
@@ -242,7 +242,7 @@ export const StatusBadge = forwardRef<HTMLSpanElement, StatusBadgeProps>(
|
||||
)}
|
||||
>
|
||||
<StatusOrb status={status} size={size} {...props} />
|
||||
<span className="text-xs font-mono text-nothing-gray-600">
|
||||
<span className="text-xs font-body text-nothing-gray-600">
|
||||
{displayLabel}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
@@ -155,7 +155,7 @@ function ToastItem({ toast, onClose }: { toast: ToastItem; onClose: () => void }
|
||||
toast.type === 'loading' && 'animate-spin'
|
||||
)}
|
||||
/>
|
||||
<p className={cn('font-mono text-sm', config.text)}>{toast.message}</p>
|
||||
<p className={cn('font-body text-sm', config.text)}>{toast.message}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user