mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-03-28 17:43:05 +01:00
Frontend color overhaul
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="MCTerm" />
|
||||
<meta name="theme-color" content="#0a0a0a" />
|
||||
<meta name="theme-color" content="#111419" />
|
||||
<meta name="description" content="Web interface for MeshCore mesh radio networks. Send and receive messages, manage contacts and channels, and configure your radio." />
|
||||
<title>RemoteTerm for MeshCore</title>
|
||||
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
|
||||
|
||||
@@ -925,12 +925,14 @@ export function App() {
|
||||
|
||||
const settingsSidebarContent = (
|
||||
<div className="sidebar w-60 h-full min-h-0 bg-card border-r border-border flex flex-col">
|
||||
<div className="flex justify-between items-center px-3 py-3 border-b border-border">
|
||||
<h2 className="text-xs uppercase text-muted-foreground font-medium">Settings</h2>
|
||||
<div className="flex justify-between items-center px-3 py-2.5 border-b border-border">
|
||||
<h2 className="text-[10px] uppercase tracking-wider text-muted-foreground font-medium">
|
||||
Settings
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCloseSettingsView}
|
||||
className="h-6 w-6 rounded text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||
className="h-6 w-6 rounded text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
|
||||
title="Back to conversations"
|
||||
aria-label="Back to conversations"
|
||||
>
|
||||
@@ -943,7 +945,7 @@ export function App() {
|
||||
key={section}
|
||||
type="button"
|
||||
className={cn(
|
||||
'w-full px-3 py-2.5 text-left border-l-2 border-transparent hover:bg-accent',
|
||||
'w-full px-3 py-2 text-left text-[13px] border-l-2 border-transparent hover:bg-accent transition-colors',
|
||||
settingsSection === section && 'bg-accent border-l-primary'
|
||||
)}
|
||||
onClick={() => setSettingsSection(section)}
|
||||
@@ -986,7 +988,7 @@ export function App() {
|
||||
{activeConversation ? (
|
||||
activeConversation.type === 'map' ? (
|
||||
<>
|
||||
<div className="flex justify-between items-center px-4 py-3 border-b border-border font-medium text-lg">
|
||||
<div className="flex justify-between items-center px-4 py-2.5 border-b border-border font-semibold text-base">
|
||||
Node Map
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden">
|
||||
@@ -1018,7 +1020,7 @@ export function App() {
|
||||
</Suspense>
|
||||
) : activeConversation.type === 'raw' ? (
|
||||
<>
|
||||
<div className="flex justify-between items-center px-4 py-3 border-b border-border font-medium text-lg">
|
||||
<div className="flex justify-between items-center px-4 py-2.5 border-b border-border font-semibold text-base">
|
||||
Raw Packet Feed
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden">
|
||||
@@ -1027,9 +1029,9 @@ export function App() {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex justify-between items-center px-4 py-3 border-b border-border font-medium text-lg gap-2">
|
||||
<div className="flex justify-between items-center px-4 py-2.5 border-b border-border gap-2">
|
||||
<span className="flex flex-wrap items-baseline gap-x-2 min-w-0 flex-1">
|
||||
<span className="flex-shrink-0">
|
||||
<span className="flex-shrink-0 font-semibold text-base">
|
||||
{activeConversation.type === 'channel' &&
|
||||
!activeConversation.name.startsWith('#') &&
|
||||
activeConversation.name !== 'Public'
|
||||
@@ -1038,7 +1040,7 @@ export function App() {
|
||||
{activeConversation.name}
|
||||
</span>
|
||||
<span
|
||||
className="font-normal text-sm text-muted-foreground font-mono truncate cursor-pointer hover:text-primary"
|
||||
className="font-normal text-[11px] text-muted-foreground font-mono truncate cursor-pointer hover:text-primary transition-colors"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigator.clipboard.writeText(activeConversation.id);
|
||||
@@ -1119,11 +1121,11 @@ export function App() {
|
||||
) : null;
|
||||
})()}
|
||||
</span>
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
<div className="flex items-center gap-0.5 flex-shrink-0">
|
||||
{/* Direct trace button (contacts only) */}
|
||||
{activeConversation.type === 'contact' && (
|
||||
<button
|
||||
className="p-1.5 rounded hover:bg-accent text-xl leading-none"
|
||||
className="p-1.5 rounded hover:bg-accent text-lg leading-none transition-colors"
|
||||
onClick={handleTrace}
|
||||
title="Direct Trace"
|
||||
>
|
||||
@@ -1134,7 +1136,7 @@ export function App() {
|
||||
{(activeConversation.type === 'channel' ||
|
||||
activeConversation.type === 'contact') && (
|
||||
<button
|
||||
className="p-1.5 rounded hover:bg-accent text-xl leading-none"
|
||||
className="p-1.5 rounded hover:bg-accent text-lg leading-none transition-colors"
|
||||
onClick={() =>
|
||||
handleToggleFavorite(
|
||||
activeConversation.type as 'channel' | 'contact',
|
||||
@@ -1156,7 +1158,7 @@ export function App() {
|
||||
activeConversation.type as 'channel' | 'contact',
|
||||
activeConversation.id
|
||||
) ? (
|
||||
<span className="text-yellow-500">★</span>
|
||||
<span className="text-amber-400">★</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground">☆</span>
|
||||
)}
|
||||
@@ -1168,7 +1170,7 @@ export function App() {
|
||||
activeConversation.name === 'Public'
|
||||
) && (
|
||||
<button
|
||||
className="p-1.5 rounded hover:bg-destructive/20 text-destructive text-xl leading-none"
|
||||
className="p-1.5 rounded hover:bg-destructive/10 text-muted-foreground hover:text-destructive text-lg leading-none transition-colors"
|
||||
onClick={() => {
|
||||
if (activeConversation.type === 'channel') {
|
||||
handleDeleteChannel(activeConversation.id);
|
||||
@@ -1234,7 +1236,7 @@ export function App() {
|
||||
|
||||
{showSettings && (
|
||||
<div className="flex-1 flex flex-col min-h-0">
|
||||
<div className="flex justify-between items-center px-4 py-3 border-b border-border font-medium text-lg">
|
||||
<div className="flex justify-between items-center px-4 py-2.5 border-b border-border font-semibold text-base">
|
||||
<span>Radio & Settings</span>
|
||||
<span className="text-sm text-muted-foreground hidden md:inline">
|
||||
{SETTINGS_SECTION_LABELS[settingsSection]}
|
||||
|
||||
@@ -170,7 +170,10 @@ export const MessageInput = forwardRef<MessageInputHandle, MessageInputProps>(fu
|
||||
const showCharCounter = !isRepeaterMode && limits !== null;
|
||||
|
||||
return (
|
||||
<form className="px-4 py-3 border-t border-border flex flex-col gap-1" onSubmit={handleSubmit}>
|
||||
<form
|
||||
className="px-4 py-2.5 border-t border-border flex flex-col gap-1"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
ref={inputRef}
|
||||
|
||||
@@ -461,14 +461,14 @@ export function MessageList({
|
||||
<div
|
||||
className={cn(
|
||||
'py-1.5 px-3 rounded-lg min-w-0',
|
||||
msg.outgoing ? 'bg-[#1e3a29]' : 'bg-muted'
|
||||
msg.outgoing ? 'bg-msg-outgoing' : 'bg-msg-incoming'
|
||||
)}
|
||||
>
|
||||
{showAvatar && (
|
||||
<div className="text-[13px] font-bold text-foreground mb-0.5">
|
||||
<div className="text-[13px] font-semibold text-foreground mb-0.5">
|
||||
{canClickSender ? (
|
||||
<span
|
||||
className="cursor-pointer hover:text-primary hover:underline"
|
||||
className="cursor-pointer hover:text-primary transition-colors"
|
||||
onClick={() => onSenderClick(displaySender)}
|
||||
title={`Mention ${displaySender}`}
|
||||
>
|
||||
@@ -587,13 +587,13 @@ export function MessageList({
|
||||
{showScrollToBottom && (
|
||||
<button
|
||||
onClick={scrollToBottom}
|
||||
className="absolute bottom-4 right-4 w-10 h-10 rounded-full bg-muted hover:bg-accent border border-border flex items-center justify-center shadow-lg transition-opacity"
|
||||
className="absolute bottom-4 right-4 w-9 h-9 rounded-full bg-card hover:bg-accent border border-border flex items-center justify-center shadow-lg transition-all hover:scale-105"
|
||||
title="Scroll to bottom"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useRef, useMemo } from 'react';
|
||||
import { MeshCoreDecoder, PayloadType, Utils } from '@michaelhart/meshcore-decoder';
|
||||
import type { RawPacket } from '../types';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface RawPacketListProps {
|
||||
packets: RawPacket[];
|
||||
@@ -204,9 +205,9 @@ export function RawPacketList({ packets }: RawPacketListProps) {
|
||||
const sortedPackets = [...decodedPackets].sort((a, b) => a.packet.timestamp - b.packet.timestamp);
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-y-auto p-4 flex flex-col gap-3" ref={listRef}>
|
||||
<div className="h-full overflow-y-auto p-4 flex flex-col gap-2" ref={listRef}>
|
||||
{sortedPackets.map(({ packet, decoded }) => (
|
||||
<div key={packet.id} className="py-2 px-3 bg-muted rounded">
|
||||
<div key={packet.id} className="py-2 px-3 bg-card rounded-md border border-border/50">
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Route type badge */}
|
||||
<span
|
||||
@@ -220,25 +221,27 @@ export function RawPacketList({ packets }: RawPacketListProps) {
|
||||
{!packet.decrypted && <span title="Encrypted">🔒</span>}
|
||||
|
||||
{/* Summary */}
|
||||
<span className={packet.decrypted ? 'text-primary' : 'text-foreground'}>
|
||||
<span
|
||||
className={cn('text-[13px]', packet.decrypted ? 'text-primary' : 'text-foreground')}
|
||||
>
|
||||
{decoded.summary}
|
||||
</span>
|
||||
|
||||
{/* Time */}
|
||||
<span className="text-muted-foreground ml-auto text-sm">
|
||||
<span className="text-muted-foreground ml-auto text-[12px] tabular-nums">
|
||||
{formatTime(packet.timestamp)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Signal info */}
|
||||
{(packet.snr !== null || packet.rssi !== null) && (
|
||||
<div className="text-[11px] text-muted-foreground mt-0.5">
|
||||
<div className="text-[11px] text-muted-foreground mt-0.5 tabular-nums">
|
||||
{formatSignalInfo(packet)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Raw hex data (always visible) */}
|
||||
<div className="font-mono text-[10px] break-all text-muted-foreground mt-1 p-1 bg-background/50 rounded">
|
||||
<div className="font-mono text-[10px] break-all text-muted-foreground mt-1.5 p-1.5 bg-background/60 rounded">
|
||||
{packet.data.toUpperCase()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -380,9 +380,9 @@ export function Sidebar({
|
||||
<div
|
||||
key={row.key}
|
||||
className={cn(
|
||||
'px-3 py-2.5 cursor-pointer flex items-center gap-2 border-l-2 border-transparent hover:bg-accent',
|
||||
'px-3 py-2 cursor-pointer flex items-center gap-2 border-l-2 border-transparent hover:bg-accent transition-colors',
|
||||
isActive(row.type, row.id) && 'bg-accent border-l-primary',
|
||||
row.unreadCount > 0 && '[&_.name]:font-bold [&_.name]:text-foreground'
|
||||
row.unreadCount > 0 && '[&_.name]:font-semibold [&_.name]:text-foreground'
|
||||
)}
|
||||
onClick={() =>
|
||||
handleSelectConversation({
|
||||
@@ -400,14 +400,14 @@ export function Sidebar({
|
||||
contactType={row.contact.type}
|
||||
/>
|
||||
)}
|
||||
<span className="name flex-1 truncate">{row.name}</span>
|
||||
<span className="name flex-1 truncate text-[13px]">{row.name}</span>
|
||||
{row.unreadCount > 0 && (
|
||||
<span
|
||||
className={cn(
|
||||
'text-[10px] font-semibold px-1.5 py-0.5 rounded-full min-w-[18px] text-center',
|
||||
row.isMention
|
||||
? 'bg-destructive text-destructive-foreground'
|
||||
: 'bg-primary text-primary-foreground'
|
||||
: 'bg-primary/90 text-primary-foreground'
|
||||
)}
|
||||
>
|
||||
{row.unreadCount}
|
||||
@@ -443,10 +443,10 @@ export function Sidebar({
|
||||
const effectiveCollapsed = isSearching ? false : collapsed;
|
||||
|
||||
return (
|
||||
<div className="flex justify-between items-center px-3 py-2 pt-3">
|
||||
<div className="flex justify-between items-center px-3 py-2 pt-3.5">
|
||||
<button
|
||||
className={cn(
|
||||
'flex items-center gap-1 text-[11px] uppercase text-muted-foreground hover:text-foreground',
|
||||
'flex items-center gap-1.5 text-[10px] uppercase tracking-wider text-muted-foreground hover:text-foreground transition-colors',
|
||||
isSearching && 'cursor-default'
|
||||
)}
|
||||
onClick={() => {
|
||||
@@ -454,14 +454,14 @@ export function Sidebar({
|
||||
}}
|
||||
title={effectiveCollapsed ? `Expand ${title}` : `Collapse ${title}`}
|
||||
>
|
||||
<span className="text-[10px]">{effectiveCollapsed ? '▸' : '▾'}</span>
|
||||
<span className="text-[9px]">{effectiveCollapsed ? '▸' : '▾'}</span>
|
||||
<span>{title}</span>
|
||||
</button>
|
||||
{(showSortToggle || unreadCount > 0) && (
|
||||
<div className="ml-auto flex items-center gap-1.5">
|
||||
{showSortToggle && (
|
||||
<button
|
||||
className="bg-transparent border border-border text-muted-foreground px-1.5 py-0.5 text-[10px] rounded hover:bg-accent hover:text-foreground mr-0.5"
|
||||
className="bg-transparent text-muted-foreground/60 px-1 py-0.5 text-[10px] rounded hover:text-foreground transition-colors"
|
||||
onClick={handleSortToggle}
|
||||
title={sortOrder === 'alpha' ? 'Sort by recent' : 'Sort alphabetically'}
|
||||
>
|
||||
@@ -469,7 +469,7 @@ export function Sidebar({
|
||||
</button>
|
||||
)}
|
||||
{unreadCount > 0 && (
|
||||
<span className="text-[10px] font-medium px-1.5 py-0.5 rounded-full bg-muted text-muted-foreground">
|
||||
<span className="text-[10px] font-medium px-1.5 py-0.5 rounded-full bg-secondary text-muted-foreground">
|
||||
{unreadCount}
|
||||
</span>
|
||||
)}
|
||||
@@ -482,31 +482,33 @@ export function Sidebar({
|
||||
return (
|
||||
<div className="sidebar w-60 h-full min-h-0 bg-card border-r border-border flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center px-3 py-3 border-b border-border">
|
||||
<h2 className="text-xs uppercase text-muted-foreground font-medium">Conversations</h2>
|
||||
<div className="flex justify-between items-center px-3 py-2.5 border-b border-border">
|
||||
<h2 className="text-[10px] uppercase tracking-wider text-muted-foreground font-medium">
|
||||
Conversations
|
||||
</h2>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onNewMessage}
|
||||
title="New Message"
|
||||
className="h-6 w-6 p-0 text-muted-foreground hover:text-foreground"
|
||||
className="h-6 w-6 p-0 text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
+
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="relative px-3 py-2 border-b border-border">
|
||||
<div className="relative px-3 py-2">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="h-8 text-sm pr-8"
|
||||
className="h-7 text-[13px] pr-8 bg-background/50"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground text-lg leading-none"
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground text-lg leading-none transition-colors"
|
||||
onClick={() => setSearchQuery('')}
|
||||
title="Clear search"
|
||||
>
|
||||
@@ -521,7 +523,7 @@ export function Sidebar({
|
||||
{!query && (
|
||||
<div
|
||||
className={cn(
|
||||
'px-3 py-2.5 cursor-pointer flex items-center gap-2 border-l-2 border-transparent hover:bg-accent',
|
||||
'px-3 py-2 cursor-pointer flex items-center gap-2 border-l-2 border-transparent hover:bg-accent transition-colors text-[13px]',
|
||||
isActive('raw', 'raw') && 'bg-accent border-l-primary'
|
||||
)}
|
||||
onClick={() =>
|
||||
@@ -533,7 +535,7 @@ export function Sidebar({
|
||||
}
|
||||
>
|
||||
<span className="text-muted-foreground text-xs">📡</span>
|
||||
<span className="flex-1 truncate">Packet Feed</span>
|
||||
<span className="flex-1 truncate text-muted-foreground">Packet Feed</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -541,7 +543,7 @@ export function Sidebar({
|
||||
{!query && (
|
||||
<div
|
||||
className={cn(
|
||||
'px-3 py-2.5 cursor-pointer flex items-center gap-2 border-l-2 border-transparent hover:bg-accent',
|
||||
'px-3 py-2 cursor-pointer flex items-center gap-2 border-l-2 border-transparent hover:bg-accent transition-colors text-[13px]',
|
||||
isActive('map', 'map') && 'bg-accent border-l-primary'
|
||||
)}
|
||||
onClick={() =>
|
||||
@@ -553,7 +555,7 @@ export function Sidebar({
|
||||
}
|
||||
>
|
||||
<span className="text-muted-foreground text-xs">🗺️</span>
|
||||
<span className="flex-1 truncate">Node Map</span>
|
||||
<span className="flex-1 truncate text-muted-foreground">Node Map</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -561,7 +563,7 @@ export function Sidebar({
|
||||
{!query && (
|
||||
<div
|
||||
className={cn(
|
||||
'px-3 py-2.5 cursor-pointer flex items-center gap-2 border-l-2 border-transparent hover:bg-accent',
|
||||
'px-3 py-2 cursor-pointer flex items-center gap-2 border-l-2 border-transparent hover:bg-accent transition-colors text-[13px]',
|
||||
isActive('visualizer', 'visualizer') && 'bg-accent border-l-primary'
|
||||
)}
|
||||
onClick={() =>
|
||||
@@ -573,7 +575,7 @@ export function Sidebar({
|
||||
}
|
||||
>
|
||||
<span className="text-muted-foreground text-xs">✨</span>
|
||||
<span className="flex-1 truncate">Mesh Visualizer</span>
|
||||
<span className="flex-1 truncate text-muted-foreground">Mesh Visualizer</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -581,18 +583,18 @@ export function Sidebar({
|
||||
{!query && (
|
||||
<div
|
||||
className={cn(
|
||||
'px-3 py-2.5 cursor-pointer flex items-center gap-2 border-l-2 border-transparent hover:bg-accent',
|
||||
'px-3 py-2 cursor-pointer flex items-center gap-2 border-l-2 border-transparent hover:bg-accent transition-colors text-[13px]',
|
||||
showCracker && 'bg-accent border-l-primary'
|
||||
)}
|
||||
onClick={onToggleCracker}
|
||||
>
|
||||
<span className="text-muted-foreground text-xs">🔓</span>
|
||||
<span className="flex-1 truncate">
|
||||
<span className="flex-1 truncate text-muted-foreground">
|
||||
{showCracker ? 'Hide' : 'Show'} Room Finder
|
||||
<span
|
||||
className={cn(
|
||||
'ml-1 text-xs',
|
||||
crackerRunning ? 'text-green-500' : 'text-muted-foreground'
|
||||
'ml-1 text-[11px]',
|
||||
crackerRunning ? 'text-primary' : 'text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
({crackerRunning ? 'running' : 'idle'})
|
||||
@@ -604,7 +606,7 @@ export function Sidebar({
|
||||
{/* Mark All Read */}
|
||||
{!query && Object.keys(unreadCounts).length > 0 && (
|
||||
<div
|
||||
className="px-3 py-2.5 cursor-pointer flex items-center gap-2 border-l-2 border-transparent hover:bg-accent"
|
||||
className="px-3 py-2 cursor-pointer flex items-center gap-2 border-l-2 border-transparent hover:bg-accent transition-colors text-[13px]"
|
||||
onClick={onMarkAllRead}
|
||||
>
|
||||
<span className="text-muted-foreground text-xs">✓</span>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Menu } from 'lucide-react';
|
||||
import type { HealthStatus, RadioConfig } from '../types';
|
||||
import { api } from '../api';
|
||||
import { toast } from './ui/sonner';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface StatusBarProps {
|
||||
health: HealthStatus | null;
|
||||
@@ -39,34 +40,39 @@ export function StatusBar({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-4 px-4 py-2 bg-card border-b border-border text-xs">
|
||||
<div className="flex items-center gap-3 px-4 py-2.5 bg-card border-b border-border text-xs">
|
||||
{/* Mobile menu button - only visible on small screens */}
|
||||
{onMenuClick && (
|
||||
<button
|
||||
onClick={onMenuClick}
|
||||
className="md:hidden p-1 bg-transparent border-none text-foreground cursor-pointer"
|
||||
className="md:hidden p-1 bg-transparent border-none text-muted-foreground hover:text-foreground cursor-pointer transition-colors"
|
||||
aria-label="Open menu"
|
||||
>
|
||||
<Menu className="h-5 w-5" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
<h1 className="text-base font-semibold mr-auto">RemoteTerm</h1>
|
||||
<h1 className="text-sm font-semibold tracking-tight mr-auto text-foreground">RemoteTerm</h1>
|
||||
|
||||
<div className="flex items-center gap-1 text-muted-foreground">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full ${connected ? 'bg-primary' : 'bg-muted-foreground'}`}
|
||||
className={cn(
|
||||
'w-2 h-2 rounded-full transition-colors',
|
||||
connected
|
||||
? 'bg-primary shadow-[0_0_6px_hsl(var(--primary)/0.5)]'
|
||||
: 'bg-muted-foreground'
|
||||
)}
|
||||
/>
|
||||
<span className="hidden lg:inline text-foreground">
|
||||
<span className="hidden lg:inline text-muted-foreground">
|
||||
{connected ? 'Connected' : 'Disconnected'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{config && (
|
||||
<div className="hidden lg:flex items-center gap-2 text-muted-foreground">
|
||||
<span className="text-foreground">{config.name || 'Unnamed'}</span>
|
||||
<span className="text-foreground font-medium">{config.name || 'Unnamed'}</span>
|
||||
<span
|
||||
className="font-mono text-muted-foreground cursor-pointer hover:text-primary"
|
||||
className="font-mono text-[11px] text-muted-foreground cursor-pointer hover:text-primary transition-colors"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(config.public_key);
|
||||
toast.success('Public key copied!');
|
||||
@@ -82,19 +88,16 @@ export function StatusBar({
|
||||
<button
|
||||
onClick={handleReconnect}
|
||||
disabled={reconnecting}
|
||||
className="px-3 py-1 bg-amber-950 border border-amber-800 text-amber-400 rounded text-xs cursor-pointer hover:bg-amber-900 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className="px-3 py-1 bg-amber-500/10 border border-amber-500/20 text-amber-400 rounded-md text-xs cursor-pointer hover:bg-amber-500/15 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{reconnecting ? 'Reconnecting...' : 'Reconnect'}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={onSettingsClick}
|
||||
className="px-3 py-1 bg-secondary border border-border text-foreground rounded text-xs cursor-pointer hover:bg-accent"
|
||||
className="px-3 py-1.5 bg-secondary border border-border text-muted-foreground rounded-md text-xs cursor-pointer hover:bg-accent hover:text-foreground transition-colors"
|
||||
>
|
||||
<span role="img" aria-label="Settings">
|
||||
🔧
|
||||
</span>{' '}
|
||||
{settingsMode ? 'Back to Chat' : 'Radio & Config'}
|
||||
{settingsMode ? 'Back to Chat' : 'Settings'}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -4,26 +4,30 @@
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 10%;
|
||||
--foreground: 0 0% 88%;
|
||||
--card: 0 0% 14%;
|
||||
--card-foreground: 0 0% 88%;
|
||||
--popover: 0 0% 14%;
|
||||
--popover-foreground: 0 0% 88%;
|
||||
--primary: 122 39% 49%;
|
||||
--background: 224 14% 8%;
|
||||
--foreground: 213 15% 90%;
|
||||
--card: 224 13% 11%;
|
||||
--card-foreground: 213 15% 90%;
|
||||
--popover: 224 13% 12%;
|
||||
--popover-foreground: 213 15% 90%;
|
||||
--primary: 152 60% 38%;
|
||||
--primary-foreground: 0 0% 100%;
|
||||
--secondary: 0 0% 20%;
|
||||
--secondary-foreground: 0 0% 88%;
|
||||
--muted: 0 0% 20%;
|
||||
--muted-foreground: 0 0% 64%;
|
||||
--accent: 0 0% 20%;
|
||||
--accent-foreground: 0 0% 88%;
|
||||
--destructive: 0 62% 50%;
|
||||
--secondary: 224 12% 17%;
|
||||
--secondary-foreground: 213 12% 85%;
|
||||
--muted: 224 12% 15%;
|
||||
--muted-foreground: 218 10% 60%;
|
||||
--accent: 224 11% 19%;
|
||||
--accent-foreground: 213 15% 90%;
|
||||
--destructive: 0 72% 51%;
|
||||
--destructive-foreground: 0 0% 100%;
|
||||
--border: 0 0% 20%;
|
||||
--input: 0 0% 20%;
|
||||
--ring: 122 39% 49%;
|
||||
--border: 224 11% 17%;
|
||||
--input: 224 11% 17%;
|
||||
--ring: 152 60% 38%;
|
||||
--radius: 0.5rem;
|
||||
|
||||
/* Semantic colors for specific contexts */
|
||||
--msg-outgoing: 152 30% 14%;
|
||||
--msg-incoming: 224 12% 15%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,11 +37,40 @@
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-family:
|
||||
system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
sans-serif;
|
||||
font-size: 14px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom scrollbar — thin, unobtrusive, matches palette */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: hsl(224 11% 22%);
|
||||
border-radius: 3px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: hsl(224 11% 28%);
|
||||
}
|
||||
|
||||
/* Firefox scrollbar */
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: hsl(224 11% 22%) transparent;
|
||||
}
|
||||
|
||||
/* Constrain CodeMirror editor width */
|
||||
.cm-editor {
|
||||
max-width: 100% !important;
|
||||
|
||||
@@ -32,7 +32,12 @@ body,
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-family:
|
||||
system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
sans-serif;
|
||||
/* Prevent overscroll/bounce on mobile */
|
||||
overscroll-behavior: none;
|
||||
padding: var(--safe-area-top) var(--safe-area-right) var(--safe-area-bottom-capped)
|
||||
|
||||
@@ -41,6 +41,8 @@ export default {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
"msg-outgoing": "hsl(var(--msg-outgoing))",
|
||||
"msg-incoming": "hsl(var(--msg-incoming))",
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
|
||||
Reference in New Issue
Block a user