Refactor UI components for improved theming, better flight trace logic and weather data (RainViewer radar tiles and METAR reports) (#17)

* Refactor UI components for improved theming and accessibility

- Updated color schemes in `fpv-hud.tsx`, `hero-banner.tsx`, `keyboard-shortcuts-help.tsx`, `mobile-flight-toast.tsx`, `provider-panel.tsx`, `scroll-area.tsx`, and `slider.tsx` to utilize foreground and background variables for better dark mode support.
- Enhanced visual consistency by replacing hardcoded colors with theme variables across various components.
- Adjusted text and background colors for improved readability and accessibility.
- Fixed minor issues with key bindings in `keyboard-shortcuts-help.tsx`.
- Optimized flight data handling in `use-trail-history.ts` and `trail-cleanup.ts` for better performance and accuracy.
- Implemented outlier filtering in trail history to reduce GPS glitches.

* feat: enhance aircraft appearance and flight trail rendering with improved safety checks and visual effects

* feat: implement last flight leg trimming and nearest airport search functionality

* feat: Enhance flight data parsing and handling

- Added optionalFinite helper function to ensure only finite numbers are processed in flight data.
- Extended FlightState type to include avionics data (ias, tas, mach, roll, trackRate, magHeading) and navigation intent (navAltitudeMcp, navAltitudeFms, navHeading, navQnh, navModes).
- Updated parseRawAircraft function to utilize optionalFinite for avionics and navigation data.
- Adjusted removeSpikePoints function to increase cosThreshold from -0.17 to -0.05 for better spike removal.
- Increased MAX_WINDOW in removePathLoops function from 120 to 300 to allow for larger path loops.
- Integrated loop cleaning in stitchHistoricalTrail function to ensure cleaner paths and altitudes.

* feat: add AtcSpectrum component for audio visualization and useAirportBoard hook for flight data management
This commit is contained in:
kew
2026-03-30 00:21:36 +05:30
committed by GitHub
parent 0e2ba9fc13
commit 498504b73b
177 changed files with 5676 additions and 1165 deletions

View File

@ -12,11 +12,14 @@ import {
Shield,
Flame,
Eye,
CloudRain,
} from "lucide-react";
import {
useSettings,
AIRSPACE_OPACITY_MIN,
AIRSPACE_OPACITY_MAX,
WEATHER_RADAR_OPACITY_MIN,
WEATHER_RADAR_OPACITY_MAX,
type OrbitDirection,
} from "@/hooks/use-settings";
import { ScrollArea } from "@/components/ui/scroll-area";
@ -139,6 +142,24 @@ export function SettingsContent() {
</>
)}
{/* ── Weather ── */}
<SectionHeader title="Weather" />
<SettingRow
icon={<CloudRain className="h-4 w-4" />}
title="Weather radar"
description="Live precipitation radar overlay (RainViewer)"
checked={settings.showWeatherRadar}
onChange={(v) => update("showWeatherRadar", v)}
/>
{settings.showWeatherRadar && (
<WeatherRadarOpacitySlider
value={settings.weatherRadarOpacity}
onChange={(v) => update("weatherRadarOpacity", v)}
/>
)}
{/* ── Performance ── */}
<SectionHeader title="Performance" />
@ -151,19 +172,19 @@ export function SettingsContent() {
badge="BETA"
/>
<div className="mx-3 my-2 h-px bg-white/4" />
<div className="mx-3 my-2 h-px bg-foreground/5" />
<div className="px-3 pt-2">
<button
type="button"
onClick={reset}
className="inline-flex h-8 items-center justify-center rounded-lg px-3 text-[12px] font-medium text-white/65 ring-1 ring-white/10 transition-colors hover:bg-white/5 hover:text-white/85"
className="inline-flex h-8 items-center justify-center rounded-lg px-3 text-[12px] font-medium text-foreground/65 ring-1 ring-foreground/10 transition-colors hover:bg-foreground/5 hover:text-foreground/85"
>
Reset to defaults
</button>
</div>
<div className="mx-3 my-2 h-px bg-white/4" />
<div className="mx-3 my-2 h-px bg-foreground/5" />
</div>
</ScrollArea>
);
@ -177,12 +198,12 @@ export function ShortcutsContent() {
{SHORTCUTS.map(({ key, description }) => (
<div
key={key}
className="flex items-center justify-between gap-3 rounded-xl px-3 py-2.5 transition-colors hover:bg-white/4"
className="flex items-center justify-between gap-3 rounded-xl px-3 py-2.5 transition-colors hover:bg-foreground/4"
>
<span className="text-[13px] font-medium text-white/68">
<span className="text-[13px] font-medium text-foreground/68">
{description}
</span>
<kbd className="flex h-7 min-w-7 items-center justify-center rounded-md bg-white/6 px-2 font-mono text-[11px] font-semibold text-white/74 ring-1 ring-white/8">
<kbd className="flex h-7 min-w-7 items-center justify-center rounded-md bg-foreground/6 px-2 font-mono text-[11px] font-semibold text-foreground/74 ring-1 ring-foreground/8">
{key}
</kbd>
</div>
@ -218,13 +239,15 @@ function OrbitSpeedSlider({
return (
<div className="flex w-full items-center gap-3.5 rounded-xl px-3 py-2.5 text-left">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-white/5 text-white/35 ring-1 ring-white/6">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-foreground/5 text-foreground/35 ring-1 ring-foreground/6">
<RotateCw className="h-4 w-4" />
</div>
<div className="flex flex-1 min-w-0 flex-col gap-2">
<div className="flex items-center justify-between">
<p className="text-[13px] font-medium text-white/80">Orbit speed</p>
<span className="text-[11px] font-semibold text-white/40 tabular-nums">
<p className="text-[13px] font-medium text-foreground/80">
Orbit speed
</p>
<span className="text-[11px] font-semibold text-foreground/40 tabular-nums">
{activeLabel}
</span>
</div>
@ -249,7 +272,7 @@ function OrbitSpeedSlider({
<span
key={preset.label}
className={`absolute h-1.5 w-1.5 rounded-full -translate-x-1/2 -translate-y-1/2 transition-colors ${
isActive ? "bg-white/50" : "bg-white/15"
isActive ? "bg-foreground/50" : "bg-foreground/15"
}`}
style={{ left: `${pct}%` }}
/>
@ -271,15 +294,15 @@ function TrailThicknessSlider({
}) {
return (
<div className="flex w-full items-center gap-3.5 rounded-xl px-3 py-2.5 text-left">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-white/5 text-white/35 ring-1 ring-white/6">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-foreground/5 text-foreground/35 ring-1 ring-foreground/6">
<Layers className="h-4 w-4" />
</div>
<div className="flex flex-1 min-w-0 flex-col gap-2">
<div className="flex items-center justify-between">
<p className="text-[13px] font-medium text-white/80">
<p className="text-[13px] font-medium text-foreground/80">
Trail thickness
</p>
<span className="text-[11px] font-semibold text-white/40 tabular-nums">
<span className="text-[11px] font-semibold text-foreground/40 tabular-nums">
{value.toFixed(1)} px
</span>
</div>
@ -305,15 +328,15 @@ function TrailDistanceSlider({
}) {
return (
<div className="flex w-full items-center gap-3.5 rounded-xl px-3 py-2.5 text-left">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-white/5 text-white/35 ring-1 ring-white/6">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-foreground/5 text-foreground/35 ring-1 ring-foreground/6">
<Route className="h-4 w-4" />
</div>
<div className="flex flex-1 min-w-0 flex-col gap-2">
<div className="flex items-center justify-between">
<p className="text-[13px] font-medium text-white/80">
<p className="text-[13px] font-medium text-foreground/80">
Trail distance
</p>
<span className="text-[11px] font-semibold text-white/40 tabular-nums">
<span className="text-[11px] font-semibold text-foreground/40 tabular-nums">
{value} pts
</span>
</div>
@ -339,15 +362,15 @@ function AirspaceOpacitySlider({
}) {
return (
<div className="flex w-full items-center gap-3.5 rounded-xl px-3 py-2.5 text-left">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-white/5 text-white/35 ring-1 ring-white/6">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-foreground/5 text-foreground/35 ring-1 ring-foreground/6">
<Eye className="h-4 w-4" />
</div>
<div className="flex flex-1 min-w-0 flex-col gap-2">
<div className="flex items-center justify-between">
<p className="text-[13px] font-medium text-white/80">
<p className="text-[13px] font-medium text-foreground/80">
Airspace opacity
</p>
<span className="text-[11px] font-semibold text-white/40 tabular-nums">
<span className="text-[11px] font-semibold text-foreground/40 tabular-nums">
{Math.round(value * 100)}%
</span>
</div>
@ -364,13 +387,47 @@ function AirspaceOpacitySlider({
);
}
function WeatherRadarOpacitySlider({
value,
onChange,
}: {
value: number;
onChange: (v: number) => void;
}) {
return (
<div className="flex w-full items-center gap-3.5 rounded-xl px-3 py-2.5 text-left">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-foreground/5 text-foreground/35 ring-1 ring-foreground/6">
<CloudRain className="h-4 w-4" />
</div>
<div className="flex flex-1 min-w-0 flex-col gap-2">
<div className="flex items-center justify-between">
<p className="text-[13px] font-medium text-foreground/80">
Radar opacity
</p>
<span className="text-[11px] font-semibold text-foreground/40 tabular-nums">
{Math.round(value * 100)}%
</span>
</div>
<Slider
min={WEATHER_RADAR_OPACITY_MIN}
max={WEATHER_RADAR_OPACITY_MAX}
step={0.05}
value={[value]}
onValueChange={(vals) => onChange(vals[0])}
aria-label="Weather radar opacity"
/>
</div>
</div>
);
}
function SectionHeader({ title }: { title: string }) {
return (
<div className="flex items-center gap-2 px-3 pt-3 pb-1">
<span className="text-[10px] font-bold tracking-widest text-white/25 uppercase">
<span className="text-[10px] font-bold tracking-widest text-foreground/25 uppercase">
{title}
</span>
<div className="h-px flex-1 bg-white/4" />
<div className="h-px flex-1 bg-foreground/4" />
</div>
);
}
@ -395,21 +452,21 @@ function SettingRow({
role="switch"
aria-checked={checked}
onClick={() => onChange(!checked)}
className="flex w-full items-center gap-3.5 rounded-xl px-3 py-3 text-left transition-colors hover:bg-white/4 active:bg-white/6"
className="flex w-full items-center gap-3.5 rounded-xl px-3 py-3 text-left transition-colors hover:bg-foreground/4 active:bg-foreground/6"
>
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-white/5 text-white/35 ring-1 ring-white/6">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-foreground/5 text-foreground/35 ring-1 ring-foreground/6">
{icon}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-1.5">
<p className="text-[13px] font-medium text-white/80">{title}</p>
<p className="text-[13px] font-medium text-foreground/80">{title}</p>
{badge && (
<span className="inline-flex items-center rounded-md bg-indigo-500/15 px-1.5 py-0.5 text-[9px] font-bold tracking-wider text-indigo-300 ring-1 ring-indigo-400/20">
{badge}
</span>
)}
</div>
<p className="mt-0.5 text-[11px] font-medium leading-relaxed text-white/22">
<p className="mt-0.5 text-[11px] font-medium leading-relaxed text-foreground/22">
{description}
</p>
</div>
@ -433,16 +490,16 @@ function SegmentRow<T extends string | number>({
}) {
return (
<div className="flex w-full items-center gap-3.5 rounded-xl px-3 py-2.5 text-left">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-white/5 text-white/35 ring-1 ring-white/6">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-foreground/5 text-foreground/35 ring-1 ring-foreground/6">
{icon}
</div>
<p className="flex-1 min-w-0 text-[13px] font-medium text-white/80">
<p className="flex-1 min-w-0 text-[13px] font-medium text-foreground/80">
{title}
</p>
<div
role="radiogroup"
aria-label={title}
className="flex shrink-0 rounded-md bg-white/4 p-0.5 ring-1 ring-white/6"
className="flex shrink-0 rounded-md bg-foreground/4 p-0.5 ring-1 ring-foreground/6"
>
{options.map((opt) => {
const isActive = opt.value === value;
@ -453,13 +510,15 @@ function SegmentRow<T extends string | number>({
aria-checked={isActive}
onClick={() => onChange(opt.value)}
className={`relative rounded-md px-2 py-1 text-[11px] font-semibold transition-colors ${
isActive ? "text-white/90" : "text-white/30 hover:text-white/50"
isActive
? "text-foreground/90"
: "text-foreground/30 hover:text-foreground/50"
}`}
>
{isActive && (
<motion.div
layoutId={`seg-${title}`}
className="absolute inset-0 rounded-md bg-white/10"
className="absolute inset-0 rounded-md bg-foreground/10"
transition={{
type: "spring",
stiffness: 500,
@ -480,14 +539,14 @@ function Toggle({ checked }: { checked: boolean }) {
return (
<div
className={`relative h-5 w-9 shrink-0 rounded-full transition-colors duration-200 ${
checked ? "bg-white/20" : "bg-white/6"
checked ? "bg-foreground/20" : "bg-foreground/6"
}`}
>
<motion.div
animate={{ x: checked ? 17 : 2 }}
transition={{ type: "spring", stiffness: 500, damping: 30 }}
className={`absolute top-0.75 h-3.5 w-3.5 rounded-full shadow-sm transition-colors duration-200 ${
checked ? "bg-white" : "bg-white/25"
checked ? "bg-foreground" : "bg-foreground/25"
}`}
/>
</div>
@ -573,11 +632,11 @@ export function AboutContent() {
return (
<ScrollArea className="h-full">
<div className="flex flex-col gap-5 p-5 pt-3">
<h3 className="text-[20px] font-bold tracking-tight text-white/90">
<h3 className="text-[20px] font-bold tracking-tight text-foreground/90">
Aeris
</h3>
<div className="space-y-3 text-[13px] leading-relaxed text-white/40">
<div className="space-y-3 text-[13px] leading-relaxed text-foreground/40">
<p>
Live flight tracking in 3D. The planes you see are real position
data comes from ADS-B Exchange, adsb.lol, and OpenSky Network,
@ -592,40 +651,49 @@ export function AboutContent() {
</p>
</div>
<div className="h-px w-full bg-white/6" />
<div className="h-px w-full bg-foreground/6" />
<p className="text-[12px] leading-relaxed text-white/30">
Built by{" "}
<p className="text-[12px] leading-relaxed text-foreground/30">
Built by a human, not just LLMs.{" "}
<a
href="https://github.com/kewonit"
target="_blank"
rel="noopener noreferrer"
className="text-white/55 underline decoration-white/15 underline-offset-2 hover:text-white/70 transition-colors"
className="text-foreground/55 underline decoration-foreground/15 underline-offset-2 hover:text-foreground/70 transition-colors"
>
kewonit
</a>
{" · "}
<a
href="https://x.com/kewonit"
target="_blank"
rel="noopener noreferrer"
className="text-foreground/55 underline decoration-foreground/15 underline-offset-2 hover:text-foreground/70 transition-colors"
>
@kewonit
</a>
. Open to internships {" "}
<a
href="mailto:kew@edbn.me"
className="text-white/55 underline decoration-white/15 underline-offset-2 hover:text-white/70 transition-colors"
className="text-foreground/55 underline decoration-foreground/15 underline-offset-2 hover:text-foreground/70 transition-colors"
>
kew@edbn.me
</a>
</p>
<p className="text-[12px] leading-relaxed text-white/30">
<p className="text-[12px] leading-relaxed text-foreground/30">
Source is on{" "}
<a
href="https://github.com/kewonit/aeris"
target="_blank"
rel="noopener noreferrer"
className="text-white/55 underline decoration-white/15 underline-offset-2 hover:text-white/70 transition-colors"
className="text-foreground/55 underline decoration-foreground/15 underline-offset-2 hover:text-foreground/70 transition-colors"
>
GitHub
</a>
. Got a question or just wanna say hi?{" "}
<a
href="mailto:aeris@edbn.me"
className="text-white/55 underline decoration-white/15 underline-offset-2 hover:text-white/70 transition-colors"
className="text-foreground/55 underline decoration-foreground/15 underline-offset-2 hover:text-foreground/70 transition-colors"
>
aeris@edbn.me
</a>
@ -639,16 +707,16 @@ export function ChangelogContent() {
return (
<ScrollArea className="h-full">
<div className="flex flex-col gap-4 p-5 pt-3">
{CHANGELOG.map((entry) => (
<div key={entry.date} className="flex gap-3">
<span className="shrink-0 pt-0.5 text-[11px] tabular-nums text-white/20 w-11">
{CHANGELOG.map((entry, i) => (
<div key={`${entry.date}-${i}`} className="flex gap-3">
<span className="shrink-0 pt-0.5 text-[11px] tabular-nums text-foreground/20 w-11">
{entry.date}
</span>
<div className="min-w-0">
<p className="text-[13px] font-medium text-white/55">
<p className="text-[13px] font-medium text-foreground/55">
{entry.title}
</p>
<p className="mt-0.5 text-[12px] leading-relaxed text-white/30">
<p className="mt-0.5 text-[12px] leading-relaxed text-foreground/30">
{entry.description}
</p>
</div>