diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index 62ac406..cf417b0 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -434,6 +434,18 @@ The `SearchView` component (`components/SearchView.tsx`) provides full-text sear UI styling is mostly utility-class driven (Tailwind-style classes in JSX) plus shared globals in `index.css` and `styles.css`. Do not rely on old class-only layout assumptions. +### Canonical style reference + +`SettingsLocalSection.tsx` contains a **ThemePreview** component with a collapsible "Canonical style reference" section. This is the authoritative catalog of text sizes, button variants, badge patterns, and interactive elements used throughout the app. **When adding or modifying UI, match the patterns shown there rather than inventing new ones.** + +Key conventions documented in the reference: + +- **Text sizes** use `rem`-based Tailwind values so they scale with the user's font-size slider. Do not use hard-locked `px` values (e.g., `text-[10px]`). The canonical sizes are `text-[0.625rem]` (10px), `text-[0.6875rem]` (11px), `text-[0.8125rem]` (13px), plus standard Tailwind `text-xs`/`text-sm`/`text-base`/`text-lg`/`text-xl`. +- **Section labels** use `text-[0.625rem] uppercase tracking-wider text-muted-foreground font-medium`. +- **Buttons** use the shadcn ` ) : ( {conversation.type === 'contact' && activeContact && ( -
+
)} -

+

