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:
@ -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>
|
||||
|
||||
Reference in New Issue
Block a user