Add canonical style reference. Closes #155.

This commit is contained in:
Jack Kingsman
2026-04-03 14:27:44 -07:00
parent 3ca4f7edf7
commit 42e1b7b5d9
23 changed files with 373 additions and 90 deletions

View File

@@ -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 `<Button>` component. Semantic color overrides (danger, warning, success) use `variant="outline"` with `className="border-{color}/50 text-{color} hover:bg-{color}/10"`.
- **Badges/tags** use `text-[0.625rem] uppercase tracking-wider px-1.5 py-0.5 rounded` with `bg-muted` (neutral) or `bg-primary/10` (active).
- **Clickable text** (copy-to-clipboard, navigational links) uses `role="button" tabIndex={0}` with `cursor-pointer hover:text-primary transition-colors`.
## Security Posture (intentional)
- No authentication UI.

View File

@@ -135,7 +135,7 @@ export function AppShell({
aria-label="Settings"
>
<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">
<h2 className="text-[0.625rem] uppercase tracking-wider text-muted-foreground font-medium">
Settings
</h2>
<button
@@ -158,7 +158,7 @@ export function AppShell({
type="button"
disabled={disabled}
className={cn(
'w-full px-3 py-2 text-left text-[13px] border-l-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset disabled:cursor-not-allowed disabled:opacity-50',
'w-full px-3 py-2 text-left text-[0.8125rem] border-l-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset disabled:cursor-not-allowed disabled:opacity-50',
!disabled && 'hover:bg-accent',
settingsSection === section && !disabled && 'bg-accent border-l-primary'
)}

View File

@@ -49,11 +49,13 @@ export function BulkAddChannelResultModal({
{result && (
<div className="grid grid-cols-2 gap-3 text-sm">
<div className="rounded-md border border-border/70 bg-muted/30 px-3 py-2">
<div className="text-xs uppercase tracking-wide text-muted-foreground">Created</div>
<div className="text-[0.625rem] uppercase tracking-wider font-medium text-muted-foreground">
Created
</div>
<div className="mt-1 font-medium">{createdChannels.length}</div>
</div>
<div className="rounded-md border border-border/70 bg-muted/30 px-3 py-2">
<div className="text-xs uppercase tracking-wide text-muted-foreground">
<div className="text-[0.625rem] uppercase tracking-wider font-medium text-muted-foreground">
Already Present
</div>
<div className="mt-1 font-medium">{result.existing_count}</div>

View File

@@ -107,11 +107,11 @@ export function ChannelInfoPane({
</span>
)}
<div className="flex items-center gap-2 mt-1.5">
<span className="text-[10px] uppercase tracking-wider px-1.5 py-0.5 rounded bg-muted text-muted-foreground font-medium">
<span className="text-[0.625rem] uppercase tracking-wider px-1.5 py-0.5 rounded bg-muted text-muted-foreground font-medium">
{channel.is_hashtag ? 'Hashtag' : 'Private Key'}
</span>
{channel.on_radio && (
<span className="text-[10px] uppercase tracking-wider px-1.5 py-0.5 rounded bg-primary/10 text-primary font-medium">
<span className="text-[0.625rem] uppercase tracking-wider px-1.5 py-0.5 rounded bg-primary/10 text-primary font-medium">
On Radio
</span>
)}
@@ -221,7 +221,7 @@ export function ChannelInfoPane({
function SectionLabel({ children }: { children: React.ReactNode }) {
return (
<h3 className="text-[10px] uppercase tracking-wider text-muted-foreground font-medium mb-1.5">
<h3 className="text-[0.625rem] uppercase tracking-wider text-muted-foreground font-medium mb-1.5">
{children}
</h3>
);
@@ -301,11 +301,13 @@ function HopWidthChart({ stats }: { stats: PathHashWidthStats }) {
className="w-2 h-2 rounded-full flex-shrink-0"
style={{ backgroundColor: d.color }}
/>
<span className="text-[11px] text-muted-foreground flex-1">{d.name}</span>
<span className="text-[11px] font-medium tabular-nums">{d.value.toLocaleString()}</span>
<span className="text-[0.6875rem] text-muted-foreground flex-1">{d.name}</span>
<span className="text-[0.6875rem] font-medium tabular-nums">
{d.value.toLocaleString()}
</span>
</div>
))}
<p className="text-[10px] text-muted-foreground pt-0.5">
<p className="text-[0.625rem] text-muted-foreground pt-0.5">
{stats.total_packets.toLocaleString()} total
</p>
</div>

View File

@@ -198,7 +198,7 @@ export function ChatHeader({
</h2>
{isPrivateChannel && !showKey ? (
<button
className="min-w-0 flex-shrink text-[11px] font-mono text-muted-foreground transition-colors hover:text-primary"
className="min-w-0 flex-shrink text-[0.6875rem] font-mono text-muted-foreground transition-colors hover:text-primary"
onClick={(e) => {
e.stopPropagation();
setShowKey(true);
@@ -209,7 +209,7 @@ export function ChatHeader({
</button>
) : (
<span
className="min-w-0 flex-1 truncate font-mono text-[11px] text-muted-foreground transition-colors hover:text-primary"
className="min-w-0 flex-1 truncate font-mono text-[0.6875rem] text-muted-foreground transition-colors hover:text-primary"
role="button"
tabIndex={0}
onKeyDown={handleKeyboardActivate}
@@ -244,7 +244,7 @@ export function ChatHeader({
className="h-3.5 w-3.5 flex-shrink-0 text-[hsl(var(--region-override))]"
aria-hidden="true"
/>
<span className="min-w-0 truncate text-[11px] font-medium text-[hsl(var(--region-override))]">
<span className="min-w-0 truncate text-[0.6875rem] font-medium text-[hsl(var(--region-override))]">
{activeFloodScopeDisplay}
</span>
</button>
@@ -253,7 +253,7 @@ export function ChatHeader({
</span>
</span>
{conversation.type === 'contact' && activeContact && (
<div className="col-span-2 row-start-2 min-w-0 text-[11px] text-muted-foreground min-[1100px]:col-span-1 min-[1100px]:col-start-2 min-[1100px]:row-start-1">
<div className="col-span-2 row-start-2 min-w-0 text-[0.6875rem] text-muted-foreground min-[1100px]:col-span-1 min-[1100px]:col-start-2 min-[1100px]:row-start-1">
<ContactStatusInfo
contact={activeContact}
ourLat={config?.lat ?? null}
@@ -315,7 +315,7 @@ export function ChatHeader({
aria-hidden="true"
/>
{notificationsEnabled && (
<span className="hidden md:inline text-[11px] font-medium text-status-connected">
<span className="hidden md:inline text-[0.6875rem] font-medium text-status-connected">
Notifications On
</span>
)}
@@ -333,7 +333,7 @@ export function ChatHeader({
aria-hidden="true"
/>
{activeFloodScopeDisplay && (
<span className="hidden text-[11px] font-medium text-[hsl(var(--region-override))] sm:inline">
<span className="hidden text-[0.6875rem] font-medium text-[hsl(var(--region-override))] sm:inline">
{activeFloodScopeDisplay}
</span>
)}

View File

@@ -292,7 +292,7 @@ export function ContactInfoPane({
{contact.public_key}
</span>
<div className="flex items-center gap-2 mt-1.5">
<span className="text-[10px] uppercase tracking-wider px-1.5 py-0.5 rounded bg-muted text-muted-foreground font-medium">
<span className="text-[0.625rem] uppercase tracking-wider px-1.5 py-0.5 rounded bg-muted text-muted-foreground font-medium">
{CONTACT_TYPE_LABELS[contact.type] ?? 'Unknown'}
</span>
</div>
@@ -568,7 +568,7 @@ export function ContactInfoPane({
function SectionLabel({ children }: { children: React.ReactNode }) {
return (
<h3 className="text-[10px] uppercase tracking-wider text-muted-foreground font-medium mb-1.5">
<h3 className="text-[0.625rem] uppercase tracking-wider text-muted-foreground font-medium mb-1.5">
{children}
</h3>
);
@@ -729,7 +729,7 @@ function ActivityChartsSection({ analytics }: { analytics: ContactAnalytics | nu
</div>
)}
<p className="text-[11px] text-muted-foreground">
<p className="text-[0.6875rem] text-muted-foreground">
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<T extends ContactAnalyticsHourlyBucket | ContactAnaly
{legendItems && (
<Legend
content={() => (
<div className="flex flex-wrap justify-center gap-x-3 gap-y-1 mt-1 text-[11px] text-muted-foreground">
<div className="flex flex-wrap justify-center gap-x-3 gap-y-1 mt-1 text-[0.6875rem] text-muted-foreground">
{legendItems.map((item) => (
<span key={item.label} className="inline-flex items-center gap-1.5">
<span

View File

@@ -74,12 +74,12 @@ function RouteCard({
<div className="rounded-md border border-border bg-muted/20 p-3">
<div className="flex items-center justify-between gap-3">
<h4 className="text-sm font-semibold">{label}</h4>
<span className="text-[11px] text-muted-foreground">
<span className="text-[0.6875rem] text-muted-foreground">
{formatRouteLabel(route.path_len, true)}
</span>
</div>
<p className="mt-2 text-sm">{chain}</p>
<div className="mt-2 flex flex-wrap gap-x-3 gap-y-1 text-[11px] text-muted-foreground">
<div className="mt-2 flex flex-wrap gap-x-3 gap-y-1 text-[0.6875rem] text-muted-foreground">
<span>Raw: {rawPath}</span>
<span>{formatPathHashMode(route.path_hash_mode)}</span>
</div>

View File

@@ -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 (
<span
@@ -965,7 +965,7 @@ export function MessageList({
)}
>
{showAvatar && (
<div className="text-[13px] font-semibold text-foreground mb-0.5">
<div className="text-[0.8125rem] font-semibold text-foreground mb-0.5">
{canClickSender ? (
<span
className="cursor-pointer hover:text-primary transition-colors"
@@ -980,7 +980,7 @@ export function MessageList({
) : (
displaySender
)}
<span className="font-normal text-muted-foreground ml-2 text-[11px]">
<span className="font-normal text-muted-foreground ml-2 text-[0.6875rem]">
{formatTime(msg.sender_timestamp || msg.received_at)}
</span>
{!msg.outgoing && msg.paths && msg.paths.length > 0 && (
@@ -1008,7 +1008,7 @@ export function MessageList({
))}
{!showAvatar && (
<>
<span className="text-[10px] text-muted-foreground ml-2">
<span className="text-[0.625rem] text-muted-foreground ml-2">
{formatTime(msg.sender_timestamp || msg.received_at)}
</span>
{!msg.outgoing && msg.paths && msg.paths.length > 0 && (

View File

@@ -221,7 +221,7 @@ export function PathModal({
>
<span className="flex flex-col items-center leading-tight">
<span> Resend</span>
<span className="text-[10px] font-normal opacity-80">
<span className="text-[0.625rem] font-normal opacity-80">
Only repeated by new repeaters
</span>
</span>
@@ -237,7 +237,7 @@ export function PathModal({
>
<span className="flex flex-col items-center leading-tight">
<span> Resend as new</span>
<span className="text-[10px] font-normal opacity-80">
<span className="text-[0.625rem] font-normal opacity-80">
Will appear as duplicate to receivers
</span>
</span>

View File

@@ -312,7 +312,7 @@ function CompactMetaCard({
}) {
return (
<div className="rounded-lg border border-border/70 bg-card/70 p-2.5">
<div className="text-[10px] uppercase tracking-[0.18em] text-muted-foreground">{label}</div>
<div className="text-[0.625rem] uppercase tracking-wider text-muted-foreground">{label}</div>
<div className="mt-1 text-sm font-medium leading-tight text-foreground">{primary}</div>
{secondary ? (
<div className="mt-1 text-xs leading-tight text-muted-foreground">{secondary}</div>
@@ -340,7 +340,7 @@ function FullPacketHex({
const byteRuns = useMemo(() => buildByteRuns(bytes, byteOwners), [byteOwners, bytes]);
return (
<div className="font-mono text-[15px] leading-7 text-foreground">
<div className="font-mono text-[0.9375rem] leading-7 text-foreground">
{byteRuns.map((run, index) => {
const fieldId = run.fieldId;
const palette = fieldId ? colorMap.get(fieldId) : null;
@@ -446,7 +446,9 @@ function FieldBox({
<div className="flex flex-col items-start gap-2 sm:flex-row sm:justify-between">
<div className="min-w-0">
<div className="text-base font-semibold leading-tight text-foreground">{field.name}</div>
<div className="mt-0.5 text-[11px] text-muted-foreground">{formatByteRange(field)}</div>
<div className="mt-0.5 text-[0.6875rem] text-muted-foreground">
{formatByteRange(field)}
</div>
</div>
<div
className={cn(
@@ -464,7 +466,7 @@ function FieldBox({
{field.decryptedMessage ? (
<div className="mt-2 rounded border border-border/50 bg-background/40 p-2">
<div className="text-[10px] uppercase tracking-[0.16em] text-muted-foreground">
<div className="text-[0.625rem] uppercase tracking-wider text-muted-foreground">
{field.name === 'Ciphertext' ? 'Plaintext' : 'Decoded value'}
</div>
<PlaintextContent text={field.decryptedMessage} />
@@ -486,11 +488,13 @@ function FieldBox({
<div className="text-sm font-medium leading-tight text-foreground">
{part.field}
</div>
<div className="mt-0.5 text-[11px] text-muted-foreground">Bits {part.bits}</div>
<div className="mt-0.5 text-[0.6875rem] text-muted-foreground">
Bits {part.bits}
</div>
</div>
<div className="text-right">
<div className="font-mono text-sm text-foreground">{part.binary}</div>
<div className="mt-0.5 text-[11px] text-muted-foreground">{part.value}</div>
<div className="mt-0.5 text-[0.6875rem] text-muted-foreground">{part.value}</div>
</div>
</div>
</div>
@@ -598,7 +602,7 @@ export function RawPacketInspectionPanel({ packet, channels }: RawPacketInspecti
<section className="rounded-lg border border-border/70 bg-card/70 p-3">
<div className="flex flex-wrap items-start justify-between gap-2">
<div className="min-w-0">
<div className="text-[10px] uppercase tracking-[0.18em] text-muted-foreground">
<div className="text-[0.625rem] uppercase tracking-wider text-muted-foreground">
Summary
</div>
<div className="mt-1 text-base font-semibold leading-tight text-foreground">
@@ -611,7 +615,7 @@ export function RawPacketInspectionPanel({ packet, channels }: RawPacketInspecti
</div>
{packetContext ? (
<div className="mt-2 rounded-md border border-border/60 bg-background/35 px-2.5 py-2">
<div className="text-[10px] uppercase tracking-[0.18em] text-muted-foreground">
<div className="text-[0.625rem] uppercase tracking-wider text-muted-foreground">
{packetContext.title}
</div>
<div className="mt-1 text-sm font-medium leading-tight text-foreground">

View File

@@ -211,7 +211,9 @@ function getCoverageMessage(
function StatTile({ label, value, detail }: { label: string; value: string; detail?: string }) {
return (
<div className="break-inside-avoid rounded-lg border border-border/70 bg-card/80 p-3">
<div className="text-[11px] uppercase tracking-wide text-muted-foreground">{label}</div>
<div className="text-[0.625rem] uppercase tracking-wider font-medium text-muted-foreground">
{label}
</div>
<div className="mt-1 text-xl font-semibold tabular-nums text-foreground">{value}</div>
{detail ? <div className="mt-1 text-xs text-muted-foreground">{detail}</div> : null}
</div>
@@ -329,7 +331,7 @@ function NeighborList({
: `Last seen ${new Date(item.lastSeen * 1000).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`}
</div>
{!isNeighborIdentityResolvable(item, contacts) ? (
<div className="text-[11px] text-warning">Identity not resolvable</div>
<div className="text-[0.6875rem] text-warning">Identity not resolvable</div>
) : null}
</div>
{mode !== 'signal' ? (
@@ -363,7 +365,7 @@ function TimelineChart({ bins }: { bins: PacketTimelineBin[] }) {
<section className="mb-4 break-inside-avoid rounded-lg border border-border/70 bg-card/70 p-3">
<div className="flex items-center justify-between gap-3">
<h3 className="text-sm font-semibold text-foreground">Traffic Timeline</h3>
<div className="flex flex-wrap justify-end gap-2 text-[11px] text-muted-foreground">
<div className="flex flex-wrap justify-end gap-2 text-[0.6875rem] text-muted-foreground">
{typeOrder.map((type, i) => (
<span key={type} className="inline-flex items-center gap-1">
<span
@@ -513,7 +515,7 @@ export function RawPacketFeedView({
<div className="break-inside-avoid rounded-lg border border-border/70 bg-card/70 p-3">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<div className="text-[11px] uppercase tracking-wide text-muted-foreground">
<div className="text-[0.625rem] uppercase tracking-wider font-medium text-muted-foreground">
Coverage
</div>
<div

View File

@@ -101,7 +101,7 @@ export function RawPacketList({ packets, channels, onPacketClick }: RawPacketLis
<div className="flex items-center gap-2">
{/* Route type badge */}
<span
className={`text-[10px] font-mono px-1.5 py-0.5 rounded ${getRouteTypeColor(decoded.routeType)}`}
className={`text-[0.625rem] font-mono px-1.5 py-0.5 rounded ${getRouteTypeColor(decoded.routeType)}`}
title={decoded.routeType}
>
{getRouteTypeLabel(decoded.routeType)}
@@ -117,26 +117,29 @@ export function RawPacketList({ packets, channels, onPacketClick }: RawPacketLis
{/* Summary */}
<span
className={cn('text-[13px]', packet.decrypted ? 'text-primary' : 'text-foreground')}
className={cn(
'text-[0.8125rem]',
packet.decrypted ? 'text-primary' : 'text-foreground'
)}
>
{decoded.summary}
</span>
{/* Time */}
<span className="text-muted-foreground ml-auto text-[12px] tabular-nums">
<span className="text-muted-foreground ml-auto text-xs 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 tabular-nums">
<div className="text-[0.6875rem] 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.5 p-1.5 bg-background/60 rounded">
<div className="font-mono text-[0.625rem] break-all text-muted-foreground mt-1.5 p-1.5 bg-background/60 rounded">
{packet.data.toUpperCase()}
</div>
</>

View File

@@ -181,7 +181,7 @@ export function RepeaterDashboard({
)}
</h2>
<span
className="min-w-0 flex-1 truncate font-mono text-[11px] text-muted-foreground transition-colors hover:text-primary"
className="min-w-0 flex-1 truncate font-mono text-[0.6875rem] text-muted-foreground transition-colors hover:text-primary"
role="button"
tabIndex={0}
onKeyDown={handleKeyboardActivate}
@@ -197,7 +197,7 @@ export function RepeaterDashboard({
</span>
</span>
{contact && (
<div className="col-span-2 row-start-2 min-w-0 text-[11px] text-muted-foreground min-[1100px]:col-span-1 min-[1100px]:col-start-2 min-[1100px]:row-start-1">
<div className="col-span-2 row-start-2 min-w-0 text-[0.6875rem] text-muted-foreground min-[1100px]:col-span-1 min-[1100px]:col-start-2 min-[1100px]:row-start-1">
<ContactStatusInfo contact={contact} ourLat={radioLat} ourLon={radioLon} />
</div>
)}
@@ -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'}
</Button>
@@ -254,7 +254,7 @@ export function RepeaterDashboard({
aria-hidden="true"
/>
{notificationsEnabled && (
<span className="hidden md:inline text-[11px] font-medium text-status-connected">
<span className="hidden md:inline text-[0.6875rem] font-medium text-status-connected">
Notifications On
</span>
)}

View File

@@ -290,7 +290,7 @@ export function SearchView({
<div className="flex items-center gap-2 mb-1">
<span
className={cn(
'text-[10px] font-medium px-1.5 py-0.5 rounded',
'text-[0.625rem] font-medium px-1.5 py-0.5 rounded',
result.type === 'CHAN'
? 'bg-primary/20 text-primary'
: 'bg-secondary text-secondary-foreground'
@@ -298,12 +298,12 @@ export function SearchView({
>
{typeBadge}
</span>
<span className="text-[12px] font-medium text-foreground truncate">{convName}</span>
<span className="text-[11px] text-muted-foreground ml-auto flex-shrink-0">
<span className="text-xs font-medium text-foreground truncate">{convName}</span>
<span className="text-[0.6875rem] text-muted-foreground ml-auto flex-shrink-0">
{formatTime(result.received_at)}
</span>
</div>
<div className="text-[13px] text-foreground/80 line-clamp-2 break-words">
<div className="text-[0.8125rem] text-foreground/80 line-clamp-2 break-words">
{result.sender_name && !result.outgoing && (
<span className="text-muted-foreground">{result.sender_name}: </span>
)}

View File

@@ -584,7 +584,7 @@ export function Sidebar({
contactType={row.contact.type}
/>
)}
<span className="name flex-1 truncate text-[13px]">{row.name}</span>
<span className="name flex-1 truncate text-[0.8125rem]">{row.name}</span>
<span className="ml-auto flex items-center gap-1">
{row.notificationsEnabled && (
<span aria-label="Notifications enabled" title="Notifications enabled">
@@ -594,7 +594,7 @@ export function Sidebar({
{row.unreadCount > 0 && (
<span
className={cn(
'text-[10px] font-semibold px-1.5 py-0.5 rounded-full min-w-[18px] text-center',
'text-[0.625rem] font-semibold px-1.5 py-0.5 rounded-full min-w-[18px] text-center',
highlightUnread
? 'bg-badge-mention text-badge-mention-foreground'
: 'bg-badge-unread/90 text-badge-unread-foreground'
@@ -626,7 +626,7 @@ export function Sidebar({
key={key}
data-active={active ? 'true' : undefined}
className={cn(
'sidebar-action-row px-3 py-2 cursor-pointer flex items-center gap-2 border-l-2 border-transparent hover:bg-accent transition-colors text-[13px] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
'sidebar-action-row px-3 py-2 cursor-pointer flex items-center gap-2 border-l-2 border-transparent hover:bg-accent transition-colors text-[0.8125rem] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
active && 'bg-accent border-l-primary'
)}
role="button"
@@ -735,7 +735,7 @@ export function Sidebar({
{showCracker ? 'Hide' : 'Show'} Channel Finder
<span
className={cn(
'ml-1 text-[11px]',
'ml-1 text-[0.6875rem]',
crackerRunning ? 'text-primary' : 'text-muted-foreground'
)}
>
@@ -763,7 +763,7 @@ export function Sidebar({
<div className="flex justify-between items-center px-3 py-2 pt-3.5">
<button
className={cn(
'flex items-center gap-1.5 text-[10px] uppercase tracking-wider text-muted-foreground hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded',
'flex items-center gap-1.5 text-[0.625rem] uppercase tracking-wider text-muted-foreground hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded',
isSearching && 'cursor-default'
)}
aria-expanded={!effectiveCollapsed}
@@ -783,7 +783,7 @@ export function Sidebar({
<div className="ml-auto flex items-center gap-1.5">
{sortSection && sectionSortOrder && (
<button
className="bg-transparent text-muted-foreground/60 px-1 py-0.5 text-[10px] rounded hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
className="bg-transparent text-muted-foreground/60 px-1 py-0.5 text-[0.625rem] rounded hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
onClick={() => handleSortToggle(sortSection)}
aria-label={
sectionSortOrder === 'alpha'
@@ -802,7 +802,7 @@ export function Sidebar({
{unreadCount > 0 && (
<span
className={cn(
'text-[10px] font-medium px-1.5 py-0.5 rounded-full',
'text-[0.625rem] font-medium px-1.5 py-0.5 rounded-full',
highlightUnread
? 'bg-badge-mention text-badge-mention-foreground'
: 'bg-secondary text-muted-foreground'
@@ -831,7 +831,7 @@ export function Sidebar({
onClick={onNewMessage}
title="Add channel or contact"
aria-label="Add channel or contact"
className="h-8 w-full justify-start gap-2 border-primary/20 bg-primary/5 px-3 text-[13px] text-primary hover:bg-primary/10 hover:text-primary"
className="h-8 w-full justify-start gap-2 border-primary/20 bg-primary/5 px-3 text-[0.8125rem] text-primary hover:bg-primary/10 hover:text-primary"
>
<SquarePen className="h-4 w-4" />
<span>Add Channel/Contact</span>
@@ -848,7 +848,7 @@ export function Sidebar({
aria-label="Search conversations"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className={cn('h-7 text-[13px] bg-background/50', searchQuery ? 'pr-8' : 'pr-3')}
className={cn('h-7 text-[0.8125rem] bg-background/50', searchQuery ? 'pr-8' : 'pr-3')}
/>
{searchQuery && (
<button
@@ -874,7 +874,7 @@ export function Sidebar({
{/* Mark All Read */}
{!query && Object.values(unreadCounts).some((c) => c > 0) && (
<div
className="px-3 py-2 cursor-pointer flex items-center gap-2 border-l-2 border-transparent hover:bg-accent transition-colors text-[13px] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
className="px-3 py-2 cursor-pointer flex items-center gap-2 border-l-2 border-transparent hover:bg-accent transition-colors text-[0.8125rem] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
role="button"
tabIndex={0}
onKeyDown={handleKeyboardActivate}

View File

@@ -123,7 +123,7 @@ export function StatusBar({
<div className="hidden lg:flex items-center gap-2 text-muted-foreground">
<span className="text-foreground font-medium">{config.name || 'Unnamed'}</span>
<span
className="font-mono text-[11px] text-muted-foreground cursor-pointer hover:text-primary transition-colors"
className="font-mono text-[0.6875rem] text-muted-foreground cursor-pointer hover:text-primary transition-colors"
role="button"
tabIndex={0}
onKeyDown={handleKeyboardActivate}

View File

@@ -118,7 +118,7 @@ function TraceNodeRow({
>
<div
className={cn(
'flex h-9 w-9 items-center justify-center rounded-full border text-[11px] font-semibold uppercase tracking-wide',
'flex h-9 w-9 items-center justify-center rounded-full border text-[0.6875rem] font-semibold uppercase tracking-wide',
fixed
? 'border-primary/30 bg-primary/10 text-primary'
: 'border-border bg-muted text-muted-foreground'
@@ -129,12 +129,12 @@ function TraceNodeRow({
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-medium">{title}</div>
<div className="truncate text-xs text-muted-foreground">{subtitle}</div>
{meta ? <div className="mt-1 text-[11px] text-muted-foreground">{meta}</div> : null}
{note ? <div className="mt-1 text-[11px] text-muted-foreground">{note}</div> : null}
{meta ? <div className="mt-1 text-[0.6875rem] text-muted-foreground">{meta}</div> : null}
{note ? <div className="mt-1 text-[0.6875rem] text-muted-foreground">{note}</div> : null}
</div>
{snr ? (
<div className="shrink-0 text-right">
<div className="text-[11px] text-muted-foreground">SNR</div>
<div className="text-[0.6875rem] text-muted-foreground">SNR</div>
<div className="font-mono text-sm">{snr}</div>
</div>
) : null}
@@ -370,7 +370,7 @@ export function TracePane({ contacts, config, onRunTracePath }: TracePaneProps)
))}
</div>
{sortMode === 'distance' && !canSortByDistance ? (
<p className="mt-2 text-[11px] text-muted-foreground">
<p className="mt-2 text-[0.6875rem] text-muted-foreground">
Distance sorting is using known repeater coordinates, but the local radio does not
currently have a valid location.
</p>
@@ -421,12 +421,12 @@ export function TracePane({ contacts, config, onRunTracePath }: TracePaneProps)
{getShortKey(contact.public_key)}
</div>
{sortMode === 'distance' && distanceKm !== null ? (
<div className="mt-1 text-[11px] text-muted-foreground">
<div className="mt-1 text-[0.6875rem] text-muted-foreground">
{distanceKm.toFixed(1)} km away
</div>
) : null}
{selectedCount > 0 ? (
<div className="mt-1 text-[11px] text-muted-foreground">
<div className="mt-1 text-[0.6875rem] text-muted-foreground">
Added {selectedCount} time{selectedCount === 1 ? '' : 's'}
</div>
) : null}

View File

@@ -113,7 +113,7 @@ export function TelemetryHistoryPane({
<div className="flex items-center gap-2">
<h3 className="text-sm font-medium">Telemetry History</h3>
{entries.length > 0 && (
<span className="text-[10px] text-muted-foreground">{entries.length} samples</span>
<span className="text-[0.625rem] text-muted-foreground">{entries.length} samples</span>
)}
</div>
</div>
@@ -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 (
<code className="text-[11px]">POST /api/contacts/&lt;key&gt;/repeater/status</code>), 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{' '}
<code className="text-[0.6875rem]">POST /api/contacts/&lt;key&gt;/repeater/status</code>
), 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{' '}
<a
href="#settings/database"
className="underline text-primary hover:text-primary/80 transition-colors"
@@ -179,7 +179,7 @@ export function TelemetryHistoryPane({
type="button"
onClick={() => 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'

View File

@@ -141,10 +141,10 @@ export function RepeaterPane({
<div className="flex items-center justify-between px-3 py-2 bg-muted/50 border-b border-border">
<div className="min-w-0">
<h3 className="text-sm font-medium">{title}</h3>
{headerNote && <p className="text-[11px] text-muted-foreground">{headerNote}</p>}
{headerNote && <p className="text-[0.6875rem] text-muted-foreground">{headerNote}</p>}
{fetchedAt && (
<p
className="text-[11px] text-muted-foreground"
className="text-[0.6875rem] text-muted-foreground"
title={new Date(fetchedAt).toLocaleString()}
>
Fetched {formatFetchedTime(fetchedAt)} ({formatFetchedRelative(fetchedAt)})

View File

@@ -250,7 +250,7 @@ export function SettingsDatabaseSection({
<div key={key} className="flex items-center justify-between gap-2">
<div className="flex-1 min-w-0">
<span className="text-sm truncate block">{displayName}</span>
<span className="text-[10px] text-muted-foreground font-mono">
<span className="text-[0.625rem] text-muted-foreground font-mono">
{key.slice(0, 12)}
</span>
</div>

View File

@@ -537,7 +537,7 @@ function CreateIntegrationDialog({
<div className="space-y-4">
{sectionedOptions.map((group) => (
<div key={group.section} className="space-y-1.5">
<div className="px-2 text-[11px] font-semibold uppercase tracking-[0.12em] text-muted-foreground">
<div className="px-2 text-[0.6875rem] font-semibold uppercase tracking-wider text-muted-foreground">
{group.section}
</div>
{group.options.map((option) => {
@@ -577,7 +577,7 @@ function CreateIntegrationDialog({
{selectedOption ? (
<>
<div className="space-y-1.5">
<div className="text-xs font-semibold uppercase tracking-[0.12em] text-muted-foreground">
<div className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
{selectedOption.section}
</div>
<h3 className="text-lg font-semibold">{selectedOption.label}</h3>

View File

@@ -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 (
<div className={`rounded-lg border border-border bg-card p-3 ${className ?? ''}`}>
<p className="text-xs text-muted-foreground mb-3">
@@ -271,7 +275,7 @@ function ThemePreview({ className }: { className?: string }) {
</div>
<div className="mt-4 rounded-md border border-border bg-background p-2">
<p className="mb-2 text-[11px] font-medium text-muted-foreground">Sidebar preview</p>
<p className="mb-2 text-[0.6875rem] font-medium text-muted-foreground">Sidebar preview</p>
<div className="space-y-1">
<PreviewSidebarRow
active
@@ -289,7 +293,7 @@ function ThemePreview({ className }: { className?: string }) {
leading={<ContactAvatar name="Alice" publicKey={'ab'.repeat(32)} size={24} />}
label="Alice"
badge={
<span className="rounded-full bg-badge-unread/90 px-1.5 py-0.5 text-[10px] font-semibold text-badge-unread-foreground">
<span className="rounded-full bg-badge-unread/90 px-1.5 py-0.5 text-[0.625rem] font-semibold text-badge-unread-foreground">
3
</span>
}
@@ -298,13 +302,267 @@ function ThemePreview({ className }: { className?: string }) {
leading={<ContactAvatar name="Mesh Ops" publicKey={'cd'.repeat(32)} size={24} />}
label="Mesh Ops"
badge={
<span className="rounded-full bg-badge-mention px-1.5 py-0.5 text-[10px] font-semibold text-badge-mention-foreground">
<span className="rounded-full bg-badge-mention px-1.5 py-0.5 text-[0.625rem] font-semibold text-badge-mention-foreground">
@2
</span>
}
/>
</div>
</div>
{/* ── Style Reference (collapsible) ── */}
<button
type="button"
onClick={() => setShowStyleRef((v) => !v)}
className="mt-4 flex w-full items-center gap-1.5 text-[0.6875rem] font-medium text-muted-foreground hover:text-foreground transition-colors"
>
<ChevronRight
className={cn('h-3.5 w-3.5 transition-transform', showStyleRef && 'rotate-90')}
/>
Canonical style reference
</button>
{showStyleRef && (
<>
{/* ── Text Hierarchy ── */}
<PreviewSection title="Text hierarchy">
<div className="space-y-2">
<PreviewTextRow
classes="text-xl font-semibold"
label="text-xl font-semibold"
desc="Hero / large data"
/>
<PreviewTextRow
classes="text-lg font-semibold"
label="text-lg font-semibold"
desc="Sheet / dialog title"
/>
<PreviewTextRow
classes="text-base font-semibold"
label="text-base font-semibold"
desc="Section title"
/>
<PreviewTextRow classes="text-sm" label="text-sm" desc="Body text, form labels" />
<PreviewTextRow
classes="text-xs text-muted-foreground"
label="text-xs text-muted-foreground"
desc="Helper text"
/>
<PreviewTextRow
classes="text-[0.6875rem] text-muted-foreground"
label="text-[0.6875rem] text-muted-foreground"
desc="Metadata, timestamps"
/>
<div>
<p className="text-[0.625rem] uppercase tracking-wider text-muted-foreground font-medium">
Section Label
</p>
<p className="text-[0.625rem] text-muted-foreground/60 mt-0.5">
text-[0.625rem] uppercase tracking-wider text-muted-foreground font-medium
</p>
</div>
</div>
</PreviewSection>
{/* ── Mono Text ── */}
<PreviewSection title="Mono text">
<div className="space-y-1.5">
<div>
<p className="text-xs font-mono text-muted-foreground">
a1b2c3d4e5f6...7890abcdef01
</p>
<p className="text-[0.625rem] text-muted-foreground/60">
text-xs font-mono keys, identifiers
</p>
</div>
<div>
<p className="text-[0.6875rem] font-mono">1h 23m 45s uptime</p>
<p className="text-[0.625rem] text-muted-foreground/60">
text-[0.6875rem] font-mono metadata mono
</p>
</div>
<div>
<p className="text-sm font-mono">$ req_status_sync 0xA1B2...</p>
<p className="text-[0.625rem] text-muted-foreground/60">
text-sm font-mono console / code
</p>
</div>
</div>
</PreviewSection>
{/* ── Badges ── */}
<PreviewSection title="Badges and tags">
<div className="flex flex-wrap items-center gap-2">
<span className="text-[0.625rem] uppercase tracking-wider px-1.5 py-0.5 rounded bg-muted text-muted-foreground font-medium">
Hashtag
</span>
<span className="text-[0.625rem] uppercase tracking-wider px-1.5 py-0.5 rounded bg-muted text-muted-foreground font-medium">
Repeater
</span>
<span className="text-[0.625rem] uppercase tracking-wider px-1.5 py-0.5 rounded bg-primary/10 text-primary font-medium">
On Radio
</span>
<span className="rounded-full bg-badge-unread/90 px-1.5 py-0.5 text-[0.625rem] font-semibold text-badge-unread-foreground">
3
</span>
<span className="rounded-full bg-badge-mention px-1.5 py-0.5 text-[0.625rem] font-semibold text-badge-mention-foreground">
@2
</span>
</div>
<p className="text-[0.625rem] text-muted-foreground/60 mt-1.5">
Muted: bg-muted &middot; Primary: bg-primary/10 &middot; Unread/Mention: bg-badge-*
</p>
</PreviewSection>
{/* ── Buttons ── */}
<PreviewSection title="Buttons">
<div className="space-y-3">
<div>
<p className="text-[0.625rem] text-muted-foreground/60 mb-1.5">
Standard variants (size sm)
</p>
<div className="flex flex-wrap gap-1.5">
<Button size="sm">Default</Button>
<Button size="sm" variant="outline">
Outline
</Button>
<Button size="sm" variant="secondary">
Secondary
</Button>
<Button size="sm" variant="destructive">
Destructive
</Button>
<Button size="sm" variant="ghost">
Ghost
</Button>
<Button size="icon" variant="outline">
<Settings className="h-4 w-4" />
</Button>
<Button size="icon" variant="outline">
<Send className="h-4 w-4" />
</Button>
</div>
</div>
<div>
<p className="text-[0.625rem] text-muted-foreground/60 mb-1.5">
Semantic outline variants
</p>
<div className="flex flex-wrap gap-1.5">
<Button
size="sm"
variant="outline"
className="border-destructive/50 text-destructive hover:bg-destructive/10"
>
Danger
</Button>
<Button
size="sm"
variant="outline"
className="border-warning/50 text-warning hover:bg-warning/10"
>
Warning
</Button>
<Button
size="sm"
variant="outline"
className="border-green-600/50 text-green-600 hover:bg-green-600/10"
>
Success
</Button>
</div>
</div>
<div>
<p className="text-[0.625rem] text-muted-foreground/60 mb-1.5">
Metric selector pills
</p>
<div className="flex gap-1">
{['Voltage', 'Noise Floor', 'Packets'].map((label, i) => (
<button
key={label}
type="button"
className={cn(
'text-[0.6875rem] px-2 py-0.5 rounded transition-colors',
i === 0
? 'bg-primary text-primary-foreground'
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
)}
>
{label}
</button>
))}
</div>
</div>
</div>
</PreviewSection>
{/* ── Clickable Text ── */}
<PreviewSection title="Clickable text">
<div className="space-y-1.5">
<span
role="button"
tabIndex={0}
className="text-xs font-mono text-muted-foreground cursor-pointer hover:text-primary transition-colors block"
>
a1b2c3d4e5f6 (click to copy)
</span>
<span
role="button"
tabIndex={0}
className="text-sm cursor-pointer underline underline-offset-2 decoration-muted-foreground/50 hover:text-primary transition-colors"
>
Underlined navigational link
</span>
</div>
<p className="text-[0.625rem] text-muted-foreground/60 mt-1.5">
cursor-pointer hover:text-primary transition-colors use role=&quot;button&quot; +
tabIndex
</p>
</PreviewSection>
{/* ── Inline Alerts ── */}
<PreviewSection title="Inline alerts">
<div className="space-y-1.5">
<div className="rounded-md border border-info/30 bg-info/10 px-3 py-2 text-xs text-info">
Info: channel slot cache refreshed from radio.
</div>
<div className="rounded-md border border-warning/30 bg-warning/10 px-3 py-2 text-xs text-warning">
Warning: radio clock skew detected.
</div>
<div className="rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive">
Error: post-connect setup timed out. Reboot the radio and restart.
</div>
</div>
</PreviewSection>
</>
)}
</div>
);
}
function PreviewSection({ title, children }: { title: string; children: React.ReactNode }) {
return (
<div className="mt-4 rounded-md border border-border bg-background p-2">
<p className="mb-2 text-[0.6875rem] font-medium text-muted-foreground">{title}</p>
{children}
</div>
);
}
function PreviewTextRow({
classes,
label,
desc,
}: {
classes: string;
label: string;
desc: string;
}) {
return (
<div>
<p className={classes}>Sample text at this size</p>
<p className="text-[0.625rem] text-muted-foreground/60">
{label} {desc}
</p>
</div>
);
}
@@ -327,7 +585,7 @@ function PreviewMessage({
return (
<div className={`flex ${alignRight ? 'justify-end' : 'justify-start'}`}>
<div className={`max-w-[85%] ${alignRight ? 'items-end' : 'items-start'} flex flex-col`}>
<span className="mb-1 text-[11px] text-muted-foreground">{sender}</span>
<span className="mb-1 text-[0.6875rem] text-muted-foreground">{sender}</span>
<div className={`rounded-2xl px-3 py-2 text-sm break-words ${bubbleClassName}`}>{text}</div>
</div>
</div>
@@ -348,7 +606,7 @@ function PreviewSidebarRow({
return (
<div
data-active={active ? 'true' : undefined}
className={`sidebar-action-row flex items-center gap-2 rounded-md border-l-2 px-3 py-2 text-[13px] ${
className={`sidebar-action-row flex items-center gap-2 rounded-md border-l-2 px-3 py-2 text-[0.8125rem] ${
active ? 'border-l-primary bg-accent text-foreground' : 'border-l-transparent'
}`}
>

View File

@@ -95,7 +95,7 @@ export function VisualizerControls({
{PACKET_LEGEND_ITEMS.map((item) => (
<div key={item.label} className="flex items-center gap-2">
<div
className="w-5 h-5 rounded-full flex items-center justify-center text-[8px] font-bold text-white"
className="w-5 h-5 rounded-full flex items-center justify-center text-[0.5rem] font-bold text-white"
style={{ backgroundColor: item.color }}
>
{item.label}