fix: migrate all components to theme-aware CSS variables

Replace ~150 hardcoded Tailwind color classes (bg-zinc-*, text-zinc-*,
border-white/*, text-cyan-*, bg-cyan-*) with CSS custom properties
(--pc-*) across all 17 components.

Add @theme block in index.css for Tailwind v4 theme-aware utility
classes (bg-pc-elevated, text-pc-text, border-pc-border, etc.).

Add --pc-hover, --pc-hover-strong, --pc-separator variables per theme
(white/alpha for dark/OLED, black/alpha for light).

Theme switcher (dark/light/OLED) now actually works — all UI elements
respond to theme changes in real-time.

Fixes #55
This commit is contained in:
Nicolas Varrot
2026-02-13 00:29:50 +00:00
parent 62663e1ac9
commit b60c0ce3c4
17 changed files with 184 additions and 155 deletions

View File

@@ -204,15 +204,15 @@ function InternalsSummary({ blocks }: { blocks: MessageBlock[] }) {
function InternalOnlyMessage({ message }: { message: ChatMessageType }) {
return (
<div className="animate-fade-in flex gap-3 px-4 py-1">
<div className="shrink-0 mt-0.5 flex h-6 w-6 items-center justify-center rounded-xl border border-white/5 bg-zinc-800/30">
<Wrench className="h-3 w-3 text-zinc-500" />
<div className="shrink-0 mt-0.5 flex h-6 w-6 items-center justify-center rounded-xl border border-pc-border bg-pc-elevated/30">
<Wrench className="h-3 w-3 text-pc-text-muted" />
</div>
<div className="min-w-0 flex-1">
<div className="space-y-1">
{renderInternalBlocks(message.blocks)}
</div>
{message.timestamp && (
<div className="mt-0.5 text-[10px] text-zinc-600">
<div className="mt-0.5 text-[10px] text-pc-text-faint">
{formatTimestamp(message.timestamp)}
</div>
)}
@@ -233,7 +233,7 @@ function CopyButton({ text }: { text: string }) {
return (
<button
onClick={handleCopy}
className="absolute top-2 right-2 h-7 w-7 rounded-lg border border-white/8 bg-zinc-800/80 backdrop-blur-sm flex items-center justify-center text-zinc-400 hover:text-cyan-300 hover:border-cyan-400/30 transition-all opacity-0 group-hover:opacity-100"
className="absolute top-2 right-2 h-7 w-7 rounded-lg border border-pc-border bg-pc-elevated/80 backdrop-blur-sm flex items-center justify-center text-pc-text-secondary hover:text-pc-accent-light hover:border-[var(--pc-accent-dim)] transition-all opacity-0 group-hover:opacity-100"
title={copied ? t('message.copied') : t('message.copy')}
aria-label={t('message.copy')}
>
@@ -271,18 +271,18 @@ function MetadataViewer({ metadata }: { metadata?: Record<string, unknown> }) {
<button
ref={btnRef}
onClick={() => setOpen(o => !o)}
className="h-7 w-7 rounded-lg border border-white/8 bg-zinc-800/80 backdrop-blur-sm flex items-center justify-center text-zinc-400 hover:text-cyan-300 hover:border-cyan-400/30 transition-all opacity-0 group-hover:opacity-100"
className="h-7 w-7 rounded-lg border border-pc-border bg-pc-elevated/80 backdrop-blur-sm flex items-center justify-center text-pc-text-secondary hover:text-pc-accent-light hover:border-[var(--pc-accent-dim)] transition-all opacity-0 group-hover:opacity-100"
title={t('message.metadata')}
aria-label={t('message.metadata')}
>
<Info size={13} />
</button>
{open && pos && createPortal(
<div ref={panelRef} className="fixed z-[9999] w-72 max-h-64 overflow-auto rounded-xl border border-white/10 bg-zinc-900/95 backdrop-blur-md shadow-xl p-3 text-[11px] text-zinc-400 font-mono leading-relaxed custom-scrollbar" style={{ top: pos.top, left: pos.left, transform: 'translateY(-100%)' }}>
<div ref={panelRef} className="fixed z-[9999] w-72 max-h-64 overflow-auto rounded-xl border border-pc-border-strong bg-pc-input/95 backdrop-blur-md shadow-xl p-3 text-[11px] text-pc-text-secondary font-mono leading-relaxed custom-scrollbar" style={{ top: pos.top, left: pos.left, transform: 'translateY(-100%)' }}>
{Object.entries(metadata).map(([k, v]) => (
<div key={k} className="flex gap-2 py-0.5">
<span className="text-cyan-400/70 shrink-0">{k}:</span>
<span className="text-zinc-300 break-all">{typeof v === 'object' ? JSON.stringify(v) : String(v)}</span>
<span className="text-pc-accent/70 shrink-0">{k}:</span>
<span className="text-pc-text break-all">{typeof v === 'object' ? JSON.stringify(v) : String(v)}</span>
</div>
))}
</div>,
@@ -309,12 +309,12 @@ function SystemEventMessage({ message }: { message: ChatMessageType }) {
return (
<div className="animate-fade-in flex items-center justify-center gap-2 px-4 py-1.5 my-0.5">
<div className="flex items-center gap-1.5 max-w-[85%] rounded-full px-3 py-1 bg-zinc-800/30 border border-white/5">
<Zap className="h-3 w-3 text-zinc-500 shrink-0" />
<span className="text-[11px] font-medium text-zinc-500 shrink-0">{label}</span>
<span className="text-[11px] text-zinc-500 truncate">{display}</span>
<div className="flex items-center gap-1.5 max-w-[85%] rounded-full px-3 py-1 bg-pc-elevated/30 border border-pc-border">
<Zap className="h-3 w-3 text-pc-text-muted shrink-0" />
<span className="text-[11px] font-medium text-pc-text-muted shrink-0">{label}</span>
<span className="text-[11px] text-pc-text-muted truncate">{display}</span>
{message.timestamp && (
<span className="text-[10px] text-zinc-600 shrink-0 ml-1">{formatTimestamp(message.timestamp)}</span>
<span className="text-[10px] text-pc-text-faint shrink-0 ml-1">{formatTimestamp(message.timestamp)}</span>
)}
</div>
</div>
@@ -343,12 +343,12 @@ export function ChatMessageComponent({ message, onRetry, agentAvatarUrl }: { mes
return (
<div className={`animate-fade-in flex gap-3 px-4 py-2 ${isUser ? 'flex-row-reverse' : ''}`}>
{/* Avatar */}
<div className="shrink-0 mt-1 flex h-9 w-9 items-center justify-center rounded-2xl border border-white/8 bg-zinc-800/40 overflow-hidden">
<div className="shrink-0 mt-1 flex h-9 w-9 items-center justify-center rounded-2xl border border-pc-border bg-pc-elevated/40 overflow-hidden">
{isUser
? <User className="h-4 w-4 text-cyan-200" />
? <User className="h-4 w-4 text-pc-accent-light" />
: agentAvatarUrl
? <img src={agentAvatarUrl} alt="Agent" className="h-full w-full object-cover" />
: <Bot className="h-4 w-4 text-cyan-200" />
: <Bot className="h-4 w-4 text-pc-accent-light" />
}
</div>
@@ -356,8 +356,8 @@ export function ChatMessageComponent({ message, onRetry, agentAvatarUrl }: { mes
<div className={`min-w-0 max-w-[80%] ${isUser ? 'text-right' : ''}`}>
<div className={`group relative inline-block text-left rounded-3xl px-4 py-3 text-sm leading-relaxed max-w-full overflow-hidden ${
isUser
? 'bg-gradient-to-b from-cyan-800/40 to-cyan-900/25 text-zinc-100 border border-cyan-400/30'
: 'bg-zinc-800/40 text-zinc-300 border border-white/8 shadow-[0_0_0_1px_rgba(255,255,255,0.03)]'
? 'bg-[var(--pc-user-bubble)] text-pc-text border border-[var(--pc-user-border)]'
: 'bg-pc-elevated/40 text-pc-text border border-pc-border shadow-[0_0_0_1px_rgba(255,255,255,0.03)]'
}`}>
{/* Action buttons */}
{!isUser && !message.isStreaming && getPlainText(message).trim() && (
@@ -370,7 +370,7 @@ export function ChatMessageComponent({ message, onRetry, agentAvatarUrl }: { mes
{isUser && onRetry && (
<button
onClick={() => onRetry(getPlainText(message))}
className="absolute top-2 right-2 h-7 w-7 rounded-lg border border-white/8 bg-zinc-800/80 backdrop-blur-sm flex items-center justify-center text-zinc-400 hover:text-cyan-300 hover:border-cyan-400/30 transition-all opacity-0 group-hover:opacity-100"
className="absolute top-2 right-2 h-7 w-7 rounded-lg border border-pc-border bg-pc-elevated/80 backdrop-blur-sm flex items-center justify-center text-pc-text-secondary hover:text-pc-accent-light hover:border-[var(--pc-accent-dim)] transition-all opacity-0 group-hover:opacity-100"
title={t('message.retry')}
aria-label={t('message.retry')}
>
@@ -408,7 +408,7 @@ export function ChatMessageComponent({ message, onRetry, agentAvatarUrl }: { mes
{!isUser && <InternalsSummary blocks={message.blocks} />}
</div>
{message.timestamp && (
<div className={`mt-1 text-[11px] text-zinc-500 ${isUser ? 'text-right pr-2' : 'pl-2'}`}>
<div className={`mt-1 text-[11px] text-pc-text-muted ${isUser ? 'text-right pr-2' : 'pl-2'}`}>
{formatTimestamp(message.timestamp)}
</div>
)}