import { useState } from 'react'; import { ChevronRight, Logs, MessageSquare, Send, Settings } from 'lucide-react'; import { Button } from '../ui/button'; import { Input } from '../ui/input'; import { Label } from '../ui/label'; import { Separator } from '../ui/separator'; import { cn } from '../../lib/utils'; import { ContactAvatar } from '../ContactAvatar'; import { captureLastViewedConversationFromHash, getReopenLastConversationEnabled, setReopenLastConversationEnabled, } from '../../utils/lastViewedConversation'; import { ThemeSelector } from './ThemeSelector'; import { getLocalLabel, setLocalLabel, type LocalLabel } from '../../utils/localLabel'; import { DISTANCE_UNIT_LABELS, DISTANCE_UNITS, setSavedDistanceUnit, } from '../../utils/distanceUnits'; import { useDistanceUnit } from '../../contexts/DistanceUnitContext'; import { DEFAULT_FONT_SCALE, FONT_SCALE_SLIDER_STEP, MAX_FONT_SCALE, MIN_FONT_SCALE, getSavedFontScale, setSavedFontScale, } from '../../utils/fontScale'; import { getAutoFocusInputEnabled, setAutoFocusInputEnabled } from '../../utils/autoFocusInput'; import { BATTERY_DISPLAY_CHANGE_EVENT, getShowBatteryPercent, setShowBatteryPercent as saveBatteryPercent, getShowBatteryVoltage, setShowBatteryVoltage as saveBatteryVoltage, } from '../../utils/batteryDisplay'; export function SettingsLocalSection({ onLocalLabelChange, className, }: { onLocalLabelChange?: (label: LocalLabel) => void; className?: string; }) { const { distanceUnit, setDistanceUnit } = useDistanceUnit(); const [reopenLastConversation, setReopenLastConversation] = useState( getReopenLastConversationEnabled ); const [localLabelText, setLocalLabelText] = useState(() => getLocalLabel().text); const [localLabelColor, setLocalLabelColor] = useState(() => getLocalLabel().color); const [autoFocusInput, setAutoFocusInput] = useState(getAutoFocusInputEnabled); const [batteryPercent, setBatteryPercent] = useState(getShowBatteryPercent); const [batteryVoltage, setBatteryVoltage] = useState(getShowBatteryVoltage); const [fontScale, setFontScale] = useState(getSavedFontScale); const [fontScaleSlider, setFontScaleSlider] = useState(getSavedFontScale); const [fontScaleInput, setFontScaleInput] = useState(() => String(getSavedFontScale())); const commitFontScale = (nextScale: number) => { const normalized = setSavedFontScale(nextScale); setFontScale(normalized); setFontScaleSlider(normalized); setFontScaleInput(String(normalized)); }; const restoreFontScaleInput = () => { setFontScaleInput(String(fontScale)); }; const handleSliderChange = (nextScale: number) => { setFontScaleSlider(nextScale); setFontScaleInput(String(nextScale)); }; const handleSliderCommit = (nextScale: number) => { commitFontScale(nextScale); }; const handleToggleReopenLastConversation = (enabled: boolean) => { setReopenLastConversation(enabled); setReopenLastConversationEnabled(enabled); if (enabled) { captureLastViewedConversationFromHash(); } }; return (

These settings apply only to this device/browser.

{ const text = e.target.value; setLocalLabelText(text); setLocalLabel(text, localLabelColor); onLocalLabelChange?.({ text, color: localLabelColor }); }} placeholder="e.g. Home Base, Field Radio 2" aria-label="Local label text" className="flex-1" /> { const color = e.target.value; setLocalLabelColor(color); setLocalLabel(localLabelText, color); onLocalLabelChange?.({ text: localLabelText, color }); }} aria-label="Local label color" className="w-10 h-9 rounded border border-input cursor-pointer bg-transparent p-0.5" />

Display a colored banner at the top of the page to identify this instance.

