Icon overhaul

This commit is contained in:
Jack Kingsman
2026-03-10 17:43:15 -07:00
parent c9ede1f71f
commit 7c68973e30
11 changed files with 251 additions and 157 deletions

View File

@@ -9,6 +9,7 @@ import { ChannelInfoPane } from './ChannelInfoPane';
import { Toaster } from './ui/sonner';
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from './ui/sheet';
import {
SETTINGS_SECTION_ICONS,
SETTINGS_SECTION_LABELS,
SETTINGS_SECTION_ORDER,
type SettingsSection,
@@ -115,20 +116,26 @@ export function AppShell({
</button>
</div>
<div className="flex-1 min-h-0 overflow-y-auto py-1 [contain:layout_paint]">
{SETTINGS_SECTION_ORDER.map((section) => (
<button
key={section}
type="button"
className={cn(
'w-full px-3 py-2 text-left text-[13px] border-l-2 border-transparent hover:bg-accent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset',
settingsSection === section && 'bg-accent border-l-primary'
)}
aria-current={settingsSection === section ? 'true' : undefined}
onClick={() => onSettingsSectionChange(section)}
>
{SETTINGS_SECTION_LABELS[section]}
</button>
))}
{SETTINGS_SECTION_ORDER.map((section) => {
const Icon = SETTINGS_SECTION_ICONS[section];
return (
<button
key={section}
type="button"
className={cn(
'w-full px-3 py-2 text-left text-[13px] border-l-2 border-transparent hover:bg-accent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset',
settingsSection === section && 'bg-accent border-l-primary'
)}
aria-current={settingsSection === section ? 'true' : undefined}
onClick={() => onSettingsSectionChange(section)}
>
<span className="flex items-center gap-2">
<Icon className="h-4 w-4 text-muted-foreground" aria-hidden="true" />
<span>{SETTINGS_SECTION_LABELS[section]}</span>
</span>
</button>
);
})}
</div>
</nav>
);
@@ -216,7 +223,13 @@ export function AppShell({
<h2 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]}
<span className="inline-flex items-center gap-1.5">
{(() => {
const Icon = SETTINGS_SECTION_ICONS[settingsSection];
return <Icon className="h-4 w-4" aria-hidden="true" />;
})()}
<span>{SETTINGS_SECTION_LABELS[settingsSection]}</span>
</span>
</span>
</h2>
<div className="flex-1 min-h-0 overflow-hidden">

View File

@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react';
import { Star } from 'lucide-react';
import { api } from '../api';
import { formatTime } from '../utils/messageParser';
import { isFavorite } from '../utils/favorites';
@@ -125,12 +126,12 @@ export function ChannelInfoPane({
>
{isFavorite(favorites, 'channel', channel.key) ? (
<>
<span className="text-favorite text-lg">&#9733;</span>
<Star className="h-4.5 w-4.5 fill-current text-favorite" aria-hidden="true" />
<span>Remove from favorites</span>
</>
) : (
<>
<span className="text-muted-foreground text-lg">&#9734;</span>
<Star className="h-4.5 w-4.5 text-muted-foreground" aria-hidden="true" />
<span>Add to favorites</span>
</>
)}

View File

@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react';
import { Globe2, Info, Route, Star, Trash2 } from 'lucide-react';
import { toast } from './ui/sonner';
import { isFavorite } from '../utils/favorites';
import { handleKeyboardActivate } from '../utils/a11y';
@@ -71,8 +72,8 @@ export function ChatHeader({
};
return (
<header 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">
<header className="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"
@@ -92,103 +93,124 @@ export function ChatHeader({
/>
</span>
)}
<h2
className={`flex-shrink-0 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);
}
<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
}
: undefined
}
>
{conversation.type === 'channel' &&
!conversation.name.startsWith('#') &&
activeChannel?.is_hashtag
? '#'
: ''}
{conversation.name}
</h2>
{isPrivateChannel && !showKey ? (
<button
className="font-normal text-[11px] text-muted-foreground font-mono hover:text-primary transition-colors"
onClick={(e) => {
e.stopPropagation();
setShowKey(true);
}}
title="Reveal channel key"
>
Show Key
</button>
) : (
<span
className="font-normal text-[11px] text-muted-foreground font-mono truncate cursor-pointer hover:text-primary transition-colors"
role="button"
tabIndex={0}
onKeyDown={handleKeyboardActivate}
onClick={(e) => {
e.stopPropagation();
navigator.clipboard.writeText(conversation.id);
toast.success(
conversation.type === 'channel' ? 'Room key copied!' : 'Contact key copied!'
);
}}
title="Click to copy"
aria-label={conversation.type === 'channel' ? 'Copy channel key' : 'Copy contact key'}
>
{conversation.type === 'channel' ? conversation.id.toLowerCase() : conversation.id}
>
<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>
{isPrivateChannel && !showKey ? (
<button
className="min-w-0 flex-shrink text-[11px] font-mono text-muted-foreground transition-colors hover:text-primary"
onClick={(e) => {
e.stopPropagation();
setShowKey(true);
}}
title="Reveal channel key"
>
Show Key
</button>
) : (
<span
className="min-w-0 flex-1 truncate font-mono text-[11px] text-muted-foreground transition-colors hover:text-primary"
role="button"
tabIndex={0}
onKeyDown={handleKeyboardActivate}
onClick={(e) => {
e.stopPropagation();
navigator.clipboard.writeText(conversation.id);
toast.success(
conversation.type === 'channel' ? 'Room key copied!' : 'Contact key copied!'
);
}}
title="Click to copy"
aria-label={
conversation.type === 'channel' ? 'Copy channel key' : 'Copy contact key'
}
>
{conversation.type === 'channel'
? conversation.id.toLowerCase()
: conversation.id}
</span>
)}
</span>
{conversation.type === 'channel' && activeChannel?.flood_scope_override && (
<span className="min-w-0 basis-full text-[11px] text-amber-700 dark:text-amber-300 truncate">
Regional override active:{' '}
{stripRegionScopePrefix(activeChannel.flood_scope_override)}
</span>
)}
{conversation.type === 'contact' &&
(() => {
const contact = contacts.find((c) => c.public_key === conversation.id);
if (!contact) return null;
return (
<span className="min-w-0 flex-none text-[11px] text-muted-foreground max-sm:basis-full">
<ContactStatusInfo
contact={contact}
ourLat={config?.lat ?? null}
ourLon={config?.lon ?? null}
/>
</span>
);
})()}
</span>
)}
{conversation.type === 'channel' && activeChannel?.flood_scope_override && (
<span className="basis-full sm:basis-auto text-[11px] text-amber-700 dark:text-amber-300 truncate">
Regional override active: {stripRegionScopePrefix(activeChannel.flood_scope_override)}
</span>
)}
{conversation.type === 'contact' &&
(() => {
const contact = contacts.find((c) => c.public_key === conversation.id);
if (!contact) return null;
return (
<ContactStatusInfo
contact={contact}
ourLat={config?.lat ?? null}
ourLon={config?.lon ?? null}
/>
);
})()}
</span>
</span>
<div className="flex items-center gap-0.5 flex-shrink-0">
{conversation.type === 'contact' && (
<button
className="p-1.5 rounded hover:bg-accent text-lg leading-none transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
className="p-1 rounded hover:bg-accent text-lg leading-none transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
onClick={onTrace}
title="Direct Trace"
aria-label="Direct Trace"
>
<span aria-hidden="true">&#x1F6CE;</span>
<Route className="h-4 w-4" aria-hidden="true" />
</button>
)}
{conversation.type === 'channel' && onSetChannelFloodScopeOverride && (
<button
className="p-1.5 rounded hover:bg-accent text-lg leading-none transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
className="p-1 rounded hover:bg-accent text-lg leading-none transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
onClick={handleEditFloodScopeOverride}
title="Set regional override"
aria-label="Set regional override"
>
<span aria-hidden="true">&#127758;</span>
<Globe2 className="h-4 w-4" aria-hidden="true" />
</button>
)}
{(conversation.type === 'channel' || conversation.type === 'contact') && (
<button
className="p-1.5 rounded hover:bg-accent text-lg leading-none transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
className="p-1 rounded hover:bg-accent text-lg leading-none transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
onClick={() =>
onToggleFavorite(conversation.type as 'channel' | 'contact', conversation.id)
}
@@ -200,15 +222,15 @@ export function ChatHeader({
}
>
{isFavorite(favorites, conversation.type as 'channel' | 'contact', conversation.id) ? (
<span className="text-favorite">&#9733;</span>
<Star className="h-4 w-4 fill-current text-favorite" aria-hidden="true" />
) : (
<span className="text-muted-foreground">&#9734;</span>
<Star className="h-4 w-4 text-muted-foreground" aria-hidden="true" />
)}
</button>
)}
{!(conversation.type === 'channel' && conversation.name === 'Public') && (
<button
className="p-1.5 rounded hover:bg-destructive/10 text-muted-foreground hover:text-destructive text-lg leading-none transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
className="p-1 rounded hover:bg-destructive/10 text-muted-foreground hover:text-destructive text-lg leading-none transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
onClick={() => {
if (conversation.type === 'channel') {
onDeleteChannel(conversation.id);
@@ -219,7 +241,7 @@ export function ChatHeader({
title="Delete"
aria-label="Delete"
>
<span aria-hidden="true">&#128465;</span>
<Trash2 className="h-4 w-4" aria-hidden="true" />
</button>
)}
</div>

View File

@@ -1,4 +1,5 @@
import { type ReactNode, useEffect, useState } from 'react';
import { Ban, Star } from 'lucide-react';
import { api } from '../api';
import { formatTime } from '../utils/messageParser';
import {
@@ -152,12 +153,12 @@ export function ContactInfoPane({
>
{blockedNames.includes(nameOnlyValue) ? (
<>
<span className="text-destructive text-lg">&#x2718;</span>
<Ban className="h-4.5 w-4.5 text-destructive" aria-hidden="true" />
<span>Unblock this name</span>
</>
) : (
<>
<span className="text-muted-foreground text-lg">&#x2718;</span>
<Ban className="h-4.5 w-4.5 text-muted-foreground" aria-hidden="true" />
<span>Block this name</span>
</>
)}
@@ -283,12 +284,12 @@ export function ContactInfoPane({
>
{isFavorite(favorites, 'contact', contact.public_key) ? (
<>
<span className="text-favorite text-lg">&#9733;</span>
<Star className="h-4.5 w-4.5 fill-current text-favorite" aria-hidden="true" />
<span>Remove from favorites</span>
</>
) : (
<>
<span className="text-muted-foreground text-lg">&#9734;</span>
<Star className="h-4.5 w-4.5 text-muted-foreground" aria-hidden="true" />
<span>Add to favorites</span>
</>
)}
@@ -306,12 +307,12 @@ export function ContactInfoPane({
>
{blockedKeys.includes(contact.public_key.toLowerCase()) ? (
<>
<span className="text-destructive text-lg">&#x2718;</span>
<Ban className="h-4.5 w-4.5 text-destructive" aria-hidden="true" />
<span>Unblock this key</span>
</>
) : (
<>
<span className="text-muted-foreground text-lg">&#x2718;</span>
<Ban className="h-4.5 w-4.5 text-muted-foreground" aria-hidden="true" />
<span>Block this key</span>
</>
)}
@@ -325,12 +326,12 @@ export function ContactInfoPane({
>
{blockedNames.includes(contact.name) ? (
<>
<span className="text-destructive text-lg">&#x2718;</span>
<Ban className="h-4.5 w-4.5 text-destructive" aria-hidden="true" />
<span>Unblock name &ldquo;{contact.name}&rdquo;</span>
</>
) : (
<>
<span className="text-muted-foreground text-lg">&#x2718;</span>
<Ban className="h-4.5 w-4.5 text-muted-foreground" aria-hidden="true" />
<span>Block name &ldquo;{contact.name}&rdquo;</span>
</>
)}

View File

@@ -1,4 +1,5 @@
import { useState, useRef } from 'react';
import { Dice5 } from 'lucide-react';
import type { Contact, Conversation } from '../types';
import { getContactDisplayName } from '../utils/pubkey';
import {
@@ -256,7 +257,7 @@ export function NewMessageModal({
title="Generate random key"
aria-label="Generate random key"
>
<span aria-hidden="true">🎲</span>
<Dice5 className="h-4 w-4" aria-hidden="true" />
</Button>
</div>
</div>

View File

@@ -1,5 +1,6 @@
import { toast } from './ui/sonner';
import { Button } from './ui/button';
import { Route, Star, Trash2 } from 'lucide-react';
import { RepeaterLogin } from './RepeaterLogin';
import { useRepeaterDashboard } from '../hooks/useRepeaterDashboard';
import { isFavorite } from '../utils/favorites';
@@ -71,23 +72,33 @@ export function RepeaterDashboard({
return (
<div className="flex-1 flex flex-col min-h-0">
{/* Header */}
<header className="flex justify-between items-start sm: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 font-semibold text-base">{conversation.name}</span>
<span
className="font-normal text-[11px] text-muted-foreground font-mono truncate cursor-pointer hover:text-primary transition-colors"
role="button"
tabIndex={0}
onKeyDown={handleKeyboardActivate}
onClick={() => {
navigator.clipboard.writeText(conversation.id);
toast.success('Contact key copied!');
}}
title="Click to copy"
>
{conversation.id}
<header className="flex justify-between items-start px-4 py-2.5 border-b border-border gap-2">
<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">
<span className="min-w-0 flex-shrink truncate font-semibold text-base">
{conversation.name}
</span>
<span
className="min-w-0 flex-1 truncate font-mono text-[11px] text-muted-foreground transition-colors hover:text-primary"
role="button"
tabIndex={0}
onKeyDown={handleKeyboardActivate}
onClick={() => {
navigator.clipboard.writeText(conversation.id);
toast.success('Contact key copied!');
}}
title="Click to copy"
>
{conversation.id}
</span>
</span>
{contact && (
<span className="min-w-0 flex-none text-[11px] text-muted-foreground max-sm:basis-full">
<ContactStatusInfo contact={contact} ourLat={radioLat} ourLon={radioLon} />
</span>
)}
</span>
{contact && <ContactStatusInfo contact={contact} ourLat={radioLat} ourLon={radioLon} />}
</span>
<div className="flex items-center gap-0.5 flex-shrink-0">
{loggedIn && (
@@ -102,15 +113,15 @@ export function RepeaterDashboard({
</Button>
)}
<button
className="p-1.5 rounded hover:bg-accent text-lg leading-none transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
className="p-1 rounded hover:bg-accent text-lg leading-none transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
onClick={onTrace}
title="Direct Trace"
aria-label="Direct Trace"
>
<span aria-hidden="true">&#x1F6CE;</span>
<Route className="h-4 w-4" aria-hidden="true" />
</button>
<button
className="p-1.5 rounded hover:bg-accent text-lg leading-none transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
className="p-1 rounded hover:bg-accent text-lg leading-none transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
onClick={() => onToggleFavorite('contact', conversation.id)}
title={
isFav
@@ -120,18 +131,18 @@ export function RepeaterDashboard({
aria-label={isFav ? 'Remove from favorites' : 'Add to favorites'}
>
{isFav ? (
<span className="text-favorite">&#9733;</span>
<Star className="h-4 w-4 fill-current text-favorite" aria-hidden="true" />
) : (
<span className="text-muted-foreground">&#9734;</span>
<Star className="h-4 w-4 text-muted-foreground" aria-hidden="true" />
)}
</button>
<button
className="p-1.5 rounded hover:bg-destructive/10 text-muted-foreground hover:text-destructive text-lg leading-none transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
className="p-1 rounded hover:bg-destructive/10 text-muted-foreground hover:text-destructive text-lg leading-none transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
onClick={() => onDeleteContact(conversation.id)}
title="Delete"
aria-label="Delete"
>
<span aria-hidden="true">&#128465;</span>
<Trash2 className="h-4 w-4" aria-hidden="true" />
</button>
</div>
</header>

View File

@@ -7,7 +7,11 @@ import type {
RadioConfigUpdate,
} from '../types';
import type { LocalLabel } from '../utils/localLabel';
import { SETTINGS_SECTION_LABELS, type SettingsSection } from './settings/settingsConstants';
import {
SETTINGS_SECTION_ICONS,
SETTINGS_SECTION_LABELS,
type SettingsSection,
} from './settings/settingsConstants';
import { SettingsRadioSection } from './settings/SettingsRadioSection';
import { SettingsLocalSection } from './settings/SettingsLocalSection';
@@ -138,6 +142,7 @@ export function SettingsModal(props: SettingsModalProps) {
const renderSectionHeader = (section: SettingsSection): ReactNode => {
if (!showSectionButton) return null;
const Icon = SETTINGS_SECTION_ICONS[section];
return (
<button
type="button"
@@ -145,8 +150,9 @@ export function SettingsModal(props: SettingsModalProps) {
aria-expanded={expandedSections[section]}
onClick={() => toggleSection(section)}
>
<span className="font-medium" role="heading" aria-level={3}>
{SETTINGS_SECTION_LABELS[section]}
<span className="inline-flex items-center gap-2 font-medium" role="heading" aria-level={3}>
<Icon className="h-4 w-4 text-muted-foreground" aria-hidden="true" />
<span>{SETTINGS_SECTION_LABELS[section]}</span>
</span>
<span className="text-muted-foreground md:hidden" aria-hidden="true">
{expandedSections[section] ? '' : '+'}

View File

@@ -1,4 +1,16 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
CheckCheck,
ChevronDown,
ChevronRight,
LockOpen,
Map,
Search as SearchIcon,
Sparkles,
SquarePen,
Waypoints,
X,
} from 'lucide-react';
import {
CONTACT_TYPE_REPEATER,
type Contact,
@@ -459,7 +471,7 @@ export function Sidebar({
}: {
key: string;
active?: boolean;
icon: string;
icon: React.ReactNode;
label: React.ReactNode;
onClick: () => void;
}) => (
@@ -475,7 +487,7 @@ export function Sidebar({
onKeyDown={handleKeyboardActivate}
onClick={onClick}
>
<span className="text-muted-foreground text-xs" aria-hidden="true">
<span className="text-muted-foreground" aria-hidden="true">
{icon}
</span>
<span className="flex-1 truncate text-muted-foreground">{label}</span>
@@ -507,7 +519,7 @@ export function Sidebar({
renderSidebarActionRow({
key: 'tool-raw',
active: isActive('raw', 'raw'),
icon: '📡',
icon: <Waypoints className="h-4 w-4" />,
label: 'Packet Feed',
onClick: () =>
handleSelectConversation({
@@ -519,7 +531,7 @@ export function Sidebar({
renderSidebarActionRow({
key: 'tool-map',
active: isActive('map', 'map'),
icon: '🗺️',
icon: <Map className="h-4 w-4" />,
label: 'Node Map',
onClick: () =>
handleSelectConversation({
@@ -531,7 +543,7 @@ export function Sidebar({
renderSidebarActionRow({
key: 'tool-visualizer',
active: isActive('visualizer', 'visualizer'),
icon: '✨',
icon: <Sparkles className="h-4 w-4" />,
label: 'Mesh Visualizer',
onClick: () =>
handleSelectConversation({
@@ -543,7 +555,7 @@ export function Sidebar({
renderSidebarActionRow({
key: 'tool-search',
active: isActive('search', 'search'),
icon: '🔍',
icon: <SearchIcon className="h-4 w-4" />,
label: 'Message Search',
onClick: () =>
handleSelectConversation({
@@ -555,7 +567,7 @@ export function Sidebar({
renderSidebarActionRow({
key: 'tool-cracker',
active: showCracker,
icon: '🔓',
icon: <LockOpen className="h-4 w-4" />,
label: (
<>
{showCracker ? 'Hide' : 'Show'} Room Finder
@@ -597,9 +609,11 @@ export function Sidebar({
}}
title={effectiveCollapsed ? `Expand ${title}` : `Collapse ${title}`}
>
<span className="text-[9px]" aria-hidden="true">
{effectiveCollapsed ? '▸' : '▾'}
</span>
{effectiveCollapsed ? (
<ChevronRight className="h-3.5 w-3.5" aria-hidden="true" />
) : (
<ChevronDown className="h-3.5 w-3.5" aria-hidden="true" />
)}
<span>{title}</span>
</button>
{(showSortToggle || unreadCount > 0) && (
@@ -651,7 +665,7 @@ export function Sidebar({
aria-label="New message"
className="h-6 w-6 p-0 text-muted-foreground hover:text-foreground transition-colors"
>
+
<SquarePen className="h-4 w-4" />
</Button>
</div>
@@ -672,7 +686,7 @@ export function Sidebar({
title="Clear search"
aria-label="Clear search"
>
×
<X className="h-4 w-4" />
</button>
)}
</div>
@@ -696,9 +710,7 @@ export function Sidebar({
onKeyDown={handleKeyboardActivate}
onClick={onMarkAllRead}
>
<span className="text-muted-foreground text-xs" aria-hidden="true">
</span>
<CheckCheck className="h-4 w-4 text-muted-foreground" aria-hidden="true" />
<span className="flex-1 truncate text-muted-foreground">Mark all as read</span>
</div>
)}

View File

@@ -52,16 +52,16 @@ export function StatusBar({
{onMenuClick && (
<button
onClick={onMenuClick}
className="md:hidden p-1 bg-transparent border-none text-muted-foreground hover:text-foreground cursor-pointer transition-colors"
className="md:hidden p-0.5 bg-transparent border-none text-muted-foreground hover:text-foreground cursor-pointer transition-colors"
aria-label="Open menu"
>
<Menu className="h-5 w-5" />
<Menu className="h-4 w-4" />
</button>
)}
<h1 className="text-base font-semibold tracking-tight mr-auto text-foreground flex items-center gap-1.5">
<svg
className="h-5 w-5 shrink-0 text-white"
className="h-4 w-4 shrink-0 text-white"
viewBox="0 0 512 512"
fill="currentColor"
aria-hidden="true"

View File

@@ -1,4 +1,5 @@
import { useState, useEffect, useMemo } from 'react';
import { MapPinned } from 'lucide-react';
import { Input } from '../ui/input';
import { Label } from '../ui/label';
import { Button } from '../ui/button';
@@ -406,7 +407,14 @@ export function SettingsRadioSection({
onClick={handleGetLocation}
disabled={gettingLocation}
>
{gettingLocation ? 'Getting...' : '📍 Use My Location'}
{gettingLocation ? (
'Getting...'
) : (
<>
<MapPinned className="mr-1.5 h-4 w-4" aria-hidden="true" />
Use My Location
</>
)}
</Button>
</div>
<div className="grid grid-cols-2 gap-4">

View File

@@ -1,3 +1,13 @@
import {
BarChart3,
Database,
Info,
MonitorCog,
RadioTower,
Share2,
type LucideIcon,
} from 'lucide-react';
export type SettingsSection = 'radio' | 'local' | 'database' | 'fanout' | 'statistics' | 'about';
export const SETTINGS_SECTION_ORDER: SettingsSection[] = [
@@ -10,10 +20,19 @@ export const SETTINGS_SECTION_ORDER: SettingsSection[] = [
];
export const SETTINGS_SECTION_LABELS: Record<SettingsSection, string> = {
radio: '📻 Radio',
local: '🖥️ Local Configuration',
database: '🗄️ Database & Messaging',
fanout: '📤 MQTT & Automation',
statistics: '📊 Statistics',
radio: 'Radio',
local: 'Local Configuration',
database: 'Database & Messaging',
fanout: 'MQTT & Automation',
statistics: 'Statistics',
about: 'About',
};
export const SETTINGS_SECTION_ICONS: Record<SettingsSection, LucideIcon> = {
radio: RadioTower,
local: MonitorCog,
database: Database,
fanout: Share2,
statistics: BarChart3,
about: Info,
};