mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-03-28 17:43:05 +01:00
Icon overhaul
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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">★</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">☆</span>
|
||||
<Star className="h-4.5 w-4.5 text-muted-foreground" aria-hidden="true" />
|
||||
<span>Add to favorites</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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">🛎</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">🌎</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">★</span>
|
||||
<Star className="h-4 w-4 fill-current text-favorite" aria-hidden="true" />
|
||||
) : (
|
||||
<span className="text-muted-foreground">☆</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">🗑</span>
|
||||
<Trash2 className="h-4 w-4" aria-hidden="true" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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">✘</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">✘</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">★</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">☆</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">✘</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">✘</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">✘</span>
|
||||
<Ban className="h-4.5 w-4.5 text-destructive" aria-hidden="true" />
|
||||
<span>Unblock name “{contact.name}”</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="text-muted-foreground text-lg">✘</span>
|
||||
<Ban className="h-4.5 w-4.5 text-muted-foreground" aria-hidden="true" />
|
||||
<span>Block name “{contact.name}”</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">🛎</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">★</span>
|
||||
<Star className="h-4 w-4 fill-current text-favorite" aria-hidden="true" />
|
||||
) : (
|
||||
<span className="text-muted-foreground">☆</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">🗑</span>
|
||||
<Trash2 className="h-4 w-4" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -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] ? '−' : '+'}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user