Controls how distances are shown throughout the app.

{(batteryPercent || batteryVoltage) && (

Battery data updates every 60 seconds and may take up to a minute to appear after connecting.

)}
handleSliderChange(Number(event.target.value))} onMouseUp={(event) => handleSliderCommit(Number(event.currentTarget.value))} onTouchEnd={(event) => handleSliderCommit(Number(event.currentTarget.value))} onKeyUp={(event) => handleSliderCommit(Number(event.currentTarget.value))} onBlur={(event) => handleSliderCommit(Number(event.currentTarget.value))} aria-label="Relative font size slider" className="w-full accent-primary sm:flex-1" />
{ const nextValue = event.target.value; setFontScaleInput(nextValue); if (nextValue === '') { return; } if (event.target.validity.valid && Number.isFinite(event.target.valueAsNumber)) { commitFontScale(event.target.valueAsNumber); } }} onBlur={() => { const parsed = Number.parseFloat(fontScaleInput); if (!Number.isFinite(parsed)) { restoreFontScaleInput(); return; } commitFontScale(parsed); }} onKeyDown={(event) => { if (event.key !== 'Enter') { return; } event.preventDefault(); const parsed = Number.parseFloat(fontScaleInput); if (!Number.isFinite(parsed)) { restoreFontScaleInput(); return; } commitFontScale(parsed); }} aria-label="Relative font size percentage" /> %

Scales the app's typography for this browser only. The slider moves in 5% steps; the number field accepts any value from 25% to 400%.

); } function ThemePreview({ className }: { className?: string }) { const [showStyleRef, setShowStyleRef] = useState(false); return (

Preview alert, message, sidebar, and badge contrast for the selected theme.

Connected preview: radio link healthy and syncing. Warning preview: packet audit suggests missing history. Error preview: radio reconnect failed.

Sidebar preview

{/* ── Style Reference (collapsible) ── */} {showStyleRef && ( <> {/* ── Text Hierarchy ── */}

Section Label

text-[0.625rem] uppercase tracking-wider text-muted-foreground font-medium

{/* ── Mono Text ── */}

a1b2c3d4e5f6...7890abcdef01

text-xs font-mono — keys, identifiers

1h 23m 45s uptime

text-[0.6875rem] font-mono — metadata mono

$ req_status_sync 0xA1B2...

text-sm font-mono — console / code

{/* ── Badges ── */}
Hashtag Repeater On Radio 3 @2

Muted: bg-muted · Primary: bg-primary/10 · Unread/Mention: bg-badge-*

{/* ── Buttons ── */}

Standard variants (size sm)

Semantic outline variants

Metric selector pills

{['Voltage', 'Noise Floor', 'Packets'].map((label, i) => ( ))}
{/* ── Clickable Text ── */}
a1b2c3d4e5f6 (click to copy) Underlined navigational link

cursor-pointer hover:text-primary transition-colors — use role="button" + tabIndex

{/* ── Inline Alerts ── */}
Info: channel slot cache refreshed from radio.
Warning: radio clock skew detected.
Error: post-connect setup timed out. Reboot the radio and restart.
)}
); } function PreviewSection({ title, children }: { title: string; children: React.ReactNode }) { return (

{title}

{children}
); } function PreviewTextRow({ classes, label, desc, }: { classes: string; label: string; desc: string; }) { return (

Sample text at this size

{label} — {desc}

); } function PreviewBanner({ children, className }: { children: React.ReactNode; className: string }) { return
{children}
; } function PreviewMessage({ sender, text, bubbleClassName, alignRight = false, }: { sender: string; text: string; bubbleClassName: string; alignRight?: boolean; }) { return (
{sender}
{text}
); } function PreviewSidebarRow({ leading, label, badge, active = false, }: { leading: React.ReactNode; label: string; badge?: React.ReactNode; active?: boolean; }) { return (
{label} {badge} {!badge && ( )}
); }