mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-05-01 19:12:57 +02:00
716 lines
26 KiB
TypeScript
716 lines
26 KiB
TypeScript
import { useState } from 'react';
|
|
import { ChevronRight, Logs, MessageSquare, Send, Settings } from 'lucide-react';
|
|
import { Button } from '../ui/button';
|
|
import { Input } from '../ui/input';
|
|
import { Label } from '../ui/label';
|
|
import { Separator } from '../ui/separator';
|
|
import { cn } from '../../lib/utils';
|
|
import { ContactAvatar } from '../ContactAvatar';
|
|
import {
|
|
captureLastViewedConversationFromHash,
|
|
getReopenLastConversationEnabled,
|
|
setReopenLastConversationEnabled,
|
|
} from '../../utils/lastViewedConversation';
|
|
import { ThemeSelector } from './ThemeSelector';
|
|
import { getLocalLabel, setLocalLabel, type LocalLabel } from '../../utils/localLabel';
|
|
import {
|
|
DISTANCE_UNIT_LABELS,
|
|
DISTANCE_UNITS,
|
|
setSavedDistanceUnit,
|
|
} from '../../utils/distanceUnits';
|
|
import { useDistanceUnit } from '../../contexts/DistanceUnitContext';
|
|
import {
|
|
DEFAULT_FONT_SCALE,
|
|
FONT_SCALE_SLIDER_STEP,
|
|
MAX_FONT_SCALE,
|
|
MIN_FONT_SCALE,
|
|
getSavedFontScale,
|
|
setSavedFontScale,
|
|
} from '../../utils/fontScale';
|
|
import { getAutoFocusInputEnabled, setAutoFocusInputEnabled } from '../../utils/autoFocusInput';
|
|
import {
|
|
BATTERY_DISPLAY_CHANGE_EVENT,
|
|
getShowBatteryPercent,
|
|
setShowBatteryPercent as saveBatteryPercent,
|
|
getShowBatteryVoltage,
|
|
setShowBatteryVoltage as saveBatteryVoltage,
|
|
} from '../../utils/batteryDisplay';
|
|
import {
|
|
STATUS_DOT_PULSE_CHANGE_EVENT,
|
|
getStatusDotPulseEnabled,
|
|
setStatusDotPulseEnabled as saveStatusDotPulse,
|
|
} from '../../utils/statusDotPulse';
|
|
|
|
export function SettingsLocalSection({
|
|
onLocalLabelChange,
|
|
className,
|
|
}: {
|
|
onLocalLabelChange?: (label: LocalLabel) => void;
|
|
className?: string;
|
|
}) {
|
|
const { distanceUnit, setDistanceUnit } = useDistanceUnit();
|
|
const [reopenLastConversation, setReopenLastConversation] = useState(
|
|
getReopenLastConversationEnabled
|
|
);
|
|
const [localLabelText, setLocalLabelText] = useState(() => getLocalLabel().text);
|
|
const [localLabelColor, setLocalLabelColor] = useState(() => getLocalLabel().color);
|
|
const [autoFocusInput, setAutoFocusInput] = useState(getAutoFocusInputEnabled);
|
|
const [batteryPercent, setBatteryPercent] = useState(getShowBatteryPercent);
|
|
const [batteryVoltage, setBatteryVoltage] = useState(getShowBatteryVoltage);
|
|
const [statusDotPulse, setStatusDotPulse] = useState(getStatusDotPulseEnabled);
|
|
const [fontScale, setFontScale] = useState(getSavedFontScale);
|
|
const [fontScaleSlider, setFontScaleSlider] = useState(getSavedFontScale);
|
|
const [fontScaleInput, setFontScaleInput] = useState(() => String(getSavedFontScale()));
|
|
|
|
const commitFontScale = (nextScale: number) => {
|
|
const normalized = setSavedFontScale(nextScale);
|
|
setFontScale(normalized);
|
|
setFontScaleSlider(normalized);
|
|
setFontScaleInput(String(normalized));
|
|
};
|
|
|
|
const restoreFontScaleInput = () => {
|
|
setFontScaleInput(String(fontScale));
|
|
};
|
|
|
|
const handleSliderChange = (nextScale: number) => {
|
|
setFontScaleSlider(nextScale);
|
|
setFontScaleInput(String(nextScale));
|
|
};
|
|
|
|
const handleSliderCommit = (nextScale: number) => {
|
|
commitFontScale(nextScale);
|
|
};
|
|
|
|
const handleToggleReopenLastConversation = (enabled: boolean) => {
|
|
setReopenLastConversation(enabled);
|
|
setReopenLastConversationEnabled(enabled);
|
|
if (enabled) {
|
|
captureLastViewedConversationFromHash();
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className={className}>
|
|
<p className="text-sm text-muted-foreground">
|
|
These settings apply only to this device/browser.
|
|
</p>
|
|
|
|
<div className="space-y-1">
|
|
<Label>Color Scheme</Label>
|
|
<ThemeSelector />
|
|
<ThemePreview className="mt-6" />
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
<div className="space-y-3">
|
|
<Label>Local Label</Label>
|
|
<div className="flex items-center gap-2">
|
|
<Input
|
|
value={localLabelText}
|
|
onChange={(e) => {
|
|
const text = e.target.value;
|
|
setLocalLabelText(text);
|
|
setLocalLabel(text, localLabelColor);
|
|
onLocalLabelChange?.({ text, color: localLabelColor });
|
|
}}
|
|
placeholder="e.g. Home Base, Field Radio 2"
|
|
aria-label="Local label text"
|
|
className="flex-1"
|
|
/>
|
|
<input
|
|
type="color"
|
|
value={localLabelColor}
|
|
onChange={(e) => {
|
|
const color = e.target.value;
|
|
setLocalLabelColor(color);
|
|
setLocalLabel(localLabelText, color);
|
|
onLocalLabelChange?.({ text: localLabelText, color });
|
|
}}
|
|
aria-label="Local label color"
|
|
className="w-10 h-9 rounded border border-input cursor-pointer bg-transparent p-0.5"
|
|
/>
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
Display a colored banner at the top of the page to identify this instance.
|
|
</p>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
<div className="space-y-3">
|
|
<Label htmlFor="distance-units">Distance Units</Label>
|
|
<select
|
|
id="distance-units"
|
|
value={distanceUnit}
|
|
onChange={(event) => {
|
|
const nextUnit = event.target.value as (typeof DISTANCE_UNITS)[number];
|
|
setSavedDistanceUnit(nextUnit);
|
|
setDistanceUnit(nextUnit);
|
|
}}
|
|
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
|
|
>
|
|
{DISTANCE_UNITS.map((unit) => (
|
|
<option key={unit} value={unit}>
|
|
{DISTANCE_UNIT_LABELS[unit]}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<p className="text-xs text-muted-foreground">
|
|
Controls how distances are shown throughout the app.
|
|
</p>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
<div className="space-y-3">
|
|
<Label>UI Tweaks</Label>
|
|
|
|
<label className="flex items-center gap-3 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={reopenLastConversation}
|
|
onChange={(e) => handleToggleReopenLastConversation(e.target.checked)}
|
|
className="w-4 h-4 rounded border-input accent-primary"
|
|
/>
|
|
<span className="text-sm">Reopen to last viewed channel/conversation</span>
|
|
</label>
|
|
|
|
<label className="flex items-center gap-3 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={autoFocusInput}
|
|
onChange={(e) => {
|
|
const v = e.target.checked;
|
|
setAutoFocusInput(v);
|
|
setAutoFocusInputEnabled(v);
|
|
}}
|
|
className="w-4 h-4 rounded border-input accent-primary"
|
|
/>
|
|
<span className="text-sm">Auto-focus input on conversation load (desktop only)</span>
|
|
</label>
|
|
|
|
<label className="flex items-center gap-3 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={batteryPercent}
|
|
onChange={(e) => {
|
|
const v = e.target.checked;
|
|
setBatteryPercent(v);
|
|
saveBatteryPercent(v);
|
|
window.dispatchEvent(new Event(BATTERY_DISPLAY_CHANGE_EVENT));
|
|
}}
|
|
className="w-4 h-4 rounded border-input accent-primary"
|
|
/>
|
|
<span className="text-sm">Show battery percentage in status bar</span>
|
|
</label>
|
|
|
|
<label className="flex items-center gap-3 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={batteryVoltage}
|
|
onChange={(e) => {
|
|
const v = e.target.checked;
|
|
setBatteryVoltage(v);
|
|
saveBatteryVoltage(v);
|
|
window.dispatchEvent(new Event(BATTERY_DISPLAY_CHANGE_EVENT));
|
|
}}
|
|
className="w-4 h-4 rounded border-input accent-primary"
|
|
/>
|
|
<span className="text-sm">Show battery voltage in status bar</span>
|
|
</label>
|
|
|
|
{(batteryPercent || batteryVoltage) && (
|
|
<p className="text-xs text-muted-foreground ml-7">
|
|
Battery data updates every 60 seconds and may take up to a minute to appear after
|
|
connecting.
|
|
</p>
|
|
)}
|
|
|
|
<label className="flex items-center gap-3 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={statusDotPulse}
|
|
onChange={(e) => {
|
|
const v = e.target.checked;
|
|
setStatusDotPulse(v);
|
|
saveStatusDotPulse(v);
|
|
window.dispatchEvent(new Event(STATUS_DOT_PULSE_CHANGE_EVENT));
|
|
}}
|
|
className="w-4 h-4 rounded border-input accent-primary"
|
|
/>
|
|
<span className="text-sm">
|
|
Glitter status dot as packets arrive (blue = channel, purple = DM, cyan = advert, dark
|
|
green = other)
|
|
</span>
|
|
</label>
|
|
|
|
<div className="space-y-3">
|
|
<Label htmlFor="font-scale-input">Relative Font Size</Label>
|
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
|
|
<input
|
|
type="range"
|
|
min={MIN_FONT_SCALE}
|
|
max={MAX_FONT_SCALE}
|
|
step={FONT_SCALE_SLIDER_STEP}
|
|
value={fontScaleSlider}
|
|
onChange={(event) => handleSliderChange(Number(event.target.value))}
|
|
onMouseUp={(event) => handleSliderCommit(Number(event.currentTarget.value))}
|
|
onTouchEnd={(event) => handleSliderCommit(Number(event.currentTarget.value))}
|
|
onKeyUp={(event) => handleSliderCommit(Number(event.currentTarget.value))}
|
|
onBlur={(event) => handleSliderCommit(Number(event.currentTarget.value))}
|
|
aria-label="Relative font size slider"
|
|
className="w-full accent-primary sm:flex-1"
|
|
/>
|
|
<div className="flex items-center gap-2 sm:w-40">
|
|
<Input
|
|
id="font-scale-input"
|
|
type="number"
|
|
inputMode="decimal"
|
|
min={MIN_FONT_SCALE}
|
|
max={MAX_FONT_SCALE}
|
|
step="any"
|
|
value={fontScaleInput}
|
|
onChange={(event) => {
|
|
const nextValue = event.target.value;
|
|
setFontScaleInput(nextValue);
|
|
|
|
if (nextValue === '') {
|
|
return;
|
|
}
|
|
|
|
if (event.target.validity.valid && Number.isFinite(event.target.valueAsNumber)) {
|
|
commitFontScale(event.target.valueAsNumber);
|
|
}
|
|
}}
|
|
onBlur={() => {
|
|
const parsed = Number.parseFloat(fontScaleInput);
|
|
if (!Number.isFinite(parsed)) {
|
|
restoreFontScaleInput();
|
|
return;
|
|
}
|
|
commitFontScale(parsed);
|
|
}}
|
|
onKeyDown={(event) => {
|
|
if (event.key !== 'Enter') {
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
const parsed = Number.parseFloat(fontScaleInput);
|
|
if (!Number.isFinite(parsed)) {
|
|
restoreFontScaleInput();
|
|
return;
|
|
}
|
|
commitFontScale(parsed);
|
|
}}
|
|
aria-label="Relative font size percentage"
|
|
/>
|
|
<span className="text-sm text-muted-foreground">%</span>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => commitFontScale(DEFAULT_FONT_SCALE)}
|
|
className="inline-flex h-9 items-center justify-center rounded-md border border-input px-3 text-sm font-medium transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
|
disabled={fontScale === DEFAULT_FONT_SCALE}
|
|
>
|
|
Reset
|
|
</button>
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
Scales the app's typography for this browser only. The slider moves in 5% steps;
|
|
the number field accepts any value from 25% to 400%.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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">
|
|
Preview alert, message, sidebar, and badge contrast for the selected theme.
|
|
</p>
|
|
|
|
<div className="space-y-2">
|
|
<PreviewBanner className="border border-status-connected/30 bg-status-connected/15 text-status-connected">
|
|
Connected preview: radio link healthy and syncing.
|
|
</PreviewBanner>
|
|
<PreviewBanner className="border border-warning/50 bg-warning/10 text-warning">
|
|
Warning preview: packet audit suggests missing history.
|
|
</PreviewBanner>
|
|
<PreviewBanner className="border border-destructive/30 bg-destructive/10 text-destructive">
|
|
Error preview: radio reconnect failed.
|
|
</PreviewBanner>
|
|
</div>
|
|
|
|
<div className="mt-4 space-y-2">
|
|
<PreviewMessage
|
|
sender="Alice"
|
|
bubbleClassName="bg-msg-incoming text-foreground"
|
|
text="Hello, mesh!"
|
|
/>
|
|
<PreviewMessage
|
|
sender="You"
|
|
alignRight
|
|
bubbleClassName="bg-msg-outgoing text-foreground"
|
|
text="Hi there! I'm using RemoteTerm."
|
|
/>
|
|
</div>
|
|
|
|
<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">Sidebar preview</p>
|
|
<div className="space-y-1">
|
|
<PreviewSidebarRow
|
|
active
|
|
leading={
|
|
<span
|
|
className="flex h-6 w-6 items-center justify-center rounded-md bg-primary/10 text-primary"
|
|
aria-hidden="true"
|
|
>
|
|
<Logs className="h-3.5 w-3.5" />
|
|
</span>
|
|
}
|
|
label="Packet Feed"
|
|
/>
|
|
<PreviewSidebarRow
|
|
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-[0.625rem] font-semibold text-badge-unread-foreground">
|
|
3
|
|
</span>
|
|
}
|
|
/>
|
|
<PreviewSidebarRow
|
|
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-[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>
|
|
);
|
|
}
|
|
|
|
function PreviewBanner({ children, className }: { children: React.ReactNode; className: string }) {
|
|
return <div className={`rounded-md px-3 py-2 text-xs ${className}`}>{children}</div>;
|
|
}
|
|
|
|
function PreviewMessage({
|
|
sender,
|
|
text,
|
|
bubbleClassName,
|
|
alignRight = false,
|
|
}: {
|
|
sender: string;
|
|
text: string;
|
|
bubbleClassName: string;
|
|
alignRight?: boolean;
|
|
}) {
|
|
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-[0.6875rem] text-muted-foreground">{sender}</span>
|
|
<div className={`rounded-2xl px-3 py-2 text-sm break-words ${bubbleClassName}`}>{text}</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function PreviewSidebarRow({
|
|
leading,
|
|
label,
|
|
badge,
|
|
active = false,
|
|
}: {
|
|
leading: React.ReactNode;
|
|
label: string;
|
|
badge?: React.ReactNode;
|
|
active?: boolean;
|
|
}) {
|
|
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-[0.8125rem] ${
|
|
active ? 'border-l-primary bg-accent text-foreground' : 'border-l-transparent'
|
|
}`}
|
|
>
|
|
<span className="sidebar-tool-icon" aria-hidden="true">
|
|
{leading}
|
|
</span>
|
|
<span className={`sidebar-tool-label min-w-0 flex-1 truncate ${active ? 'font-medium' : ''}`}>
|
|
{label}
|
|
</span>
|
|
{badge}
|
|
{!badge && (
|
|
<span className="sidebar-tool-icon" aria-hidden="true">
|
|
<MessageSquare className="h-3.5 w-3.5" />
|
|
</span>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|