feat: PWA install prompt, brand-colored channel icons, image load fix, semantic connection indicator
- Fix image display bug: remove loading='lazy' + use opacity instead of hidden (#66) - PWA: add id/scope to manifest, fix SW cache name, add install prompt hook + button (#64) - Colorize channel icons with brand colors (Discord blurple, Telegram blue, etc.) (#68) - Use semantic green for connection indicator instead of theme accent (#70) - Replace Sparkles icon with PinchChat logo in sidebar (#65) - Replace decorative dots in sidebar footer with GitHub link (#71)
This commit is contained in:
@@ -81,7 +81,7 @@ export function Header({ status, sessionKey, onToggleSidebar, activeSessionData,
|
||||
<span className="hidden sm:contents"><LanguageSelector /></span>
|
||||
{status === 'connected' ? (
|
||||
<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-[var(--pc-accent)] shadow-[0_0_12px_var(--pc-accent-dim)]" />
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-400 shadow-[0_0_12px_rgba(52,211,153,0.4)]" />
|
||||
<span className="text-xs text-pc-text hidden sm:inline">{t('header.connected')}</span>
|
||||
</div>
|
||||
) : status === 'connecting' ? (
|
||||
|
||||
@@ -64,17 +64,18 @@ export function ImageBlock({ src, alt }: ImageBlockProps) {
|
||||
aria-label={`View ${alt || 'image'} full size`}
|
||||
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)]"
|
||||
>
|
||||
{loading && (
|
||||
<div className="w-48 h-32 rounded-xl bg-pc-elevated/50 animate-pulse" />
|
||||
)}
|
||||
<img
|
||||
src={src}
|
||||
alt={alt || 'Image'}
|
||||
className={`max-w-full max-h-80 rounded-xl${loading ? ' hidden' : ''}`}
|
||||
loading="lazy"
|
||||
onLoad={() => setLoading(false)}
|
||||
onError={() => { setLoading(false); setError(true); }}
|
||||
/>
|
||||
<div className="relative">
|
||||
{loading && (
|
||||
<div className="w-48 h-32 rounded-xl bg-pc-elevated/50 animate-pulse" />
|
||||
)}
|
||||
<img
|
||||
src={src}
|
||||
alt={alt || 'Image'}
|
||||
className={`max-w-full max-h-80 rounded-xl transition-opacity duration-200${loading ? ' absolute top-0 left-0 opacity-0 pointer-events-none' : ' opacity-100'}`}
|
||||
onLoad={() => setLoading(false)}
|
||||
onError={() => { setLoading(false); setError(true); }}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{lightbox && <Lightbox src={src} alt={alt} onClose={() => setLightbox(false)} />}
|
||||
|
||||
@@ -46,44 +46,63 @@ function SlackIcon({ size = 15, className = '' }: { size?: number; className?: s
|
||||
* Returns the appropriate icon for a session based on its channel or kind.
|
||||
* Detects cron sessions from the session key pattern.
|
||||
*/
|
||||
export function SessionIcon({ session, isActive, isCurrentSession }: {
|
||||
export function SessionIcon({ session, isCurrentSession }: {
|
||||
session: Session;
|
||||
isActive?: boolean;
|
||||
isActive?: boolean; // kept for API compat, brand colors used instead
|
||||
isCurrentSession?: boolean;
|
||||
}) {
|
||||
const size = 15;
|
||||
const baseClass = isCurrentSession ? 'text-pc-accent-light/70' : isActive ? 'text-violet-400/70' : '';
|
||||
// Brand colors for each channel
|
||||
const brandColor = (() => {
|
||||
const isCron = session.key.includes(':cron:');
|
||||
if (isCron) return 'text-amber-400/80';
|
||||
const isSubAgent = session.key.includes(':spawn:') || session.key.includes(':sub:');
|
||||
if (isSubAgent) return 'text-violet-400/80';
|
||||
const ch = session.channel?.toLowerCase();
|
||||
switch (ch) {
|
||||
case 'discord': return 'text-[#5865F2]';
|
||||
case 'telegram': return 'text-[#26A5E4]';
|
||||
case 'signal': return 'text-[#3A76F0]';
|
||||
case 'whatsapp': return 'text-[#25D366]';
|
||||
case 'slack': return 'text-[#E01E5A]';
|
||||
case 'webchat': return 'text-emerald-400/80';
|
||||
case 'teamspeak': return 'text-[#2580C3]';
|
||||
default: return 'text-pc-text-secondary';
|
||||
}
|
||||
})();
|
||||
|
||||
const colorClass = isCurrentSession ? 'text-pc-accent-light' : brandColor;
|
||||
|
||||
// Detect cron sessions from key pattern
|
||||
const isCron = session.key.includes(':cron:');
|
||||
if (isCron) {
|
||||
return <Clock size={size} className={baseClass} />;
|
||||
return <Clock size={size} className={colorClass} />;
|
||||
}
|
||||
|
||||
// Detect sub-agent / spawned sessions
|
||||
const isSubAgent = session.key.includes(':spawn:') || session.key.includes(':sub:');
|
||||
if (isSubAgent) {
|
||||
return <Bot size={size} className={baseClass} />;
|
||||
return <Bot size={size} className={colorClass} />;
|
||||
}
|
||||
|
||||
const channel = session.channel?.toLowerCase();
|
||||
|
||||
switch (channel) {
|
||||
case 'discord':
|
||||
return <DiscordIcon size={size} className={baseClass} />;
|
||||
return <DiscordIcon size={size} className={colorClass} />;
|
||||
case 'telegram':
|
||||
return <TelegramIcon size={size} className={baseClass} />;
|
||||
return <TelegramIcon size={size} className={colorClass} />;
|
||||
case 'signal':
|
||||
return <SignalIcon size={size} className={baseClass} />;
|
||||
return <SignalIcon size={size} className={colorClass} />;
|
||||
case 'whatsapp':
|
||||
return <WhatsAppIcon size={size} className={baseClass} />;
|
||||
return <WhatsAppIcon size={size} className={colorClass} />;
|
||||
case 'slack':
|
||||
return <SlackIcon size={size} className={baseClass} />;
|
||||
return <SlackIcon size={size} className={colorClass} />;
|
||||
case 'webchat':
|
||||
return <Globe size={size} className={baseClass} />;
|
||||
return <Globe size={size} className={colorClass} />;
|
||||
case 'teamspeak':
|
||||
return <MessageSquare size={size} className={baseClass} />;
|
||||
return <MessageSquare size={size} className={colorClass} />;
|
||||
default:
|
||||
return <MessageSquare size={size} className={baseClass} />;
|
||||
return <MessageSquare size={size} className={colorClass} />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useState, useMemo, useRef, useEffect, useCallback } from 'react';
|
||||
import { X, Sparkles, Search, Pin, Trash2, Columns2, Clock, Bot, MessageSquare, Globe, Zap, ArrowUpCircle } from 'lucide-react';
|
||||
import { X, Search, Pin, Trash2, Columns2, Clock, Bot, MessageSquare, Globe, Zap, ArrowUpCircle, Download } from 'lucide-react';
|
||||
import type { Session } from '../types';
|
||||
import { useT } from '../hooks/useLocale';
|
||||
import { SessionIcon } from './SessionIcon';
|
||||
import { sessionDisplayName } from '../lib/sessionName';
|
||||
import { relativeTime } from '../lib/relativeTime';
|
||||
import { useUpdateCheck } from '../hooks/useUpdateCheck';
|
||||
import { usePwaInstall } from '../hooks/usePwaInstall';
|
||||
|
||||
function VersionBadge() {
|
||||
const update = useUpdateCheck(__APP_VERSION__);
|
||||
@@ -31,6 +32,34 @@ function VersionBadge() {
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarFooter() {
|
||||
const pwa = usePwaInstall();
|
||||
return (
|
||||
<div className="px-4 py-3 border-t border-pc-border flex items-center justify-center gap-3">
|
||||
{pwa.canInstall && (
|
||||
<button
|
||||
onClick={pwa.install}
|
||||
className="inline-flex items-center gap-1 text-[10px] text-pc-accent-light hover:text-[var(--pc-accent)] transition-colors"
|
||||
title="Install app"
|
||||
>
|
||||
<Download size={11} />
|
||||
<span>Install</span>
|
||||
</button>
|
||||
)}
|
||||
<a
|
||||
href="https://github.com/MarlBurroW/pinchchat"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-pc-text-faint hover:text-pc-text-secondary transition-colors"
|
||||
title="GitHub"
|
||||
>
|
||||
<Globe size={11} />
|
||||
</a>
|
||||
<VersionBadge />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const PINNED_KEY = 'pinchchat-pinned-sessions';
|
||||
const WIDTH_KEY = 'pinchchat-sidebar-width';
|
||||
const ORDER_KEY = 'pinchchat-session-order';
|
||||
@@ -258,8 +287,8 @@ export function Sidebar({ sessions, activeSession, onSwitch, onDelete, onSplit,
|
||||
<div className="flex items-center gap-2">
|
||||
<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="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-pc-accent-light" />
|
||||
<div className="relative flex h-8 w-8 items-center justify-center rounded-xl overflow-hidden">
|
||||
<img src="/logo.png" alt="PinchChat" className="h-8 w-8 object-contain" />
|
||||
</div>
|
||||
</div>
|
||||
<span className="font-semibold text-sm text-pc-text tracking-wide">{t('sidebar.title')}</span>
|
||||
@@ -511,12 +540,7 @@ export function Sidebar({ sessions, activeSession, onSwitch, onDelete, onSplit,
|
||||
})}
|
||||
</div>
|
||||
{/* Footer with version */}
|
||||
<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-[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)]" />
|
||||
<VersionBadge />
|
||||
</div>
|
||||
<SidebarFooter />
|
||||
{/* Resize drag handle */}
|
||||
<div
|
||||
onMouseDown={startDrag}
|
||||
|
||||
Reference in New Issue
Block a user