Hourly lines compare the last 24 hours against 7-day and all-time averages for the same hour slots. {!analytics.includes_direct_messages && @@ -821,7 +821,7 @@ function ActivityLineChart ( -

+
{legendItems.map((item) => (

{label}

- + {formatRouteLabel(route.path_len, true)}

{chain}

-
+
Raw: {rawPath} {formatPathHashMode(route.path_hash_mode)}
diff --git a/frontend/src/components/MessageList.tsx b/frontend/src/components/MessageList.tsx index 6fe3c18..64d96ce 100644 --- a/frontend/src/components/MessageList.tsx +++ b/frontend/src/components/MessageList.tsx @@ -220,8 +220,8 @@ function HopCountBadge({ paths, onClick, variant }: HopCountBadgeProps) { const className = variant === 'header' - ? 'font-normal text-muted-foreground ml-1 text-[11px] cursor-pointer hover:text-primary hover:underline' - : 'text-[10px] text-muted-foreground ml-1 cursor-pointer hover:text-primary hover:underline'; + ? 'font-normal text-muted-foreground ml-1 text-[0.6875rem] cursor-pointer hover:text-primary hover:underline' + : 'text-[0.625rem] text-muted-foreground ml-1 cursor-pointer hover:text-primary hover:underline'; return ( {showAvatar && ( -
+
{canClickSender ? ( + {formatTime(msg.sender_timestamp || msg.received_at)} {!msg.outgoing && msg.paths && msg.paths.length > 0 && ( @@ -1008,7 +1008,7 @@ export function MessageList({ ))} {!showAvatar && ( <> - + {formatTime(msg.sender_timestamp || msg.received_at)} {!msg.outgoing && msg.paths && msg.paths.length > 0 && ( diff --git a/frontend/src/components/PathModal.tsx b/frontend/src/components/PathModal.tsx index 1a9883f..db2c824 100644 --- a/frontend/src/components/PathModal.tsx +++ b/frontend/src/components/PathModal.tsx @@ -221,7 +221,7 @@ export function PathModal({ > ↻ Resend - + Only repeated by new repeaters @@ -237,7 +237,7 @@ export function PathModal({ > ↻ Resend as new - + Will appear as duplicate to receivers diff --git a/frontend/src/components/RawPacketDetailModal.tsx b/frontend/src/components/RawPacketDetailModal.tsx index ba72f50..328c732 100644 --- a/frontend/src/components/RawPacketDetailModal.tsx +++ b/frontend/src/components/RawPacketDetailModal.tsx @@ -312,7 +312,7 @@ function CompactMetaCard({ }) { return (
-
{label}
+
{label}
{primary}
{secondary ? (
{secondary}
@@ -340,7 +340,7 @@ function FullPacketHex({ const byteRuns = useMemo(() => buildByteRuns(bytes, byteOwners), [byteOwners, bytes]); return ( -
+
{byteRuns.map((run, index) => { const fieldId = run.fieldId; const palette = fieldId ? colorMap.get(fieldId) : null; @@ -446,7 +446,9 @@ function FieldBox({
{field.name}
-
{formatByteRange(field)}
+
+ {formatByteRange(field)} +
-
+
{field.name === 'Ciphertext' ? 'Plaintext' : 'Decoded value'}
@@ -486,11 +488,13 @@ function FieldBox({
{part.field}
-
Bits {part.bits}
+
+ Bits {part.bits} +
{part.binary}
-
{part.value}
+
{part.value}
@@ -598,7 +602,7 @@ export function RawPacketInspectionPanel({ packet, channels }: RawPacketInspecti
-
+
Summary
@@ -611,7 +615,7 @@ export function RawPacketInspectionPanel({ packet, channels }: RawPacketInspecti
{packetContext ? (
-
+
{packetContext.title}
diff --git a/frontend/src/components/RawPacketFeedView.tsx b/frontend/src/components/RawPacketFeedView.tsx index 4dd6a9b..3015c0f 100644 --- a/frontend/src/components/RawPacketFeedView.tsx +++ b/frontend/src/components/RawPacketFeedView.tsx @@ -211,7 +211,9 @@ function getCoverageMessage( function StatTile({ label, value, detail }: { label: string; value: string; detail?: string }) { return (
-
{label}
+
+ {label} +
{value}
{detail ?
{detail}
: null}
@@ -329,7 +331,7 @@ function NeighborList({ : `Last seen ${new Date(item.lastSeen * 1000).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`}
{!isNeighborIdentityResolvable(item, contacts) ? ( -
Identity not resolvable
+
Identity not resolvable
) : null}
{mode !== 'signal' ? ( @@ -363,7 +365,7 @@ function TimelineChart({ bins }: { bins: PacketTimelineBin[] }) {

Traffic Timeline

-
+
{typeOrder.map((type, i) => (
-
+
Coverage
{/* Route type badge */} {getRouteTypeLabel(decoded.routeType)} @@ -117,26 +117,29 @@ export function RawPacketList({ packets, channels, onPacketClick }: RawPacketLis {/* Summary */} {decoded.summary} {/* Time */} - + {formatTime(packet.timestamp)}
{/* Signal info */} {(packet.snr !== null || packet.rssi !== null) && ( -
+
{formatSignalInfo(packet)}
)} {/* Raw hex data (always visible) */} -
+
{packet.data.toUpperCase()}
diff --git a/frontend/src/components/RepeaterDashboard.tsx b/frontend/src/components/RepeaterDashboard.tsx index d3eb138..f9c8231 100644 --- a/frontend/src/components/RepeaterDashboard.tsx +++ b/frontend/src/components/RepeaterDashboard.tsx @@ -181,7 +181,7 @@ export function RepeaterDashboard({ )} {contact && ( -
+
)} @@ -208,7 +208,7 @@ export function RepeaterDashboard({ size="sm" onClick={loadAll} disabled={anyLoading} - className="h-7 px-2 text-[11px] leading-none border-success text-success hover:bg-success/10 hover:text-success sm:h-8 sm:px-3 sm:text-xs" + className="h-7 px-2 text-[0.6875rem] leading-none border-success text-success hover:bg-success/10 hover:text-success sm:h-8 sm:px-3 sm:text-xs" > {anyLoading ? 'Loading...' : 'Load All'} @@ -254,7 +254,7 @@ export function RepeaterDashboard({ aria-hidden="true" /> {notificationsEnabled && ( - + Notifications On )} diff --git a/frontend/src/components/SearchView.tsx b/frontend/src/components/SearchView.tsx index 7932996..9e1d84d 100644 --- a/frontend/src/components/SearchView.tsx +++ b/frontend/src/components/SearchView.tsx @@ -290,7 +290,7 @@ export function SearchView({
{typeBadge} - {convName} - + {convName} + {formatTime(result.received_at)}
-
+
{result.sender_name && !result.outgoing && ( {result.sender_name}: )} diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index e6e4fea..065fd7a 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -584,7 +584,7 @@ export function Sidebar({ contactType={row.contact.type} /> )} - {row.name} + {row.name} {row.notificationsEnabled && ( @@ -594,7 +594,7 @@ export function Sidebar({ {row.unreadCount > 0 && ( @@ -763,7 +763,7 @@ export function Sidebar({
{sortMode === 'distance' && distanceKm !== null ? ( -
+
{distanceKm.toFixed(1)} km away
) : null} {selectedCount > 0 ? ( -
+
Added {selectedCount} time{selectedCount === 1 ? '' : 's'}
) : null} diff --git a/frontend/src/components/repeater/RepeaterTelemetryHistoryPane.tsx b/frontend/src/components/repeater/RepeaterTelemetryHistoryPane.tsx index de7b478..df7c799 100644 --- a/frontend/src/components/repeater/RepeaterTelemetryHistoryPane.tsx +++ b/frontend/src/components/repeater/RepeaterTelemetryHistoryPane.tsx @@ -113,7 +113,7 @@ export function TelemetryHistoryPane({

Telemetry History

{entries.length > 0 && ( - {entries.length} samples + {entries.length} samples )}
@@ -124,10 +124,10 @@ export function TelemetryHistoryPane({ Any time repeater telemetry is fetched, the metrics are stored for 30 days (or 1,000 samples, whichever comes first). This telemetry is stored on normal interactive fetches via the repeater pane, API calls to the endpoint ( - POST /api/contacts/<key>/repeater/status), or - when the repeater is opted into interval telemetry polling, in which case the repeater - will be polled for metrics every 8 hours. You can see which repeaters are opted into - this flow in the{' '} + POST /api/contacts/<key>/repeater/status + ), or when the repeater is opted into interval telemetry polling, in which case the + repeater will be polled for metrics every 8 hours. You can see which repeaters are opted + into this flow in the{' '} setMetric(m)} className={cn( - 'text-[11px] px-2 py-0.5 rounded transition-colors', + 'text-[0.6875rem] px-2 py-0.5 rounded transition-colors', metric === m ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:text-foreground hover:bg-accent' diff --git a/frontend/src/components/repeater/repeaterPaneShared.tsx b/frontend/src/components/repeater/repeaterPaneShared.tsx index 069a283..cac74d1 100644 --- a/frontend/src/components/repeater/repeaterPaneShared.tsx +++ b/frontend/src/components/repeater/repeaterPaneShared.tsx @@ -141,10 +141,10 @@ export function RepeaterPane({

{title}

- {headerNote &&

{headerNote}

} + {headerNote &&

{headerNote}

} {fetchedAt && (

Fetched {formatFetchedTime(fetchedAt)} ({formatFetchedRelative(fetchedAt)}) diff --git a/frontend/src/components/settings/SettingsDatabaseSection.tsx b/frontend/src/components/settings/SettingsDatabaseSection.tsx index 2987daa..0ae6c6e 100644 --- a/frontend/src/components/settings/SettingsDatabaseSection.tsx +++ b/frontend/src/components/settings/SettingsDatabaseSection.tsx @@ -250,7 +250,7 @@ export function SettingsDatabaseSection({

{displayName} - + {key.slice(0, 12)}
diff --git a/frontend/src/components/settings/SettingsFanoutSection.tsx b/frontend/src/components/settings/SettingsFanoutSection.tsx index d163924..18a7dff 100644 --- a/frontend/src/components/settings/SettingsFanoutSection.tsx +++ b/frontend/src/components/settings/SettingsFanoutSection.tsx @@ -537,7 +537,7 @@ function CreateIntegrationDialog({
{sectionedOptions.map((group) => (
-
+
{group.section}
{group.options.map((option) => { @@ -577,7 +577,7 @@ function CreateIntegrationDialog({ {selectedOption ? ( <>
-
+
{selectedOption.section}

{selectedOption.label}

diff --git a/frontend/src/components/settings/SettingsLocalSection.tsx b/frontend/src/components/settings/SettingsLocalSection.tsx index 92d15ed..d6f36ef 100644 --- a/frontend/src/components/settings/SettingsLocalSection.tsx +++ b/frontend/src/components/settings/SettingsLocalSection.tsx @@ -1,8 +1,10 @@ import { useState } from 'react'; -import { Logs, MessageSquare } from 'lucide-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, @@ -238,6 +240,8 @@ export function SettingsLocalSection({ } function ThemePreview({ className }: { className?: string }) { + const [showStyleRef, setShowStyleRef] = useState(false); + return (

@@ -271,7 +275,7 @@ function ThemePreview({ className }: { className?: string }) {

-

Sidebar preview

+

Sidebar preview

} label="Alice" badge={ - + 3 } @@ -298,13 +302,267 @@ function ThemePreview({ className }: { className?: string }) { leading={} label="Mesh Ops" badge={ - + @2 } />
+ + {/* ── 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} +

); } @@ -327,7 +585,7 @@ function PreviewMessage({ return (
- {sender} + {sender}
{text}
@@ -348,7 +606,7 @@ function PreviewSidebarRow({ return (
diff --git a/frontend/src/components/visualizer/VisualizerControls.tsx b/frontend/src/components/visualizer/VisualizerControls.tsx index 62cffa8..8f3f436 100644 --- a/frontend/src/components/visualizer/VisualizerControls.tsx +++ b/frontend/src/components/visualizer/VisualizerControls.tsx @@ -95,7 +95,7 @@ export function VisualizerControls({ {PACKET_LEGEND_ITEMS.map((item) => (
{item.label}