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:
@@ -136,30 +136,30 @@ export function Chat({ messages, isGenerating, isLoadingHistory, status, session
|
|||||||
<div ref={scrollContainerRef} className="flex-1 overflow-y-auto overflow-x-hidden relative" role="log" aria-label={t('chat.messages')} aria-live="polite">
|
<div ref={scrollContainerRef} className="flex-1 overflow-y-auto overflow-x-hidden relative" role="log" aria-label={t('chat.messages')} aria-live="polite">
|
||||||
<div className="max-w-4xl mx-auto py-4 w-full">
|
<div className="max-w-4xl mx-auto py-4 w-full">
|
||||||
{messages.length === 0 && isLoadingHistory && (
|
{messages.length === 0 && isLoadingHistory && (
|
||||||
<div className="flex flex-col items-center justify-center h-[60vh] text-zinc-500">
|
<div className="flex flex-col items-center justify-center h-[60vh] text-pc-text-muted">
|
||||||
<Loader2 className="h-8 w-8 text-cyan-300/60 animate-spin mb-4" />
|
<Loader2 className="h-8 w-8 text-pc-accent-light/60 animate-spin mb-4" />
|
||||||
<div className="text-sm text-zinc-500">{t('chat.loadingHistory')}</div>
|
<div className="text-sm text-pc-text-muted">{t('chat.loadingHistory')}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{messages.length === 0 && !isLoadingHistory && (
|
{messages.length === 0 && !isLoadingHistory && (
|
||||||
<div className="flex flex-col items-center justify-center h-[60vh] text-zinc-500">
|
<div className="flex flex-col items-center justify-center h-[60vh] text-pc-text-muted">
|
||||||
<div className="relative mb-6">
|
<div className="relative mb-6">
|
||||||
<div className="absolute -inset-4 rounded-3xl bg-gradient-to-r from-cyan-400/10 via-indigo-500/10 to-violet-500/10 blur-2xl" />
|
<div className="absolute -inset-4 rounded-3xl bg-gradient-to-r from-cyan-400/10 via-indigo-500/10 to-violet-500/10 blur-2xl" />
|
||||||
<div className="relative flex h-16 w-16 items-center justify-center rounded-3xl border border-white/8 bg-zinc-800/40">
|
<div className="relative flex h-16 w-16 items-center justify-center rounded-3xl border border-pc-border bg-pc-elevated/40">
|
||||||
<Bot className="h-8 w-8 text-cyan-200" />
|
<Bot className="h-8 w-8 text-pc-accent-light" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-lg text-zinc-200 font-semibold">{t('chat.welcome')}</div>
|
<div className="text-lg text-pc-text font-semibold">{t('chat.welcome')}</div>
|
||||||
<div className="text-sm mt-1 text-zinc-500">{t('chat.welcomeSub')}</div>
|
<div className="text-sm mt-1 text-pc-text-muted">{t('chat.welcomeSub')}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{visibleMessages.map(({ msg, showSep }) => (
|
{visibleMessages.map(({ msg, showSep }) => (
|
||||||
<div key={msg.id}>
|
<div key={msg.id}>
|
||||||
{showSep && (
|
{showSep && (
|
||||||
<div className="flex items-center gap-3 py-3 px-4 select-none" aria-label={formatDateSeparator(msg.timestamp, t)}>
|
<div className="flex items-center gap-3 py-3 px-4 select-none" aria-label={formatDateSeparator(msg.timestamp, t)}>
|
||||||
<div className="flex-1 h-px bg-white/8" />
|
<div className="flex-1 h-px bg-[var(--pc-hover-strong)]" />
|
||||||
<span className="text-[11px] font-medium text-zinc-500 uppercase tracking-wider">{formatDateSeparator(msg.timestamp, t)}</span>
|
<span className="text-[11px] font-medium text-pc-text-muted uppercase tracking-wider">{formatDateSeparator(msg.timestamp, t)}</span>
|
||||||
<div className="flex-1 h-px bg-white/8" />
|
<div className="flex-1 h-px bg-[var(--pc-hover-strong)]" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<ChatMessageComponent message={msg} onRetry={!isGenerating ? handleSend : undefined} agentAvatarUrl={agentAvatarUrl} />
|
<ChatMessageComponent message={msg} onRetry={!isGenerating ? handleSend : undefined} agentAvatarUrl={agentAvatarUrl} />
|
||||||
@@ -176,7 +176,7 @@ export function Chat({ messages, isGenerating, isLoadingHistory, status, session
|
|||||||
onClick={globalState === 'expand-all' ? collapseAll : expandAll}
|
onClick={globalState === 'expand-all' ? collapseAll : expandAll}
|
||||||
aria-label={globalState === 'expand-all' ? t('chat.collapseTools') : t('chat.expandTools')}
|
aria-label={globalState === 'expand-all' ? t('chat.collapseTools') : t('chat.expandTools')}
|
||||||
title={globalState === 'expand-all' ? t('chat.collapseTools') : t('chat.expandTools')}
|
title={globalState === 'expand-all' ? t('chat.collapseTools') : t('chat.expandTools')}
|
||||||
className="flex items-center gap-1.5 rounded-full border border-white/10 bg-zinc-800/90 backdrop-blur-lg px-3 py-2 text-xs text-zinc-300 shadow-lg hover:bg-zinc-700/90 transition-all hover:shadow-violet-500/10"
|
className="flex items-center gap-1.5 rounded-full border border-pc-border-strong bg-pc-elevated/90 backdrop-blur-lg px-3 py-2 text-xs text-pc-text shadow-lg hover:bg-pc-elevated/90 transition-all hover:shadow-violet-500/10"
|
||||||
>
|
>
|
||||||
{globalState === 'expand-all' ? <ChevronsDownUp size={14} className="text-violet-300" /> : <ChevronsUpDown size={14} className="text-violet-300" />}
|
{globalState === 'expand-all' ? <ChevronsDownUp size={14} className="text-violet-300" /> : <ChevronsUpDown size={14} className="text-violet-300" />}
|
||||||
</button>
|
</button>
|
||||||
@@ -185,9 +185,9 @@ export function Chat({ messages, isGenerating, isLoadingHistory, status, session
|
|||||||
<button
|
<button
|
||||||
onClick={() => scrollToBottom('smooth')}
|
onClick={() => scrollToBottom('smooth')}
|
||||||
aria-label={t('chat.scrollToBottom')}
|
aria-label={t('chat.scrollToBottom')}
|
||||||
className="flex items-center gap-1.5 rounded-full border border-white/10 bg-zinc-800/90 backdrop-blur-lg px-3.5 py-2 text-xs text-zinc-300 shadow-lg hover:bg-zinc-700/90 transition-all hover:shadow-cyan-500/10"
|
className="flex items-center gap-1.5 rounded-full border border-pc-border-strong bg-pc-elevated/90 backdrop-blur-lg px-3.5 py-2 text-xs text-pc-text shadow-lg hover:bg-pc-elevated/90 transition-all hover:shadow-cyan-500/10"
|
||||||
>
|
>
|
||||||
<ArrowDown size={14} className="text-cyan-300" />
|
<ArrowDown size={14} className="text-pc-accent-light" />
|
||||||
<span className="hidden sm:inline">{t('chat.scrollToBottom')}</span>
|
<span className="hidden sm:inline">{t('chat.scrollToBottom')}</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ export function ChatInput({ onSend, onAbort, isGenerating, disabled, sessionKey
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="border-t border-white/8 bg-[var(--pc-bg-input)]/60 backdrop-blur-xl p-4"
|
className="border-t border-pc-border bg-[var(--pc-bg-input)]/60 backdrop-blur-xl p-4"
|
||||||
role="form"
|
role="form"
|
||||||
aria-label={t('chat.inputLabel')}
|
aria-label={t('chat.inputLabel')}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
@@ -222,26 +222,26 @@ export function ChatInput({ onSend, onAbort, isGenerating, disabled, sessionKey
|
|||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
>
|
>
|
||||||
<div className="max-w-4xl mx-auto">
|
<div className="max-w-4xl mx-auto">
|
||||||
<div className={`rounded-3xl border bg-[var(--pc-bg-surface)]/40 p-3 shadow-[0_0_0_1px_rgba(255,255,255,0.03)] transition-colors ${isDragOver ? 'border-cyan-400/40 bg-cyan-400/5' : 'border-white/8'}`}>
|
<div className={`rounded-3xl border bg-[var(--pc-bg-surface)]/40 p-3 shadow-[0_0_0_1px_rgba(255,255,255,0.03)] transition-colors ${isDragOver ? 'border-[var(--pc-accent-dim)] bg-[var(--pc-accent-glow)]' : 'border-pc-border'}`}>
|
||||||
{/* File previews */}
|
{/* File previews */}
|
||||||
{files.length > 0 && (
|
{files.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-2 mb-3 px-1">
|
<div className="flex flex-wrap gap-2 mb-3 px-1">
|
||||||
{files.map(f => (
|
{files.map(f => (
|
||||||
<div key={f.id} className="group relative flex items-center gap-2 rounded-2xl border border-white/8 bg-zinc-800/50 px-3 py-2 text-xs text-zinc-400">
|
<div key={f.id} className="group relative flex items-center gap-2 rounded-2xl border border-pc-border bg-pc-elevated/50 px-3 py-2 text-xs text-pc-text-secondary">
|
||||||
{f.preview ? (
|
{f.preview ? (
|
||||||
<img src={f.preview} alt="" className="h-8 w-8 rounded-lg object-cover" />
|
<img src={f.preview} alt="" className="h-8 w-8 rounded-lg object-cover" />
|
||||||
) : (
|
) : (
|
||||||
<FileText size={16} className="text-zinc-500 shrink-0" />
|
<FileText size={16} className="text-pc-text-muted shrink-0" />
|
||||||
)}
|
)}
|
||||||
<div className="min-w-0 max-w-[120px]">
|
<div className="min-w-0 max-w-[120px]">
|
||||||
<div className="truncate text-zinc-300">{f.file.name}</div>
|
<div className="truncate text-pc-text">{f.file.name}</div>
|
||||||
<div className="text-[10px] text-zinc-500">{formatSize(f.file.size)}</div>
|
<div className="text-[10px] text-pc-text-muted">{formatSize(f.file.size)}</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => removeFile(f.id)}
|
onClick={() => removeFile(f.id)}
|
||||||
className="absolute -top-1.5 -right-1.5 h-5 w-5 rounded-full bg-zinc-700 border border-white/10 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity hover:bg-red-500/80"
|
className="absolute -top-1.5 -right-1.5 h-5 w-5 rounded-full bg-pc-elevated border border-pc-border-strong flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity hover:bg-red-500/80"
|
||||||
>
|
>
|
||||||
<X size={10} className="text-zinc-200" />
|
<X size={10} className="text-pc-text" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -253,7 +253,7 @@ export function ChatInput({ onSend, onAbort, isGenerating, disabled, sessionKey
|
|||||||
<button
|
<button
|
||||||
onClick={() => fileInputRef.current?.click()}
|
onClick={() => fileInputRef.current?.click()}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className="shrink-0 h-11 w-11 rounded-2xl border border-white/8 bg-zinc-800/30 flex items-center justify-center text-zinc-400 hover:text-cyan-300 hover:bg-white/5 transition-colors disabled:opacity-30"
|
className="shrink-0 h-11 w-11 rounded-2xl border border-pc-border bg-pc-elevated/30 flex items-center justify-center text-pc-text-secondary hover:text-pc-accent-light hover:bg-[var(--pc-hover)] transition-colors disabled:opacity-30"
|
||||||
title={t('chat.attachFile')}
|
title={t('chat.attachFile')}
|
||||||
aria-label={t('chat.attachFile')}
|
aria-label={t('chat.attachFile')}
|
||||||
>
|
>
|
||||||
@@ -278,7 +278,7 @@ export function ChatInput({ onSend, onAbort, isGenerating, disabled, sessionKey
|
|||||||
aria-label={t('chat.inputLabel')}
|
aria-label={t('chat.inputLabel')}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
rows={1}
|
rows={1}
|
||||||
className="flex-1 bg-transparent resize-none rounded-2xl border border-white/8 bg-zinc-900/35 px-4 py-3 text-sm text-zinc-300 placeholder:text-zinc-500 outline-none focus:ring-2 focus:ring-cyan-400/30 transition-all max-h-[200px]"
|
className="flex-1 bg-transparent resize-none rounded-2xl border border-pc-border bg-pc-input/35 px-4 py-3 text-sm text-pc-text placeholder:text-pc-text-muted outline-none focus:ring-2 focus:ring-[var(--pc-accent-dim)] transition-all max-h-[200px]"
|
||||||
/>
|
/>
|
||||||
{isGenerating ? (
|
{isGenerating ? (
|
||||||
<button
|
<button
|
||||||
@@ -293,7 +293,7 @@ export function ChatInput({ onSend, onAbort, isGenerating, disabled, sessionKey
|
|||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
disabled={(!text.trim() && files.length === 0) || disabled}
|
disabled={(!text.trim() && files.length === 0) || disabled}
|
||||||
aria-label={t('chat.send')}
|
aria-label={t('chat.send')}
|
||||||
className="shrink-0 h-11 px-5 rounded-2xl bg-gradient-to-r from-cyan-500/80 via-indigo-500/70 to-violet-500/80 text-zinc-900 font-semibold text-sm hover:opacity-90 shadow-[0_8px_24px_rgba(34,211,238,0.1)] disabled:opacity-30 disabled:shadow-none transition-all flex items-center gap-2"
|
className="shrink-0 h-11 px-5 rounded-2xl bg-gradient-to-r from-cyan-500/80 via-indigo-500/70 to-violet-500/80 text-pc-text font-semibold text-sm hover:opacity-90 shadow-[0_8px_24px_rgba(34,211,238,0.1)] disabled:opacity-30 disabled:shadow-none transition-all flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<Send size={16} />
|
<Send size={16} />
|
||||||
<span className="hidden sm:inline">{t('chat.send')}</span>
|
<span className="hidden sm:inline">{t('chat.send')}</span>
|
||||||
|
|||||||
@@ -204,15 +204,15 @@ function InternalsSummary({ blocks }: { blocks: MessageBlock[] }) {
|
|||||||
function InternalOnlyMessage({ message }: { message: ChatMessageType }) {
|
function InternalOnlyMessage({ message }: { message: ChatMessageType }) {
|
||||||
return (
|
return (
|
||||||
<div className="animate-fade-in flex gap-3 px-4 py-1">
|
<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">
|
<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-zinc-500" />
|
<Wrench className="h-3 w-3 text-pc-text-muted" />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{renderInternalBlocks(message.blocks)}
|
{renderInternalBlocks(message.blocks)}
|
||||||
</div>
|
</div>
|
||||||
{message.timestamp && (
|
{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)}
|
{formatTimestamp(message.timestamp)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -233,7 +233,7 @@ function CopyButton({ text }: { text: string }) {
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={handleCopy}
|
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')}
|
title={copied ? t('message.copied') : t('message.copy')}
|
||||||
aria-label={t('message.copy')}
|
aria-label={t('message.copy')}
|
||||||
>
|
>
|
||||||
@@ -271,18 +271,18 @@ function MetadataViewer({ metadata }: { metadata?: Record<string, unknown> }) {
|
|||||||
<button
|
<button
|
||||||
ref={btnRef}
|
ref={btnRef}
|
||||||
onClick={() => setOpen(o => !o)}
|
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')}
|
title={t('message.metadata')}
|
||||||
aria-label={t('message.metadata')}
|
aria-label={t('message.metadata')}
|
||||||
>
|
>
|
||||||
<Info size={13} />
|
<Info size={13} />
|
||||||
</button>
|
</button>
|
||||||
{open && pos && createPortal(
|
{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]) => (
|
{Object.entries(metadata).map(([k, v]) => (
|
||||||
<div key={k} className="flex gap-2 py-0.5">
|
<div key={k} className="flex gap-2 py-0.5">
|
||||||
<span className="text-cyan-400/70 shrink-0">{k}:</span>
|
<span className="text-pc-accent/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-text break-all">{typeof v === 'object' ? JSON.stringify(v) : String(v)}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>,
|
</div>,
|
||||||
@@ -309,12 +309,12 @@ function SystemEventMessage({ message }: { message: ChatMessageType }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="animate-fade-in flex items-center justify-center gap-2 px-4 py-1.5 my-0.5">
|
<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">
|
<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-zinc-500 shrink-0" />
|
<Zap className="h-3 w-3 text-pc-text-muted shrink-0" />
|
||||||
<span className="text-[11px] font-medium text-zinc-500 shrink-0">{label}</span>
|
<span className="text-[11px] font-medium text-pc-text-muted shrink-0">{label}</span>
|
||||||
<span className="text-[11px] text-zinc-500 truncate">{display}</span>
|
<span className="text-[11px] text-pc-text-muted truncate">{display}</span>
|
||||||
{message.timestamp && (
|
{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>
|
||||||
</div>
|
</div>
|
||||||
@@ -343,12 +343,12 @@ export function ChatMessageComponent({ message, onRetry, agentAvatarUrl }: { mes
|
|||||||
return (
|
return (
|
||||||
<div className={`animate-fade-in flex gap-3 px-4 py-2 ${isUser ? 'flex-row-reverse' : ''}`}>
|
<div className={`animate-fade-in flex gap-3 px-4 py-2 ${isUser ? 'flex-row-reverse' : ''}`}>
|
||||||
{/* Avatar */}
|
{/* 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
|
{isUser
|
||||||
? <User className="h-4 w-4 text-cyan-200" />
|
? <User className="h-4 w-4 text-pc-accent-light" />
|
||||||
: agentAvatarUrl
|
: agentAvatarUrl
|
||||||
? <img src={agentAvatarUrl} alt="Agent" className="h-full w-full object-cover" />
|
? <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>
|
</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={`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 ${
|
<div className={`group relative inline-block text-left rounded-3xl px-4 py-3 text-sm leading-relaxed max-w-full overflow-hidden ${
|
||||||
isUser
|
isUser
|
||||||
? 'bg-gradient-to-b from-cyan-800/40 to-cyan-900/25 text-zinc-100 border border-cyan-400/30'
|
? 'bg-[var(--pc-user-bubble)] text-pc-text border border-[var(--pc-user-border)]'
|
||||||
: 'bg-zinc-800/40 text-zinc-300 border border-white/8 shadow-[0_0_0_1px_rgba(255,255,255,0.03)]'
|
: 'bg-pc-elevated/40 text-pc-text border border-pc-border shadow-[0_0_0_1px_rgba(255,255,255,0.03)]'
|
||||||
}`}>
|
}`}>
|
||||||
{/* Action buttons */}
|
{/* Action buttons */}
|
||||||
{!isUser && !message.isStreaming && getPlainText(message).trim() && (
|
{!isUser && !message.isStreaming && getPlainText(message).trim() && (
|
||||||
@@ -370,7 +370,7 @@ export function ChatMessageComponent({ message, onRetry, agentAvatarUrl }: { mes
|
|||||||
{isUser && onRetry && (
|
{isUser && onRetry && (
|
||||||
<button
|
<button
|
||||||
onClick={() => onRetry(getPlainText(message))}
|
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')}
|
title={t('message.retry')}
|
||||||
aria-label={t('message.retry')}
|
aria-label={t('message.retry')}
|
||||||
>
|
>
|
||||||
@@ -408,7 +408,7 @@ export function ChatMessageComponent({ message, onRetry, agentAvatarUrl }: { mes
|
|||||||
{!isUser && <InternalsSummary blocks={message.blocks} />}
|
{!isUser && <InternalsSummary blocks={message.blocks} />}
|
||||||
</div>
|
</div>
|
||||||
{message.timestamp && (
|
{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)}
|
{formatTimestamp(message.timestamp)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -60,14 +60,14 @@ export function CodeBlock(props: HTMLAttributes<HTMLPreElement>) {
|
|||||||
return (
|
return (
|
||||||
<div className="group/code relative">
|
<div className="group/code relative">
|
||||||
{language && (
|
{language && (
|
||||||
<div className="flex items-center justify-between px-4 py-1.5 bg-zinc-800/80 border-b border-white/5 rounded-t-lg text-[11px] text-zinc-500 font-mono select-none">
|
<div className="flex items-center justify-between px-4 py-1.5 bg-pc-elevated/80 border-b border-pc-border rounded-t-lg text-[11px] text-pc-text-muted font-mono select-none">
|
||||||
{formatLanguage(language)}
|
{formatLanguage(language)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<pre {...props} className={`${props.className || ''} ${language ? '!rounded-t-none !mt-0' : ''}`} />
|
<pre {...props} className={`${props.className || ''} ${language ? '!rounded-t-none !mt-0' : ''}`} />
|
||||||
<button
|
<button
|
||||||
onClick={handleCopy}
|
onClick={handleCopy}
|
||||||
className="absolute top-2 right-2 p-1.5 rounded-lg bg-zinc-700/60 hover:bg-zinc-600/80 border border-white/10 text-zinc-400 hover:text-zinc-200 opacity-0 group-hover/code:opacity-100 transition-opacity duration-150"
|
className="absolute top-2 right-2 p-1.5 rounded-lg bg-pc-elevated/60 hover:bg-pc-elevated/80 border border-pc-border-strong text-pc-text-secondary hover:text-pc-text opacity-0 group-hover/code:opacity-100 transition-opacity duration-150"
|
||||||
title="Copy code"
|
title="Copy code"
|
||||||
aria-label="Copy code to clipboard"
|
aria-label="Copy code to clipboard"
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -40,30 +40,30 @@ export class ErrorBoundary extends Component<Props, State> {
|
|||||||
if (this.props.fallback) return this.props.fallback;
|
if (this.props.fallback) return this.props.fallback;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-dvh flex items-center justify-center bg-[var(--pc-bg-base)] text-zinc-300 p-6">
|
<div className="h-dvh flex items-center justify-center bg-[var(--pc-bg-base)] text-pc-text p-6">
|
||||||
<div className="max-w-md w-full space-y-4 text-center">
|
<div className="max-w-md w-full space-y-4 text-center">
|
||||||
<div className="text-4xl">💥</div>
|
<div className="text-4xl">💥</div>
|
||||||
<h1 className="text-xl font-semibold text-zinc-100">
|
<h1 className="text-xl font-semibold text-pc-text">
|
||||||
{t('error.title')}
|
{t('error.title')}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-zinc-400">
|
<p className="text-sm text-pc-text-secondary">
|
||||||
{t('error.description')}
|
{t('error.description')}
|
||||||
</p>
|
</p>
|
||||||
{this.state.error && (
|
{this.state.error && (
|
||||||
<pre className="mt-3 p-3 rounded-lg bg-zinc-800/60 text-xs text-red-400 text-left overflow-auto max-h-32">
|
<pre className="mt-3 p-3 rounded-lg bg-pc-elevated/60 text-xs text-red-400 text-left overflow-auto max-h-32">
|
||||||
{this.state.error.message}
|
{this.state.error.message}
|
||||||
</pre>
|
</pre>
|
||||||
)}
|
)}
|
||||||
<div className="flex gap-3 justify-center pt-2">
|
<div className="flex gap-3 justify-center pt-2">
|
||||||
<button
|
<button
|
||||||
onClick={this.handleRetry}
|
onClick={this.handleRetry}
|
||||||
className="px-4 py-2 rounded-lg bg-zinc-700 hover:bg-zinc-600 text-sm font-medium transition-colors"
|
className="px-4 py-2 rounded-lg bg-pc-elevated hover:bg-pc-elevated text-sm font-medium transition-colors"
|
||||||
>
|
>
|
||||||
{t('error.retry')}
|
{t('error.retry')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={this.handleReload}
|
onClick={this.handleReload}
|
||||||
className="px-4 py-2 rounded-lg bg-cyan-600 hover:bg-cyan-500 text-sm font-medium transition-colors"
|
className="px-4 py-2 rounded-lg bg-[var(--pc-accent)] hover:bg-[var(--pc-accent-light)] text-sm font-medium transition-colors"
|
||||||
>
|
>
|
||||||
{t('error.reload')}
|
{t('error.reload')}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -33,23 +33,23 @@ export function Header({ status, sessionKey, onToggleSidebar, activeSessionData,
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<header className="h-14 border-b border-white/8 bg-[var(--pc-bg-surface)]/90 backdrop-blur-xl flex items-center px-4 gap-3 shrink-0" role="banner">
|
<header className="h-14 border-b border-pc-border bg-[var(--pc-bg-surface)]/90 backdrop-blur-xl flex items-center px-4 gap-3 shrink-0" role="banner">
|
||||||
<button onClick={onToggleSidebar} aria-label={t('header.toggleSidebar')} className="lg:hidden p-2 rounded-2xl hover:bg-white/5 text-zinc-400 transition-colors">
|
<button onClick={onToggleSidebar} aria-label={t('header.toggleSidebar')} className="lg:hidden p-2 rounded-2xl hover:bg-[var(--pc-hover)] text-pc-text-secondary transition-colors">
|
||||||
<Menu size={20} />
|
<Menu size={20} />
|
||||||
</button>
|
</button>
|
||||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||||
<img src={agentAvatarUrl || '/logo.png'} alt="PinchChat" className="h-9 w-9 rounded-2xl object-cover" />
|
<img src={agentAvatarUrl || '/logo.png'} alt="PinchChat" className="h-9 w-9 rounded-2xl object-cover" />
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="font-semibold text-zinc-300 text-sm tracking-wide">{t('header.title')}</span>
|
<span className="font-semibold text-pc-text text-sm tracking-wide">{t('header.title')}</span>
|
||||||
<Sparkles className="h-3.5 w-3.5 text-cyan-300/60" />
|
<Sparkles className="h-3.5 w-3.5 text-pc-accent-light/60" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-zinc-500 truncate flex items-center gap-1.5">
|
<span className="text-xs text-pc-text-muted truncate flex items-center gap-1.5">
|
||||||
{activeSessionData?.agentId && (
|
{activeSessionData?.agentId && (
|
||||||
<span className="inline-flex items-center gap-0.5 text-cyan-400/70 font-medium">
|
<span className="inline-flex items-center gap-0.5 text-pc-accent/70 font-medium">
|
||||||
<Bot className="h-3 w-3" />
|
<Bot className="h-3 w-3" />
|
||||||
{activeSessionData.agentId}
|
{activeSessionData.agentId}
|
||||||
<span className="text-zinc-600 mx-0.5">·</span>
|
<span className="text-pc-text-faint mx-0.5">·</span>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{sessionLabel}
|
{sessionLabel}
|
||||||
@@ -61,7 +61,7 @@ export function Header({ status, sessionKey, onToggleSidebar, activeSessionData,
|
|||||||
<button
|
<button
|
||||||
onClick={onToggleSound}
|
onClick={onToggleSound}
|
||||||
aria-label={soundEnabled ? t('header.soundOff') : t('header.soundOn')}
|
aria-label={soundEnabled ? t('header.soundOff') : t('header.soundOn')}
|
||||||
className="p-2 rounded-2xl hover:bg-white/5 text-zinc-500 hover:text-zinc-300 transition-colors"
|
className="p-2 rounded-2xl hover:bg-[var(--pc-hover)] text-pc-text-muted hover:text-pc-text transition-colors"
|
||||||
title={soundEnabled ? t('header.soundOff') : t('header.soundOn')}
|
title={soundEnabled ? t('header.soundOff') : t('header.soundOn')}
|
||||||
>
|
>
|
||||||
{soundEnabled ? <Volume2 size={16} /> : <VolumeOff size={16} />}
|
{soundEnabled ? <Volume2 size={16} /> : <VolumeOff size={16} />}
|
||||||
@@ -71,7 +71,7 @@ export function Header({ status, sessionKey, onToggleSidebar, activeSessionData,
|
|||||||
<button
|
<button
|
||||||
onClick={handleExport}
|
onClick={handleExport}
|
||||||
aria-label={t('header.export')}
|
aria-label={t('header.export')}
|
||||||
className="p-2 rounded-2xl hover:bg-white/5 text-zinc-500 hover:text-zinc-300 transition-colors"
|
className="p-2 rounded-2xl hover:bg-[var(--pc-hover)] text-pc-text-muted hover:text-pc-text transition-colors"
|
||||||
title={t('header.export')}
|
title={t('header.export')}
|
||||||
>
|
>
|
||||||
<Download size={16} />
|
<Download size={16} />
|
||||||
@@ -80,26 +80,26 @@ export function Header({ status, sessionKey, onToggleSidebar, activeSessionData,
|
|||||||
<ThemeSwitcher />
|
<ThemeSwitcher />
|
||||||
<LanguageSelector />
|
<LanguageSelector />
|
||||||
{status === 'connected' ? (
|
{status === 'connected' ? (
|
||||||
<div className="flex items-center gap-2 rounded-2xl border border-white/8 bg-zinc-800/30 px-3 py-1.5">
|
<div className="flex items-center gap-2 rounded-2xl border border-pc-border bg-pc-elevated/30 px-3 py-1.5">
|
||||||
<span className="w-2 h-2 rounded-full bg-cyan-300/80 shadow-[0_0_12px_rgba(34,211,238,0.6)]" />
|
<span className="w-2 h-2 rounded-full bg-[var(--pc-accent)] shadow-[0_0_12px_var(--pc-accent-dim)]" />
|
||||||
<span className="text-xs text-zinc-300 hidden sm:inline">{t('header.connected')}</span>
|
<span className="text-xs text-pc-text hidden sm:inline">{t('header.connected')}</span>
|
||||||
</div>
|
</div>
|
||||||
) : status === 'connecting' ? (
|
) : status === 'connecting' ? (
|
||||||
<div className="flex items-center gap-2 rounded-2xl border border-white/8 bg-zinc-800/30 px-3 py-1.5">
|
<div className="flex items-center gap-2 rounded-2xl border border-pc-border bg-pc-elevated/30 px-3 py-1.5">
|
||||||
<span className="w-2 h-2 rounded-full bg-yellow-400/80 pulse-dot" />
|
<span className="w-2 h-2 rounded-full bg-yellow-400/80 pulse-dot" />
|
||||||
<span className="text-xs text-zinc-300 hidden sm:inline">{t('login.connecting')}</span>
|
<span className="text-xs text-pc-text hidden sm:inline">{t('login.connecting')}</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center gap-2 rounded-2xl border border-white/8 bg-zinc-800/30 px-3 py-1.5">
|
<div className="flex items-center gap-2 rounded-2xl border border-pc-border bg-pc-elevated/30 px-3 py-1.5">
|
||||||
<span className="w-2 h-2 rounded-full bg-red-400/80" />
|
<span className="w-2 h-2 rounded-full bg-red-400/80" />
|
||||||
<span className="text-xs text-zinc-300 hidden sm:inline">{t('header.disconnected')}</span>
|
<span className="text-xs text-pc-text hidden sm:inline">{t('header.disconnected')}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{onLogout && (
|
{onLogout && (
|
||||||
<button
|
<button
|
||||||
onClick={onLogout}
|
onClick={onLogout}
|
||||||
aria-label={t('header.logout')}
|
aria-label={t('header.logout')}
|
||||||
className="p-2 rounded-2xl hover:bg-white/5 text-zinc-500 hover:text-zinc-300 transition-colors"
|
className="p-2 rounded-2xl hover:bg-[var(--pc-hover)] text-pc-text-muted hover:text-pc-text transition-colors"
|
||||||
title={t('header.logout')}
|
title={t('header.logout')}
|
||||||
>
|
>
|
||||||
<LogOut size={16} />
|
<LogOut size={16} />
|
||||||
@@ -115,17 +115,17 @@ export function Header({ status, sessionKey, onToggleSidebar, activeSessionData,
|
|||||||
const opacity = Math.max(0.35, Math.min(1, pct / 100));
|
const opacity = Math.max(0.35, Math.min(1, pct / 100));
|
||||||
const barStyle = { width: `${pct}%`, backgroundColor: `rgba(56, 189, 248, ${opacity})` };
|
const barStyle = { width: `${pct}%`, backgroundColor: `rgba(56, 189, 248, ${opacity})` };
|
||||||
return (
|
return (
|
||||||
<div className="px-4 py-1.5 bg-[var(--pc-bg-surface)]/60 border-b border-white/8 flex items-center gap-3">
|
<div className="px-4 py-1.5 bg-[var(--pc-bg-surface)]/60 border-b border-pc-border flex items-center gap-3">
|
||||||
{activeSessionData?.model && (
|
{activeSessionData?.model && (
|
||||||
<span className="inline-flex items-center gap-1 text-[10px] text-zinc-500 shrink-0" title={`Model: ${activeSessionData.model}${activeSessionData.agentId ? ` · Agent: ${activeSessionData.agentId}` : ''}`}>
|
<span className="inline-flex items-center gap-1 text-[10px] text-pc-text-muted shrink-0" title={`Model: ${activeSessionData.model}${activeSessionData.agentId ? ` · Agent: ${activeSessionData.agentId}` : ''}`}>
|
||||||
<Cpu className="h-2.5 w-2.5" />
|
<Cpu className="h-2.5 w-2.5" />
|
||||||
<span className="truncate max-w-[120px]">{activeSessionData.model.replace(/^.*\//, '')}</span>
|
<span className="truncate max-w-[120px]">{activeSessionData.model.replace(/^.*\//, '')}</span>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<div className="flex-1 h-[5px] rounded-full bg-white/5 overflow-hidden">
|
<div className="flex-1 h-[5px] rounded-full bg-[var(--pc-hover)] overflow-hidden">
|
||||||
<div className="h-full rounded-full transition-all duration-500" style={barStyle} />
|
<div className="h-full rounded-full transition-all duration-500" style={barStyle} />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[11px] text-zinc-400 tabular-nums shrink-0 whitespace-nowrap">
|
<span className="text-[11px] text-pc-text-secondary tabular-nums shrink-0 whitespace-nowrap">
|
||||||
{(total / 1000).toFixed(1)}k / {(ctx / 1000).toFixed(0)}k tokens
|
{(total / 1000).toFixed(1)}k / {(ctx / 1000).toFixed(0)}k tokens
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ function Lightbox({ src, alt, onClose }: ImageBlockProps & { onClose: () => void
|
|||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
aria-label="Close preview"
|
aria-label="Close preview"
|
||||||
className="absolute top-4 right-4 p-2 rounded-full bg-zinc-800/80 border border-white/10 text-zinc-300 hover:text-white hover:bg-zinc-700/80 transition-colors"
|
className="absolute top-4 right-4 p-2 rounded-full bg-pc-elevated/80 border border-pc-border-strong text-pc-text hover:text-white hover:bg-pc-elevated/80 transition-colors"
|
||||||
>
|
>
|
||||||
<X size={20} />
|
<X size={20} />
|
||||||
</button>
|
</button>
|
||||||
@@ -51,7 +51,7 @@ export function ImageBlock({ src, alt }: ImageBlockProps) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setLightbox(true)}
|
onClick={() => setLightbox(true)}
|
||||||
aria-label={`View ${alt || 'image'} full size`}
|
aria-label={`View ${alt || 'image'} full size`}
|
||||||
className="block rounded-xl border border-white/8 cursor-pointer hover:brightness-110 transition-all focus:outline-none focus:ring-2 focus:ring-cyan-400/40"
|
className="block rounded-xl border border-pc-border cursor-pointer hover:brightness-110 transition-all focus:outline-none focus:ring-2 focus:ring-[var(--pc-accent-dim)]"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={src}
|
src={src}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ interface Props {
|
|||||||
|
|
||||||
function Kbd({ children }: { children: React.ReactNode }) {
|
function Kbd({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<kbd className="inline-flex items-center justify-center min-w-[1.75rem] h-7 px-2 rounded-lg border border-white/10 bg-zinc-800/80 text-xs font-mono text-zinc-300 shadow-[0_1px_0_0_rgba(255,255,255,0.05)]">
|
<kbd className="inline-flex items-center justify-center min-w-[1.75rem] h-7 px-2 rounded-lg border border-pc-border-strong bg-pc-elevated/80 text-xs font-mono text-pc-text shadow-[0_1px_0_0_rgba(255,255,255,0.05)]">
|
||||||
{children}
|
{children}
|
||||||
</kbd>
|
</kbd>
|
||||||
);
|
);
|
||||||
@@ -18,7 +18,7 @@ function Kbd({ children }: { children: React.ReactNode }) {
|
|||||||
function ShortcutRow({ keys, label }: { keys: React.ReactNode; label: string }) {
|
function ShortcutRow({ keys, label }: { keys: React.ReactNode; label: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between py-2">
|
<div className="flex items-center justify-between py-2">
|
||||||
<span className="text-sm text-zinc-400">{label}</span>
|
<span className="text-sm text-pc-text-secondary">{label}</span>
|
||||||
<div className="flex items-center gap-1.5">{keys}</div>
|
<div className="flex items-center gap-1.5">{keys}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -26,7 +26,7 @@ function ShortcutRow({ keys, label }: { keys: React.ReactNode; label: string })
|
|||||||
|
|
||||||
function SectionTitle({ children }: { children: React.ReactNode }) {
|
function SectionTitle({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div className="text-[11px] uppercase tracking-wider text-zinc-500 font-semibold mt-4 mb-1 first:mt-0">
|
<div className="text-[11px] uppercase tracking-wider text-pc-text-muted font-semibold mt-4 mb-1 first:mt-0">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -56,18 +56,18 @@ export function KeyboardShortcuts({ open, onClose }: Props) {
|
|||||||
|
|
||||||
{/* Modal */}
|
{/* Modal */}
|
||||||
<div
|
<div
|
||||||
className="relative w-full max-w-md mx-4 rounded-3xl border border-white/8 bg-[var(--pc-bg-base)]/95 backdrop-blur-xl shadow-2xl animate-fade-in"
|
className="relative w-full max-w-md mx-4 rounded-3xl border border-pc-border bg-[var(--pc-bg-base)]/95 backdrop-blur-xl shadow-2xl animate-fade-in"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between px-6 py-4 border-b border-white/8">
|
<div className="flex items-center justify-between px-6 py-4 border-b border-pc-border">
|
||||||
<div className="flex items-center gap-2.5">
|
<div className="flex items-center gap-2.5">
|
||||||
<Keyboard size={18} className="text-cyan-300/70" />
|
<Keyboard size={18} className="text-pc-accent-light/70" />
|
||||||
<h2 className="text-sm font-semibold text-zinc-200">{t('shortcuts.title')}</h2>
|
<h2 className="text-sm font-semibold text-pc-text">{t('shortcuts.title')}</h2>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="h-8 w-8 rounded-xl flex items-center justify-center text-zinc-500 hover:text-zinc-300 hover:bg-white/5 transition-colors"
|
className="h-8 w-8 rounded-xl flex items-center justify-center text-pc-text-muted hover:text-pc-text hover:bg-[var(--pc-hover)] transition-colors"
|
||||||
aria-label={t('shortcuts.close')}
|
aria-label={t('shortcuts.close')}
|
||||||
>
|
>
|
||||||
<X size={16} />
|
<X size={16} />
|
||||||
@@ -83,7 +83,7 @@ export function KeyboardShortcuts({ open, onClose }: Props) {
|
|||||||
label={t('shortcuts.send')}
|
label={t('shortcuts.send')}
|
||||||
/>
|
/>
|
||||||
<ShortcutRow
|
<ShortcutRow
|
||||||
keys={<><Kbd>Shift</Kbd><span className="text-zinc-600">+</span><Kbd>Enter</Kbd></>}
|
keys={<><Kbd>Shift</Kbd><span className="text-pc-text-faint">+</span><Kbd>Enter</Kbd></>}
|
||||||
label={t('shortcuts.newline')}
|
label={t('shortcuts.newline')}
|
||||||
/>
|
/>
|
||||||
<ShortcutRow
|
<ShortcutRow
|
||||||
@@ -95,11 +95,11 @@ export function KeyboardShortcuts({ open, onClose }: Props) {
|
|||||||
<div className="py-3">
|
<div className="py-3">
|
||||||
<SectionTitle>{t('shortcuts.navigationSection')}</SectionTitle>
|
<SectionTitle>{t('shortcuts.navigationSection')}</SectionTitle>
|
||||||
<ShortcutRow
|
<ShortcutRow
|
||||||
keys={<><Kbd>{mod}</Kbd><span className="text-zinc-600">+</span><Kbd>K</Kbd></>}
|
keys={<><Kbd>{mod}</Kbd><span className="text-pc-text-faint">+</span><Kbd>K</Kbd></>}
|
||||||
label={t('shortcuts.search')}
|
label={t('shortcuts.search')}
|
||||||
/>
|
/>
|
||||||
<ShortcutRow
|
<ShortcutRow
|
||||||
keys={<><Kbd>Alt</Kbd><span className="text-zinc-600">+</span><Kbd>↑</Kbd><span className="text-zinc-600">/</span><Kbd>↓</Kbd></>}
|
keys={<><Kbd>Alt</Kbd><span className="text-pc-text-faint">+</span><Kbd>↑</Kbd><span className="text-pc-text-faint">/</span><Kbd>↓</Kbd></>}
|
||||||
label={t('shortcuts.switchSession')}
|
label={t('shortcuts.switchSession')}
|
||||||
/>
|
/>
|
||||||
<ShortcutRow
|
<ShortcutRow
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export function LanguageSelector() {
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={cycle}
|
onClick={cycle}
|
||||||
className="flex items-center gap-1.5 rounded-2xl border border-white/8 bg-zinc-800/30 px-2.5 py-1.5 text-xs text-zinc-400 hover:text-zinc-200 hover:bg-white/5 transition-colors"
|
className="flex items-center gap-1.5 rounded-2xl border border-pc-border bg-pc-elevated/30 px-2.5 py-1.5 text-xs text-pc-text-secondary hover:text-pc-text hover:bg-[var(--pc-hover)] transition-colors"
|
||||||
title="Change language"
|
title="Change language"
|
||||||
aria-label={`Language: ${localeLabels[current] || current}. Click to change.`}
|
aria-label={`Language: ${localeLabels[current] || current}. Click to change.`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -37,22 +37,22 @@ export function LoginScreen({ onConnect, error, isConnecting }: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-dvh flex items-center justify-center bg-[var(--pc-bg-base)] text-zinc-300 bg-[radial-gradient(ellipse_at_top,rgba(255,255,255,0.02),transparent_50%),radial-gradient(ellipse_at_bottom_right,rgba(99,102,241,0.04),transparent_50%)]">
|
<div className="h-dvh flex items-center justify-center bg-[var(--pc-bg-base)] text-pc-text bg-[radial-gradient(ellipse_at_top,rgba(255,255,255,0.02),transparent_50%),radial-gradient(ellipse_at_bottom_right,rgba(99,102,241,0.04),transparent_50%)]">
|
||||||
<div className="w-full max-w-md mx-4">
|
<div className="w-full max-w-md mx-4">
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<div className="flex flex-col items-center gap-3 mb-8">
|
<div className="flex flex-col items-center gap-3 mb-8">
|
||||||
<img src="/logo.png" alt="PinchChat" className="h-20 w-20 drop-shadow-lg" />
|
<img src="/logo.png" alt="PinchChat" className="h-20 w-20 drop-shadow-lg" />
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h1 className="text-2xl font-bold text-zinc-200 tracking-wide">{t('login.title')}</h1>
|
<h1 className="text-2xl font-bold text-pc-text tracking-wide">{t('login.title')}</h1>
|
||||||
<Sparkles className="h-5 w-5 text-cyan-300/60" />
|
<Sparkles className="h-5 w-5 text-pc-accent-light/60" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-zinc-500">{t('login.subtitle')}</p>
|
<p className="text-sm text-pc-text-muted">{t('login.subtitle')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Form */}
|
{/* Form */}
|
||||||
<form onSubmit={handleSubmit} className="rounded-2xl border border-white/8 bg-[var(--pc-bg-surface)]/80 backdrop-blur-xl p-6 space-y-5 shadow-2xl shadow-black/30">
|
<form onSubmit={handleSubmit} className="rounded-2xl border border-pc-border bg-[var(--pc-bg-surface)]/80 backdrop-blur-xl p-6 space-y-5 shadow-2xl shadow-black/30">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label htmlFor="gateway-url" className="block text-xs font-medium text-zinc-400 uppercase tracking-wider">
|
<label htmlFor="gateway-url" className="block text-xs font-medium text-pc-text-secondary uppercase tracking-wider">
|
||||||
{t('login.gatewayUrl')}
|
{t('login.gatewayUrl')}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -61,7 +61,7 @@ export function LoginScreen({ onConnect, error, isConnecting }: Props) {
|
|||||||
value={url}
|
value={url}
|
||||||
onChange={e => setUrl(e.target.value)}
|
onChange={e => setUrl(e.target.value)}
|
||||||
placeholder="ws://192.168.1.14:18789"
|
placeholder="ws://192.168.1.14:18789"
|
||||||
className="w-full rounded-xl border border-white/8 bg-zinc-800/50 px-4 py-3 text-sm text-zinc-200 placeholder:text-zinc-600 outline-none focus:border-cyan-400/40 focus:ring-1 focus:ring-cyan-400/20 transition-all"
|
className="w-full rounded-xl border border-pc-border bg-pc-elevated/50 px-4 py-3 text-sm text-pc-text placeholder:text-pc-text-faint outline-none focus:border-[var(--pc-accent-dim)] focus:ring-1 focus:ring-[var(--pc-accent-glow)] transition-all"
|
||||||
autoComplete="url"
|
autoComplete="url"
|
||||||
disabled={isConnecting}
|
disabled={isConnecting}
|
||||||
/>
|
/>
|
||||||
@@ -73,7 +73,7 @@ export function LoginScreen({ onConnect, error, isConnecting }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label htmlFor="gateway-token" className="block text-xs font-medium text-zinc-400 uppercase tracking-wider">
|
<label htmlFor="gateway-token" className="block text-xs font-medium text-pc-text-secondary uppercase tracking-wider">
|
||||||
{t('login.token')}
|
{t('login.token')}
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@@ -83,14 +83,14 @@ export function LoginScreen({ onConnect, error, isConnecting }: Props) {
|
|||||||
value={token}
|
value={token}
|
||||||
onChange={e => setToken(e.target.value)}
|
onChange={e => setToken(e.target.value)}
|
||||||
placeholder={t('login.tokenPlaceholder')}
|
placeholder={t('login.tokenPlaceholder')}
|
||||||
className="w-full rounded-xl border border-white/8 bg-zinc-800/50 px-4 py-3 pr-12 text-sm text-zinc-200 placeholder:text-zinc-600 outline-none focus:border-cyan-400/40 focus:ring-1 focus:ring-cyan-400/20 transition-all"
|
className="w-full rounded-xl border border-pc-border bg-pc-elevated/50 px-4 py-3 pr-12 text-sm text-pc-text placeholder:text-pc-text-faint outline-none focus:border-[var(--pc-accent-dim)] focus:ring-1 focus:ring-[var(--pc-accent-glow)] transition-all"
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
disabled={isConnecting}
|
disabled={isConnecting}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowToken(!showToken)}
|
onClick={() => setShowToken(!showToken)}
|
||||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-zinc-500 hover:text-zinc-300 transition-colors"
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-pc-text-muted hover:text-pc-text transition-colors"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
aria-label={showToken ? t('login.hideToken') : t('login.showToken')}
|
aria-label={showToken ? t('login.hideToken') : t('login.showToken')}
|
||||||
>
|
>
|
||||||
@@ -121,7 +121,7 @@ export function LoginScreen({ onConnect, error, isConnecting }: Props) {
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p className="text-center text-xs text-zinc-600 mt-6">
|
<p className="text-center text-xs text-pc-text-faint mt-6">
|
||||||
{t('login.storedLocally')}
|
{t('login.storedLocally')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export function SessionIcon({ session, isActive, isCurrentSession }: {
|
|||||||
isCurrentSession?: boolean;
|
isCurrentSession?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const size = 15;
|
const size = 15;
|
||||||
const baseClass = isCurrentSession ? 'text-cyan-300/70' : isActive ? 'text-violet-400/70' : '';
|
const baseClass = isCurrentSession ? 'text-pc-accent-light/70' : isActive ? 'text-violet-400/70' : '';
|
||||||
|
|
||||||
// Detect cron sessions from key pattern
|
// Detect cron sessions from key pattern
|
||||||
const isCron = session.key.includes(':cron:');
|
const isCron = session.key.includes(':cron:');
|
||||||
|
|||||||
@@ -142,18 +142,18 @@ export function Sidebar({ sessions, activeSession, onSwitch, onDelete, open, onC
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{open && <div className="fixed inset-0 bg-black/60 backdrop-blur-sm z-40 lg:hidden" onClick={onClose} />}
|
{open && <div className="fixed inset-0 bg-black/60 backdrop-blur-sm z-40 lg:hidden" onClick={onClose} />}
|
||||||
<aside role="navigation" aria-label="Sessions" className={`fixed lg:relative top-0 left-0 h-full bg-[var(--pc-bg-base)]/95 border-r border-white/8 z-50 transform ${dragging ? '' : 'transition-transform'} lg:translate-x-0 ${open ? 'translate-x-0' : '-translate-x-full'} flex flex-col backdrop-blur-xl`} style={{ width: `${width}px` }}>
|
<aside role="navigation" aria-label="Sessions" className={`fixed lg:relative top-0 left-0 h-full bg-[var(--pc-bg-base)]/95 border-r border-pc-border z-50 transform ${dragging ? '' : 'transition-transform'} lg:translate-x-0 ${open ? 'translate-x-0' : '-translate-x-full'} flex flex-col backdrop-blur-xl`} style={{ width: `${width}px` }}>
|
||||||
<div className="h-14 flex items-center justify-between px-4 border-b border-white/8">
|
<div className="h-14 flex items-center justify-between px-4 border-b border-pc-border">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="absolute -inset-1.5 rounded-xl bg-gradient-to-r from-cyan-400/15 to-violet-500/15 blur-lg" />
|
<div className="absolute -inset-1.5 rounded-xl bg-gradient-to-r from-cyan-400/15 to-violet-500/15 blur-lg" />
|
||||||
<div className="relative flex h-8 w-8 items-center justify-center rounded-xl border border-white/8 bg-zinc-800/50">
|
<div className="relative flex h-8 w-8 items-center justify-center rounded-xl border border-pc-border bg-pc-elevated/50">
|
||||||
<Sparkles className="h-4 w-4 text-cyan-200" />
|
<Sparkles className="h-4 w-4 text-pc-accent-light" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-semibold text-sm text-zinc-200 tracking-wide">{t('sidebar.title')}</span>
|
<span className="font-semibold text-sm text-pc-text tracking-wide">{t('sidebar.title')}</span>
|
||||||
</div>
|
</div>
|
||||||
<button onClick={onClose} className="lg:hidden p-1.5 rounded-xl hover:bg-white/5 text-zinc-400 transition-colors">
|
<button onClick={onClose} className="lg:hidden p-1.5 rounded-xl hover:bg-[var(--pc-hover)] text-pc-text-secondary transition-colors">
|
||||||
<X size={16} />
|
<X size={16} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -162,7 +162,7 @@ export function Sidebar({ sessions, activeSession, onSwitch, onDelete, open, onC
|
|||||||
{sessions.length > 3 && (
|
{sessions.length > 3 && (
|
||||||
<div className="px-2 pt-2">
|
<div className="px-2 pt-2">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-500" />
|
<Search size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-pc-text-muted" />
|
||||||
<input
|
<input
|
||||||
ref={searchRef}
|
ref={searchRef}
|
||||||
type="text"
|
type="text"
|
||||||
@@ -170,12 +170,12 @@ export function Sidebar({ sessions, activeSession, onSwitch, onDelete, open, onC
|
|||||||
onChange={e => updateFilter(e.target.value)}
|
onChange={e => updateFilter(e.target.value)}
|
||||||
placeholder={t('sidebar.search')}
|
placeholder={t('sidebar.search')}
|
||||||
aria-label={t('sidebar.search')}
|
aria-label={t('sidebar.search')}
|
||||||
className="w-full pl-8 pr-3 py-1.5 rounded-xl border border-white/8 bg-zinc-800/30 text-xs text-zinc-300 placeholder:text-zinc-500 outline-none focus:ring-1 focus:ring-cyan-400/30 transition-all"
|
className="w-full pl-8 pr-3 py-1.5 rounded-xl border border-pc-border bg-pc-elevated/30 text-xs text-pc-text placeholder:text-pc-text-muted outline-none focus:ring-1 focus:ring-[var(--pc-accent-dim)] transition-all"
|
||||||
/>
|
/>
|
||||||
{filter && (
|
{filter && (
|
||||||
<button
|
<button
|
||||||
onClick={() => updateFilter('')}
|
onClick={() => updateFilter('')}
|
||||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-zinc-500 hover:text-zinc-300"
|
className="absolute right-2 top-1/2 -translate-y-1/2 text-pc-text-muted hover:text-pc-text"
|
||||||
>
|
>
|
||||||
<X size={12} />
|
<X size={12} />
|
||||||
</button>
|
</button>
|
||||||
@@ -214,10 +214,10 @@ export function Sidebar({ sessions, activeSession, onSwitch, onDelete, open, onC
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{sessions.length === 0 && (
|
{sessions.length === 0 && (
|
||||||
<div className="px-3 py-8 text-center text-zinc-500 text-sm">{t('sidebar.empty')}</div>
|
<div className="px-3 py-8 text-center text-pc-text-muted text-sm">{t('sidebar.empty')}</div>
|
||||||
)}
|
)}
|
||||||
{sessions.length > 0 && filtered.length === 0 && (
|
{sessions.length > 0 && filtered.length === 0 && (
|
||||||
<div className="px-3 py-6 text-center text-zinc-500 text-xs">{t('sidebar.noResults')}</div>
|
<div className="px-3 py-6 text-center text-pc-text-muted text-xs">{t('sidebar.noResults')}</div>
|
||||||
)}
|
)}
|
||||||
{filtered.map((s, idx) => {
|
{filtered.map((s, idx) => {
|
||||||
const isActive = s.key === activeSession;
|
const isActive = s.key === activeSession;
|
||||||
@@ -228,7 +228,7 @@ export function Sidebar({ sessions, activeSession, onSwitch, onDelete, open, onC
|
|||||||
<div key={s.key}>
|
<div key={s.key}>
|
||||||
{isFirstUnpinned && (
|
{isFirstUnpinned && (
|
||||||
<div className="flex items-center gap-2 px-3 py-1.5 mt-1 mb-1">
|
<div className="flex items-center gap-2 px-3 py-1.5 mt-1 mb-1">
|
||||||
<div className="flex-1 h-px bg-white/5" />
|
<div className="flex-1 h-px bg-[var(--pc-hover)]" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
@@ -238,11 +238,11 @@ export function Sidebar({ sessions, activeSession, onSwitch, onDelete, open, onC
|
|||||||
onMouseEnter={() => setFocusIdx(idx)}
|
onMouseEnter={() => setFocusIdx(idx)}
|
||||||
className={`group/item w-full flex items-center gap-3 px-3 py-2.5 rounded-2xl text-left text-sm transition-all mb-1 ${
|
className={`group/item w-full flex items-center gap-3 px-3 py-2.5 rounded-2xl text-left text-sm transition-all mb-1 ${
|
||||||
isActive
|
isActive
|
||||||
? 'bg-white/5 text-cyan-200 border border-white/8 shadow-[0_0_12px_rgba(34,211,238,0.08)]'
|
? 'bg-[var(--pc-hover)] text-pc-accent-light border border-pc-border shadow-[0_0_12px_rgba(34,211,238,0.08)]'
|
||||||
: s.isActive
|
: s.isActive
|
||||||
? 'bg-violet-500/5 text-violet-200 border border-violet-500/15 shadow-[0_0_10px_rgba(168,85,247,0.06)]'
|
? 'bg-violet-500/5 text-violet-200 border border-violet-500/15 shadow-[0_0_10px_rgba(168,85,247,0.06)]'
|
||||||
: 'text-zinc-400 hover:bg-white/5 border border-transparent'
|
: 'text-pc-text-secondary hover:bg-[var(--pc-hover)] border border-transparent'
|
||||||
} ${isFocused && !isActive ? 'ring-1 ring-cyan-400/30' : ''}`}
|
} ${isFocused && !isActive ? 'ring-1 ring-[var(--pc-accent-dim)]' : ''}`}
|
||||||
>
|
>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<SessionIcon session={s} isActive={s.isActive} isCurrentSession={isActive} />
|
<SessionIcon session={s} isActive={s.isActive} isCurrentSession={isActive} />
|
||||||
@@ -250,7 +250,7 @@ export function Sidebar({ sessions, activeSession, onSwitch, onDelete, open, onC
|
|||||||
<span className="absolute -top-0.5 -right-0.5 h-2 w-2 rounded-full bg-violet-400 shadow-[0_0_8px_rgba(168,85,247,0.7)] animate-pulse" />
|
<span className="absolute -top-0.5 -right-0.5 h-2 w-2 rounded-full bg-violet-400 shadow-[0_0_8px_rgba(168,85,247,0.7)] animate-pulse" />
|
||||||
)}
|
)}
|
||||||
{s.hasUnread && !isActive && (
|
{s.hasUnread && !isActive && (
|
||||||
<span className="absolute -top-0.5 -left-0.5 h-2 w-2 rounded-full bg-cyan-400 shadow-[0_0_8px_rgba(34,211,238,0.7)]" />
|
<span className="absolute -top-0.5 -left-0.5 h-2 w-2 rounded-full bg-[var(--pc-accent)] shadow-[0_0_8px_rgba(34,211,238,0.7)]" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
@@ -258,14 +258,14 @@ export function Sidebar({ sessions, activeSession, onSwitch, onDelete, open, onC
|
|||||||
<span className="flex-1 truncate">{sessionDisplayName(s)}</span>
|
<span className="flex-1 truncate">{sessionDisplayName(s)}</span>
|
||||||
{(() => {
|
{(() => {
|
||||||
const rel = relativeTime(s.updatedAt);
|
const rel = relativeTime(s.updatedAt);
|
||||||
return rel ? <span className="text-[10px] text-zinc-500 tabular-nums shrink-0">{rel}</span> : null;
|
return rel ? <span className="text-[10px] text-pc-text-muted tabular-nums shrink-0">{rel}</span> : null;
|
||||||
})()}
|
})()}
|
||||||
<button
|
<button
|
||||||
onClick={(e) => togglePin(s.key, e)}
|
onClick={(e) => togglePin(s.key, e)}
|
||||||
className={`shrink-0 p-0.5 rounded-lg transition-all ${
|
className={`shrink-0 p-0.5 rounded-lg transition-all ${
|
||||||
isPinned
|
isPinned
|
||||||
? 'text-cyan-400 opacity-80 hover:opacity-100'
|
? 'text-pc-accent opacity-80 hover:opacity-100'
|
||||||
: 'text-zinc-600 opacity-0 group-hover/item:opacity-60 hover:!opacity-100 hover:text-zinc-400'
|
: 'text-pc-text-faint opacity-0 group-hover/item:opacity-60 hover:!opacity-100 hover:text-pc-text-secondary'
|
||||||
}`}
|
}`}
|
||||||
title={isPinned ? t('sidebar.unpin') : t('sidebar.pin')}
|
title={isPinned ? t('sidebar.unpin') : t('sidebar.pin')}
|
||||||
aria-label={isPinned ? t('sidebar.unpin') : t('sidebar.pin')}
|
aria-label={isPinned ? t('sidebar.unpin') : t('sidebar.pin')}
|
||||||
@@ -274,20 +274,20 @@ export function Sidebar({ sessions, activeSession, onSwitch, onDelete, open, onC
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); setConfirmDelete(s.key); }}
|
onClick={(e) => { e.stopPropagation(); setConfirmDelete(s.key); }}
|
||||||
className="shrink-0 p-0.5 rounded-lg transition-all text-zinc-600 opacity-0 group-hover/item:opacity-60 hover:!opacity-100 hover:text-red-400"
|
className="shrink-0 p-0.5 rounded-lg transition-all text-pc-text-faint opacity-0 group-hover/item:opacity-60 hover:!opacity-100 hover:text-red-400"
|
||||||
title={t('sidebar.delete')}
|
title={t('sidebar.delete')}
|
||||||
aria-label={t('sidebar.delete')}
|
aria-label={t('sidebar.delete')}
|
||||||
>
|
>
|
||||||
<Trash2 size={12} />
|
<Trash2 size={12} />
|
||||||
</button>
|
</button>
|
||||||
{s.messageCount != null && (
|
{s.messageCount != null && (
|
||||||
<span className={`text-[11px] px-2 py-0.5 rounded-full shrink-0 ${isActive ? 'bg-cyan-400/10 text-cyan-300' : 'bg-white/5 text-zinc-500'}`}>
|
<span className={`text-[11px] px-2 py-0.5 rounded-full shrink-0 ${isActive ? 'bg-[var(--pc-accent-glow)] text-pc-accent-light' : 'bg-[var(--pc-hover)] text-pc-text-muted'}`}>
|
||||||
{s.messageCount}
|
{s.messageCount}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{s.lastMessagePreview && (
|
{s.lastMessagePreview && (
|
||||||
<p className="text-[11px] text-zinc-500 truncate mt-0.5 leading-tight">{s.lastMessagePreview.replace(/\s+/g, ' ').slice(0, 80)}</p>
|
<p className="text-[11px] text-pc-text-muted truncate mt-0.5 leading-tight">{s.lastMessagePreview.replace(/\s+/g, ' ').slice(0, 80)}</p>
|
||||||
)}
|
)}
|
||||||
{(() => {
|
{(() => {
|
||||||
if (!s.contextTokens) return null;
|
if (!s.contextTokens) return null;
|
||||||
@@ -296,10 +296,10 @@ export function Sidebar({ sessions, activeSession, onSwitch, onDelete, open, onC
|
|||||||
const barStyle = { width: `${pct}%`, backgroundColor: `rgba(56, 189, 248, ${barOpacity})` };
|
const barStyle = { width: `${pct}%`, backgroundColor: `rgba(56, 189, 248, ${barOpacity})` };
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1.5 mt-1">
|
<div className="flex items-center gap-1.5 mt-1">
|
||||||
<div className="flex-1 h-[3px] rounded-full bg-white/5 overflow-hidden">
|
<div className="flex-1 h-[3px] rounded-full bg-[var(--pc-hover)] overflow-hidden">
|
||||||
<div className="h-full rounded-full" style={barStyle} />
|
<div className="h-full rounded-full" style={barStyle} />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[9px] text-zinc-500 tabular-nums shrink-0">{Math.round(pct)}%</span>
|
<span className="text-[9px] text-pc-text-muted tabular-nums shrink-0">{Math.round(pct)}%</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
@@ -310,17 +310,17 @@ export function Sidebar({ sessions, activeSession, onSwitch, onDelete, open, onC
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
{/* Footer with version */}
|
{/* Footer with version */}
|
||||||
<div className="px-4 py-3 border-t border-white/8 flex items-center justify-center gap-2">
|
<div className="px-4 py-3 border-t border-pc-border flex items-center justify-center gap-2">
|
||||||
<span className="h-1.5 w-1.5 rounded-full bg-violet-300/60 shadow-[0_0_10px_rgba(168,85,247,0.5)]" />
|
<span className="h-1.5 w-1.5 rounded-full bg-violet-300/60 shadow-[0_0_10px_rgba(168,85,247,0.5)]" />
|
||||||
<span className="h-1.5 w-1.5 rounded-full bg-cyan-300/60 shadow-[0_0_10px_rgba(34,211,238,0.5)]" />
|
<span className="h-1.5 w-1.5 rounded-full bg-[var(--pc-accent-dim)] shadow-[0_0_10px_rgba(34,211,238,0.5)]" />
|
||||||
<span className="h-1.5 w-1.5 rounded-full bg-indigo-300/50 shadow-[0_0_10px_rgba(99,102,241,0.4)]" />
|
<span className="h-1.5 w-1.5 rounded-full bg-indigo-300/50 shadow-[0_0_10px_rgba(99,102,241,0.4)]" />
|
||||||
<span className="ml-1 text-[9px] text-zinc-600 select-all" title={`PinchChat v${__APP_VERSION__}`}>v{__APP_VERSION__}</span>
|
<span className="ml-1 text-[9px] text-pc-text-faint select-all" title={`PinchChat v${__APP_VERSION__}`}>v{__APP_VERSION__}</span>
|
||||||
</div>
|
</div>
|
||||||
{/* Resize drag handle */}
|
{/* Resize drag handle */}
|
||||||
<div
|
<div
|
||||||
onMouseDown={startDrag}
|
onMouseDown={startDrag}
|
||||||
onTouchStart={startDrag}
|
onTouchStart={startDrag}
|
||||||
className={`hidden lg:block absolute top-0 right-0 w-1.5 h-full cursor-col-resize group/resize z-10 ${dragging ? 'bg-cyan-400/20' : 'hover:bg-cyan-400/15'} transition-colors`}
|
className={`hidden lg:block absolute top-0 right-0 w-1.5 h-full cursor-col-resize group/resize z-10 ${dragging ? 'bg-[var(--pc-accent-glow)]' : 'hover:bg-[var(--pc-accent-glow)]'} transition-colors`}
|
||||||
role="separator"
|
role="separator"
|
||||||
aria-orientation="vertical"
|
aria-orientation="vertical"
|
||||||
aria-label="Resize sidebar"
|
aria-label="Resize sidebar"
|
||||||
@@ -328,7 +328,7 @@ export function Sidebar({ sessions, activeSession, onSwitch, onDelete, open, onC
|
|||||||
aria-valuemin={MIN_WIDTH}
|
aria-valuemin={MIN_WIDTH}
|
||||||
aria-valuemax={MAX_WIDTH}
|
aria-valuemax={MAX_WIDTH}
|
||||||
>
|
>
|
||||||
<div className={`absolute top-1/2 -translate-y-1/2 right-0 w-0.5 h-8 rounded-full ${dragging ? 'bg-cyan-400/50' : 'bg-white/0 group-hover/resize:bg-cyan-400/30'} transition-colors`} />
|
<div className={`absolute top-1/2 -translate-y-1/2 right-0 w-0.5 h-8 rounded-full ${dragging ? 'bg-[var(--pc-accent-dim)]' : 'bg-transparent group-hover/resize:bg-[var(--pc-accent-dim)]'} transition-colors`} />
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
{/* Prevent text selection while dragging */}
|
{/* Prevent text selection while dragging */}
|
||||||
@@ -337,12 +337,12 @@ export function Sidebar({ sessions, activeSession, onSwitch, onDelete, open, onC
|
|||||||
{confirmDelete && (
|
{confirmDelete && (
|
||||||
<>
|
<>
|
||||||
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm z-[70]" onClick={() => setConfirmDelete(null)} />
|
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm z-[70]" onClick={() => setConfirmDelete(null)} />
|
||||||
<div className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-[80] w-72 bg-[var(--pc-bg-base)] border border-white/10 rounded-2xl p-5 shadow-2xl">
|
<div className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-[80] w-72 bg-[var(--pc-bg-base)] border border-pc-border-strong rounded-2xl p-5 shadow-2xl">
|
||||||
<p className="text-sm text-zinc-300 mb-4">{t('sidebar.deleteConfirm')}</p>
|
<p className="text-sm text-pc-text mb-4">{t('sidebar.deleteConfirm')}</p>
|
||||||
<div className="flex gap-2 justify-end">
|
<div className="flex gap-2 justify-end">
|
||||||
<button
|
<button
|
||||||
onClick={() => setConfirmDelete(null)}
|
onClick={() => setConfirmDelete(null)}
|
||||||
className="px-3 py-1.5 text-xs rounded-xl border border-white/10 text-zinc-400 hover:bg-white/5 transition-colors"
|
className="px-3 py-1.5 text-xs rounded-xl border border-pc-border-strong text-pc-text-secondary hover:bg-[var(--pc-hover)] transition-colors"
|
||||||
>
|
>
|
||||||
{t('sidebar.deleteCancel')}
|
{t('sidebar.deleteCancel')}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ export function ThinkingBlock({ text }: { text: string }) {
|
|||||||
<div className="my-2">
|
<div className="my-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => setOpen(!open)}
|
onClick={() => setOpen(!open)}
|
||||||
className="inline-flex items-center gap-1.5 rounded-2xl border border-white/8 bg-zinc-800/35 px-3 py-1.5 text-xs text-violet-300 hover:bg-white/5 transition-colors"
|
className="inline-flex items-center gap-1.5 rounded-2xl border border-pc-border bg-pc-elevated/35 px-3 py-1.5 text-xs text-violet-300 hover:bg-[var(--pc-hover)] transition-colors"
|
||||||
>
|
>
|
||||||
<Brain size={13} />
|
<Brain size={13} />
|
||||||
<span className="font-medium">{t('thinking.label')}</span>
|
<span className="font-medium">{t('thinking.label')}</span>
|
||||||
{open ? <ChevronDown size={12} className="ml-1 text-zinc-500" /> : <ChevronRight size={12} className="ml-1 text-zinc-500" />}
|
{open ? <ChevronDown size={12} className="ml-1 text-pc-text-muted" /> : <ChevronRight size={12} className="ml-1 text-pc-text-muted" />}
|
||||||
</button>
|
</button>
|
||||||
{open && (
|
{open && (
|
||||||
<div className="mt-2 rounded-2xl border border-white/8 bg-zinc-800/25 p-3 text-sm italic text-zinc-400 whitespace-pre-wrap max-h-96 overflow-y-auto">
|
<div className="mt-2 rounded-2xl border border-pc-border bg-pc-elevated/25 p-3 text-sm italic text-pc-text-secondary whitespace-pre-wrap max-h-96 overflow-y-auto">
|
||||||
{text}
|
{text}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const toolColors: Record<string, ToolColor> = {
|
|||||||
write: { border: 'border-violet-500/30', bg: 'bg-violet-500/10', text: 'text-violet-300', icon: 'text-violet-400', glow: 'shadow-[0_0_8px_rgba(139,92,246,0.15)]', expandBorder: 'border-violet-500/20', expandBg: 'bg-violet-950/20' },
|
write: { border: 'border-violet-500/30', bg: 'bg-violet-500/10', text: 'text-violet-300', icon: 'text-violet-400', glow: 'shadow-[0_0_8px_rgba(139,92,246,0.15)]', expandBorder: 'border-violet-500/20', expandBg: 'bg-violet-950/20' },
|
||||||
Edit: { border: 'border-violet-500/30', bg: 'bg-violet-500/10', text: 'text-violet-300', icon: 'text-violet-400', glow: 'shadow-[0_0_8px_rgba(139,92,246,0.15)]', expandBorder: 'border-violet-500/20', expandBg: 'bg-violet-950/20' },
|
Edit: { border: 'border-violet-500/30', bg: 'bg-violet-500/10', text: 'text-violet-300', icon: 'text-violet-400', glow: 'shadow-[0_0_8px_rgba(139,92,246,0.15)]', expandBorder: 'border-violet-500/20', expandBg: 'bg-violet-950/20' },
|
||||||
edit: { border: 'border-violet-500/30', bg: 'bg-violet-500/10', text: 'text-violet-300', icon: 'text-violet-400', glow: 'shadow-[0_0_8px_rgba(139,92,246,0.15)]', expandBorder: 'border-violet-500/20', expandBg: 'bg-violet-950/20' },
|
edit: { border: 'border-violet-500/30', bg: 'bg-violet-500/10', text: 'text-violet-300', icon: 'text-violet-400', glow: 'shadow-[0_0_8px_rgba(139,92,246,0.15)]', expandBorder: 'border-violet-500/20', expandBg: 'bg-violet-950/20' },
|
||||||
browser: { border: 'border-cyan-500/30', bg: 'bg-cyan-500/10', text: 'text-cyan-300', icon: 'text-cyan-400', glow: 'shadow-[0_0_8px_rgba(6,182,212,0.15)]', expandBorder: 'border-cyan-500/20', expandBg: 'bg-cyan-950/20' },
|
browser: { border: 'border-cyan-500/30', bg: 'bg-cyan-500/10', text: 'text-pc-accent-light', icon: 'text-pc-accent', glow: 'shadow-[0_0_8px_rgba(6,182,212,0.15)]', expandBorder: 'border-cyan-500/20', expandBg: 'bg-cyan-950/20' },
|
||||||
image: { border: 'border-pink-500/30', bg: 'bg-pink-500/10', text: 'text-pink-300', icon: 'text-pink-400', glow: 'shadow-[0_0_8px_rgba(236,72,153,0.15)]', expandBorder: 'border-pink-500/20', expandBg: 'bg-pink-950/20' },
|
image: { border: 'border-pink-500/30', bg: 'bg-pink-500/10', text: 'text-pink-300', icon: 'text-pink-400', glow: 'shadow-[0_0_8px_rgba(236,72,153,0.15)]', expandBorder: 'border-pink-500/20', expandBg: 'bg-pink-950/20' },
|
||||||
message: { border: 'border-indigo-500/30', bg: 'bg-indigo-500/10', text: 'text-indigo-300', icon: 'text-indigo-400', glow: 'shadow-[0_0_8px_rgba(99,102,241,0.15)]', expandBorder: 'border-indigo-500/20', expandBg: 'bg-indigo-950/20' },
|
message: { border: 'border-indigo-500/30', bg: 'bg-indigo-500/10', text: 'text-indigo-300', icon: 'text-indigo-400', glow: 'shadow-[0_0_8px_rgba(99,102,241,0.15)]', expandBorder: 'border-indigo-500/20', expandBg: 'bg-indigo-950/20' },
|
||||||
memory_search: { border: 'border-rose-500/30', bg: 'bg-rose-500/10', text: 'text-rose-300', icon: 'text-rose-400', glow: 'shadow-[0_0_8px_rgba(244,63,94,0.15)]', expandBorder: 'border-rose-500/20', expandBg: 'bg-rose-950/20' },
|
memory_search: { border: 'border-rose-500/30', bg: 'bg-rose-500/10', text: 'text-rose-300', icon: 'text-rose-400', glow: 'shadow-[0_0_8px_rgba(244,63,94,0.15)]', expandBorder: 'border-rose-500/20', expandBg: 'bg-rose-950/20' },
|
||||||
@@ -26,7 +26,7 @@ const toolColors: Record<string, ToolColor> = {
|
|||||||
sessions_spawn: { border: 'border-teal-500/30', bg: 'bg-teal-500/10', text: 'text-teal-300', icon: 'text-teal-400', glow: 'shadow-[0_0_8px_rgba(20,184,166,0.15)]', expandBorder: 'border-teal-500/20', expandBg: 'bg-teal-950/20' },
|
sessions_spawn: { border: 'border-teal-500/30', bg: 'bg-teal-500/10', text: 'text-teal-300', icon: 'text-teal-400', glow: 'shadow-[0_0_8px_rgba(20,184,166,0.15)]', expandBorder: 'border-teal-500/20', expandBg: 'bg-teal-950/20' },
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultColor: ToolColor = { border: 'border-zinc-500/30', bg: 'bg-zinc-500/10', text: 'text-zinc-300', icon: 'text-zinc-400', glow: 'shadow-[0_0_8px_rgba(161,161,170,0.1)]', expandBorder: 'border-zinc-500/20', expandBg: 'bg-zinc-800/25' };
|
const defaultColor: ToolColor = { border: 'border-pc-border-strong', bg: 'bg-pc-elevated/10', text: 'text-pc-text', icon: 'text-pc-text-secondary', glow: 'shadow-[0_0_8px_rgba(161,161,170,0.1)]', expandBorder: 'border-pc-border', expandBg: 'bg-pc-elevated/25' };
|
||||||
|
|
||||||
function getColor(name: string): ToolColor {
|
function getColor(name: string): ToolColor {
|
||||||
return toolColors[name] || defaultColor;
|
return toolColors[name] || defaultColor;
|
||||||
@@ -115,7 +115,7 @@ function WrapToggle({ wrap, onToggle }: { wrap: boolean; onToggle: () => void })
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={onToggle}
|
onClick={onToggle}
|
||||||
className="absolute top-2 right-8 p-1 rounded-lg bg-zinc-700/60 hover:bg-zinc-600/80 border border-white/10 text-zinc-400 hover:text-zinc-200 opacity-0 group-hover/tc-block:opacity-100 transition-opacity duration-150"
|
className="absolute top-2 right-8 p-1 rounded-lg bg-pc-elevated/60 hover:bg-pc-elevated/80 border border-pc-border-strong text-pc-text-secondary hover:text-pc-text opacity-0 group-hover/tc-block:opacity-100 transition-opacity duration-150"
|
||||||
title={wrap ? 'No wrap' : 'Word wrap'}
|
title={wrap ? 'No wrap' : 'Word wrap'}
|
||||||
aria-label="Toggle word wrap"
|
aria-label="Toggle word wrap"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -138,7 +138,7 @@ function CopyButton({ text }: { text: string }) {
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={handleCopy}
|
onClick={handleCopy}
|
||||||
className="absolute top-2 right-2 p-1 rounded-lg bg-zinc-700/60 hover:bg-zinc-600/80 border border-white/10 text-zinc-400 hover:text-zinc-200 opacity-0 group-hover/tc-block:opacity-100 transition-opacity duration-150"
|
className="absolute top-2 right-2 p-1 rounded-lg bg-pc-elevated/60 hover:bg-pc-elevated/80 border border-pc-border-strong text-pc-text-secondary hover:text-pc-text opacity-0 group-hover/tc-block:opacity-100 transition-opacity duration-150"
|
||||||
title={copied ? 'Copied!' : 'Copy'}
|
title={copied ? 'Copied!' : 'Copy'}
|
||||||
aria-label="Copy to clipboard"
|
aria-label="Copy to clipboard"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -266,7 +266,7 @@ export function ToolCall({ name, input, result }: { name: string; input?: Record
|
|||||||
|
|
||||||
{/* Result summary (always visible if result exists) */}
|
{/* Result summary (always visible if result exists) */}
|
||||||
{result && !open && (
|
{result && !open && (
|
||||||
<div className="mt-1 text-[11px] text-zinc-400 pl-2 truncate max-w-full">
|
<div className="mt-1 text-[11px] text-pc-text-secondary pl-2 truncate max-w-full">
|
||||||
{truncateResult(result)}
|
{truncateResult(result)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -280,7 +280,7 @@ export function ToolCall({ name, input, result }: { name: string; input?: Record
|
|||||||
<div className="group/tc-block relative">
|
<div className="group/tc-block relative">
|
||||||
<HighlightedPre
|
<HighlightedPre
|
||||||
text={inputStr}
|
text={inputStr}
|
||||||
className="text-xs bg-[var(--pc-bg-input)]/60 border border-white/5 p-2.5 rounded-xl overflow-x-auto text-zinc-300 font-mono"
|
className="text-xs bg-[var(--pc-bg-input)]/60 border border-pc-border p-2.5 rounded-xl overflow-x-auto text-pc-text font-mono"
|
||||||
wrap={wrap}
|
wrap={wrap}
|
||||||
/>
|
/>
|
||||||
<WrapToggle wrap={wrap} onToggle={() => setWrap(!wrap)} />
|
<WrapToggle wrap={wrap} onToggle={() => setWrap(!wrap)} />
|
||||||
@@ -299,7 +299,7 @@ export function ToolCall({ name, input, result }: { name: string; input?: Record
|
|||||||
<div className="group/tc-block relative">
|
<div className="group/tc-block relative">
|
||||||
<HighlightedPre
|
<HighlightedPre
|
||||||
text={imageData.remaining}
|
text={imageData.remaining}
|
||||||
className="text-xs bg-[var(--pc-bg-input)]/60 border border-white/5 p-2.5 rounded-xl overflow-x-auto text-zinc-300 font-mono mb-2"
|
className="text-xs bg-[var(--pc-bg-input)]/60 border border-pc-border p-2.5 rounded-xl overflow-x-auto text-pc-text font-mono mb-2"
|
||||||
wrap={wrap}
|
wrap={wrap}
|
||||||
/>
|
/>
|
||||||
<WrapToggle wrap={wrap} onToggle={() => setWrap(!wrap)} />
|
<WrapToggle wrap={wrap} onToggle={() => setWrap(!wrap)} />
|
||||||
@@ -312,7 +312,7 @@ export function ToolCall({ name, input, result }: { name: string; input?: Record
|
|||||||
<div className="group/tc-block relative">
|
<div className="group/tc-block relative">
|
||||||
<HighlightedPre
|
<HighlightedPre
|
||||||
text={result}
|
text={result}
|
||||||
className="text-xs bg-[var(--pc-bg-input)]/60 border border-white/5 p-2.5 rounded-xl overflow-x-auto text-zinc-300 max-h-64 overflow-y-auto font-mono"
|
className="text-xs bg-[var(--pc-bg-input)]/60 border border-pc-border p-2.5 rounded-xl overflow-x-auto text-pc-text max-h-64 overflow-y-auto font-mono"
|
||||||
wrap={wrap}
|
wrap={wrap}
|
||||||
/>
|
/>
|
||||||
<WrapToggle wrap={wrap} onToggle={() => setWrap(!wrap)} />
|
<WrapToggle wrap={wrap} onToggle={() => setWrap(!wrap)} />
|
||||||
|
|||||||
@@ -24,17 +24,17 @@ export function TypingIndicator() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="animate-fade-in flex items-start gap-3 px-4 py-3">
|
<div className="animate-fade-in flex items-start gap-3 px-4 py-3">
|
||||||
<div className="shrink-0 flex h-9 w-9 items-center justify-center rounded-2xl border border-white/10 bg-zinc-900/60">
|
<div className="shrink-0 flex h-9 w-9 items-center justify-center rounded-2xl border border-pc-border-strong bg-pc-input/60">
|
||||||
<Bot className="h-4 w-4 text-cyan-200" />
|
<Bot className="h-4 w-4 text-pc-accent-light" />
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-3xl border border-white/10 bg-zinc-900/55 px-4 py-3 shadow-[0_0_0_1px_rgba(255,255,255,0.03)]">
|
<div className="rounded-3xl border border-pc-border-strong bg-pc-input/55 px-4 py-3 shadow-[0_0_0_1px_rgba(255,255,255,0.03)]">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<span className="bounce-dot h-2 w-2 rounded-full bg-gradient-to-r from-cyan-300/80 to-violet-400/80" />
|
<span className="bounce-dot h-2 w-2 rounded-full bg-gradient-to-r from-cyan-300/80 to-violet-400/80" />
|
||||||
<span className="bounce-dot h-2 w-2 rounded-full bg-gradient-to-r from-cyan-300/80 to-violet-400/80" />
|
<span className="bounce-dot h-2 w-2 rounded-full bg-gradient-to-r from-cyan-300/80 to-violet-400/80" />
|
||||||
<span className="bounce-dot h-2 w-2 rounded-full bg-gradient-to-r from-cyan-300/80 to-violet-400/80" />
|
<span className="bounce-dot h-2 w-2 rounded-full bg-gradient-to-r from-cyan-300/80 to-violet-400/80" />
|
||||||
<span className="ml-2 text-xs text-zinc-400">{t('chat.thinking')}</span>
|
<span className="ml-2 text-xs text-pc-text-secondary">{t('chat.thinking')}</span>
|
||||||
{elapsed >= 2 && (
|
{elapsed >= 2 && (
|
||||||
<span className="text-[10px] text-zinc-500 tabular-nums ml-1">{formatElapsed(elapsed)}</span>
|
<span className="text-[10px] text-pc-text-muted tabular-nums ml-1">{formatElapsed(elapsed)}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ const themes: Record<ThemeName, Record<string, string>> = {
|
|||||||
'--pc-scrollbar-thumb-hover': '#71717a',
|
'--pc-scrollbar-thumb-hover': '#71717a',
|
||||||
'--pc-user-bubble': 'rgba(34,211,238,0.06)',
|
'--pc-user-bubble': 'rgba(34,211,238,0.06)',
|
||||||
'--pc-user-border': 'rgba(34,211,238,0.15)',
|
'--pc-user-border': 'rgba(34,211,238,0.15)',
|
||||||
|
'--pc-hover': 'rgba(255,255,255,0.05)',
|
||||||
|
'--pc-hover-strong': 'rgba(255,255,255,0.08)',
|
||||||
|
'--pc-separator': 'rgba(255,255,255,0.05)',
|
||||||
},
|
},
|
||||||
light: {
|
light: {
|
||||||
'--pc-bg-base': '#f4f4f5',
|
'--pc-bg-base': '#f4f4f5',
|
||||||
@@ -48,6 +51,9 @@ const themes: Record<ThemeName, Record<string, string>> = {
|
|||||||
'--pc-scrollbar-thumb-hover': '#71717a',
|
'--pc-scrollbar-thumb-hover': '#71717a',
|
||||||
'--pc-user-bubble': 'rgba(34,211,238,0.08)',
|
'--pc-user-bubble': 'rgba(34,211,238,0.08)',
|
||||||
'--pc-user-border': 'rgba(34,211,238,0.25)',
|
'--pc-user-border': 'rgba(34,211,238,0.25)',
|
||||||
|
'--pc-hover': 'rgba(0,0,0,0.05)',
|
||||||
|
'--pc-hover-strong': 'rgba(0,0,0,0.08)',
|
||||||
|
'--pc-separator': 'rgba(0,0,0,0.08)',
|
||||||
},
|
},
|
||||||
oled: {
|
oled: {
|
||||||
'--pc-bg-base': '#000000',
|
'--pc-bg-base': '#000000',
|
||||||
@@ -67,6 +73,9 @@ const themes: Record<ThemeName, Record<string, string>> = {
|
|||||||
'--pc-scrollbar-thumb-hover': '#52525b',
|
'--pc-scrollbar-thumb-hover': '#52525b',
|
||||||
'--pc-user-bubble': 'rgba(34,211,238,0.05)',
|
'--pc-user-bubble': 'rgba(34,211,238,0.05)',
|
||||||
'--pc-user-border': 'rgba(34,211,238,0.12)',
|
'--pc-user-border': 'rgba(34,211,238,0.12)',
|
||||||
|
'--pc-hover': 'rgba(255,255,255,0.04)',
|
||||||
|
'--pc-hover-strong': 'rgba(255,255,255,0.06)',
|
||||||
|
'--pc-separator': 'rgba(255,255,255,0.04)',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,23 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@import "highlight.js/styles/base16/material-palenight.min.css";
|
@import "highlight.js/styles/base16/material-palenight.min.css";
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
/* Theme-aware colors mapped to CSS custom properties */
|
||||||
|
--color-pc-base: var(--pc-bg-base);
|
||||||
|
--color-pc-surface: var(--pc-bg-surface);
|
||||||
|
--color-pc-elevated: var(--pc-bg-elevated);
|
||||||
|
--color-pc-input: var(--pc-bg-input);
|
||||||
|
--color-pc-code: var(--pc-bg-code);
|
||||||
|
--color-pc-border: var(--pc-border);
|
||||||
|
--color-pc-border-strong: var(--pc-border-strong);
|
||||||
|
--color-pc-text: var(--pc-text-primary);
|
||||||
|
--color-pc-text-secondary: var(--pc-text-secondary);
|
||||||
|
--color-pc-text-muted: var(--pc-text-muted);
|
||||||
|
--color-pc-text-faint: var(--pc-text-faint);
|
||||||
|
--color-pc-accent: var(--pc-accent);
|
||||||
|
--color-pc-accent-light: var(--pc-accent-light);
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--pc-bg-base: #1e1e24;
|
--pc-bg-base: #1e1e24;
|
||||||
--pc-bg-surface: #232329;
|
--pc-bg-surface: #232329;
|
||||||
@@ -24,6 +41,9 @@
|
|||||||
--pc-accent-rgb: 34,211,238;
|
--pc-accent-rgb: 34,211,238;
|
||||||
--pc-user-bubble: rgba(34,211,238,0.06);
|
--pc-user-bubble: rgba(34,211,238,0.06);
|
||||||
--pc-user-border: rgba(34,211,238,0.15);
|
--pc-user-border: rgba(34,211,238,0.15);
|
||||||
|
--pc-hover: rgba(255,255,255,0.05);
|
||||||
|
--pc-hover-strong: rgba(255,255,255,0.08);
|
||||||
|
--pc-separator: rgba(255,255,255,0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@@ -61,12 +81,12 @@ textarea::-webkit-scrollbar:horizontal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
textarea::-webkit-scrollbar-thumb {
|
textarea::-webkit-scrollbar-thumb {
|
||||||
background: #3f3f46;
|
background: var(--pc-scrollbar-thumb);
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
textarea::-webkit-scrollbar-thumb:hover {
|
textarea::-webkit-scrollbar-thumb:hover {
|
||||||
background: #52525b;
|
background: var(--pc-scrollbar-thumb-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
@@ -118,7 +138,7 @@ html, body {
|
|||||||
/* Markdown styles */
|
/* Markdown styles */
|
||||||
.markdown-body pre {
|
.markdown-body pre {
|
||||||
background: var(--pc-bg-code) !important;
|
background: var(--pc-bg-code) !important;
|
||||||
border: 1px solid rgba(255,255,255,0.06);
|
border: 1px solid var(--pc-border);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
@@ -136,7 +156,7 @@ html, body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body code {
|
.markdown-body code {
|
||||||
background: rgba(255,255,255,0.07);
|
background: var(--pc-accent-glow);
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
@@ -149,12 +169,12 @@ html, body {
|
|||||||
|
|
||||||
.markdown-body p { margin: 4px 0; }
|
.markdown-body p { margin: 4px 0; }
|
||||||
.markdown-body ul, .markdown-body ol { margin: 4px 0; padding-left: 20px; }
|
.markdown-body ul, .markdown-body ol { margin: 4px 0; padding-left: 20px; }
|
||||||
.markdown-body blockquote { border-left: 3px solid rgba(34,211,238,0.4); padding-left: 12px; margin: 8px 0; opacity: 0.8; }
|
.markdown-body blockquote { border-left: 3px solid var(--pc-accent-dim); padding-left: 12px; margin: 8px 0; opacity: 0.8; }
|
||||||
.markdown-body h1, .markdown-body h2, .markdown-body h3 { margin: 12px 0 4px; }
|
.markdown-body h1, .markdown-body h2, .markdown-body h3 { margin: 12px 0 4px; }
|
||||||
.markdown-body a { color: var(--pc-accent-light); text-decoration: underline; }
|
.markdown-body a { color: var(--pc-accent-light); text-decoration: underline; }
|
||||||
.markdown-body table { border-collapse: collapse; margin: 8px 0; display: block; overflow-x: auto; max-width: 100%; }
|
.markdown-body table { border-collapse: collapse; margin: 8px 0; display: block; overflow-x: auto; max-width: 100%; }
|
||||||
.markdown-body th, .markdown-body td { border: 1px solid rgba(255,255,255,0.08); padding: 6px 12px; }
|
.markdown-body th, .markdown-body td { border: 1px solid var(--pc-border); padding: 6px 12px; }
|
||||||
.markdown-body th { background: rgba(255,255,255,0.04); }
|
.markdown-body th { background: var(--pc-accent-glow); }
|
||||||
.markdown-body img { max-width: 100%; border-radius: 8px; }
|
.markdown-body img { max-width: 100%; border-radius: 8px; }
|
||||||
|
|
||||||
/* Accessibility: respect reduced-motion preferences */
|
/* Accessibility: respect reduced-motion preferences */
|
||||||
|
|||||||
Reference in New Issue
Block a user