mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-05-01 02:53:00 +02:00
Add canonical style reference. Closes #155.
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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'
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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/<key>/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/<key>/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'
|
||||
|
||||
@@ -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)})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 · Primary: bg-primary/10 · 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="button" +
|
||||
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'
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user