Add better preview pane and tweak some themes for contrast

This commit is contained in:
Jack Kingsman
2026-03-12 00:06:33 -07:00
parent 6466a5c355
commit 1f2903fc2d
8 changed files with 202 additions and 86 deletions
+44 -37
View File
@@ -94,15 +94,23 @@ export function ChatHeader({
onSetChannelFloodScopeOverride(conversation.id, nextValue);
};
const handleOpenConversationInfo = () => {
if (conversation.type === 'contact' && onOpenContactInfo) {
onOpenContactInfo(conversation.id);
return;
}
if (conversation.type === 'channel' && onOpenChannelInfo) {
onOpenChannelInfo(conversation.id);
}
};
return (
<header className="conversation-header flex justify-between items-start px-4 py-2.5 border-b border-border gap-2">
<span className="flex min-w-0 flex-1 items-start gap-2">
{conversation.type === 'contact' && onOpenContactInfo && (
<span
className="flex-shrink-0 cursor-pointer"
role="button"
tabIndex={0}
onKeyDown={handleKeyboardActivate}
<button
type="button"
className="avatar-action-button flex-shrink-0 cursor-pointer rounded-full border-none bg-transparent p-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
onClick={() => onOpenContactInfo(conversation.id)}
title="View contact info"
aria-label={`View info for ${conversation.name}`}
@@ -114,42 +122,41 @@ export function ChatHeader({
contactType={contacts.find((c) => c.public_key === conversation.id)?.type}
clickable
/>
</span>
</button>
)}
<span className="flex min-w-0 flex-1 flex-col">
<span className="flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-0.5">
<span className="flex min-w-0 flex-1 items-baseline gap-2">
<h2
className={`flex shrink min-w-0 items-center gap-1.5 font-semibold text-base ${titleClickable ? 'cursor-pointer hover:text-primary transition-colors' : ''}`}
role={titleClickable ? 'button' : undefined}
tabIndex={titleClickable ? 0 : undefined}
aria-label={titleClickable ? `View info for ${conversation.name}` : undefined}
onKeyDown={titleClickable ? handleKeyboardActivate : undefined}
onClick={
titleClickable
? () => {
if (conversation.type === 'contact' && onOpenContactInfo) {
onOpenContactInfo(conversation.id);
} else if (conversation.type === 'channel' && onOpenChannelInfo) {
onOpenChannelInfo(conversation.id);
}
}
: undefined
}
>
<span className="truncate">
{conversation.type === 'channel' &&
!conversation.name.startsWith('#') &&
activeChannel?.is_hashtag
? '#'
: ''}
{conversation.name}
</span>
{titleClickable && (
<Info
className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground/80"
aria-hidden="true"
/>
<h2 className="min-w-0 flex-1 font-semibold text-base">
{titleClickable ? (
<button
type="button"
className="flex min-w-0 shrink items-center gap-1.5 text-left hover:text-primary transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm"
aria-label={`View info for ${conversation.name}`}
onClick={handleOpenConversationInfo}
>
<span className="truncate">
{conversation.type === 'channel' &&
!conversation.name.startsWith('#') &&
activeChannel?.is_hashtag
? '#'
: ''}
{conversation.name}
</span>
<Info
className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground/80"
aria-hidden="true"
/>
</button>
) : (
<span className="truncate">
{conversation.type === 'channel' &&
!conversation.name.startsWith('#') &&
activeChannel?.is_hashtag
? '#'
: ''}
{conversation.name}
</span>
)}
</h2>
{isPrivateChannel && !showKey ? (
+31 -20
View File
@@ -606,6 +606,10 @@ export function MessageList({
(avatarName ? `name:${avatarName}` : `message:${msg.id}`);
}
}
const avatarActionLabel =
avatarName && avatarName !== 'Unknown'
? `View info for ${avatarName}`
: `View info for ${avatarKey.slice(0, 12)}`;
return (
<div
@@ -619,26 +623,33 @@ export function MessageList({
>
{!msg.outgoing && (
<div className="w-10 flex-shrink-0 flex items-start pt-0.5">
{showAvatar && avatarKey && (
<span
role={onOpenContactInfo ? 'button' : undefined}
tabIndex={onOpenContactInfo ? 0 : undefined}
onKeyDown={onOpenContactInfo ? handleKeyboardActivate : undefined}
onClick={
onOpenContactInfo
? () => onOpenContactInfo(avatarKey, msg.type === 'CHAN')
: undefined
}
>
<ContactAvatar
name={avatarName}
publicKey={avatarKey}
size={32}
clickable={!!onOpenContactInfo}
variant={avatarVariant}
/>
</span>
)}
{showAvatar &&
avatarKey &&
(onOpenContactInfo ? (
<button
type="button"
className="avatar-action-button rounded-full border-none bg-transparent p-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
aria-label={avatarActionLabel}
onClick={() => onOpenContactInfo(avatarKey, msg.type === 'CHAN')}
>
<ContactAvatar
name={avatarName}
publicKey={avatarKey}
size={32}
clickable
variant={avatarVariant}
/>
</button>
) : (
<span>
<ContactAvatar
name={avatarName}
publicKey={avatarKey}
size={32}
variant={avatarVariant}
/>
</span>
))}
</div>
)}
<div
+3 -3
View File
@@ -5,9 +5,9 @@ import {
ChevronDown,
ChevronRight,
LockOpen,
Logs,
Map,
Search as SearchIcon,
Sparkles,
SquarePen,
Waypoints,
X,
@@ -533,7 +533,7 @@ export function Sidebar({
renderSidebarActionRow({
key: 'tool-raw',
active: isActive('raw', 'raw'),
icon: <Waypoints className="h-4 w-4" />,
icon: <Logs className="h-4 w-4" />,
label: 'Packet Feed',
onClick: () =>
handleSelectConversation({
@@ -557,7 +557,7 @@ export function Sidebar({
renderSidebarActionRow({
key: 'tool-visualizer',
active: isActive('visualizer', 'visualizer'),
icon: <Sparkles className="h-4 w-4" />,
icon: <Waypoints className="h-4 w-4" />,
label: 'Mesh Visualizer',
onClick: () =>
handleSelectConversation({
@@ -1,7 +1,9 @@
import { useState } from 'react';
import { Logs, MessageSquare } from 'lucide-react';
import { Input } from '../ui/input';
import { Label } from '../ui/label';
import { Separator } from '../ui/separator';
import { ContactAvatar } from '../ContactAvatar';
import {
captureLastViewedConversationFromHash,
getReopenLastConversationEnabled,
@@ -97,7 +99,7 @@ function ThemePreview({ className }: { className?: string }) {
return (
<div className={`rounded-lg border border-border bg-card p-3 ${className ?? ''}`}>
<p className="text-xs text-muted-foreground mb-3">
Preview alert and message contrast for the selected theme.
Preview alert, message, sidebar, and badge contrast for the selected theme.
</p>
<div className="space-y-2">
@@ -125,6 +127,42 @@ function ThemePreview({ className }: { className?: string }) {
text="Hi there! I'm using RemoteTerm."
/>
</div>
<div className="mt-4 rounded-md border border-border bg-background p-2">
<p className="mb-2 text-[11px] font-medium text-muted-foreground">Sidebar preview</p>
<div className="space-y-1">
<PreviewSidebarRow
active
leading={
<span
className="flex h-6 w-6 items-center justify-center rounded-md bg-primary/10 text-primary"
aria-hidden="true"
>
<Logs className="h-3.5 w-3.5" />
</span>
}
label="Packet Feed"
/>
<PreviewSidebarRow
leading={<ContactAvatar name="Alice" publicKey={'ab'.repeat(32)} size={24} />}
label="Alice"
badge={
<span className="rounded-full bg-badge-unread/90 px-1.5 py-0.5 text-[10px] font-semibold text-badge-unread-foreground">
3
</span>
}
/>
<PreviewSidebarRow
leading={<ContactAvatar name="Mesh Ops" publicKey={'cd'.repeat(32)} size={24} />}
label="Mesh Ops"
badge={
<span className="rounded-full bg-badge-mention px-1.5 py-0.5 text-[10px] font-semibold text-badge-mention-foreground">
@2
</span>
}
/>
</div>
</div>
</div>
);
}
@@ -153,3 +191,34 @@ function PreviewMessage({
</div>
);
}
function PreviewSidebarRow({
leading,
label,
badge,
active = false,
}: {
leading: React.ReactNode;
label: string;
badge?: React.ReactNode;
active?: boolean;
}) {
return (
<div
className={`flex items-center gap-2 rounded-md border-l-2 px-3 py-2 text-[13px] ${
active ? 'border-l-primary bg-accent text-foreground' : 'border-l-transparent'
}`}
>
{leading}
<span className={`min-w-0 flex-1 truncate ${active ? 'font-medium' : 'text-foreground'}`}>
{label}
</span>
{badge}
{!badge && (
<span className="text-muted-foreground" aria-hidden="true">
<MessageSquare className="h-3.5 w-3.5" />
</span>
)}
</div>
);
}