From 04e420b6b7939c003b40c867f77efc25fff85228 Mon Sep 17 00:00:00 2001 From: Ben Betz Date: Sat, 11 Jul 2026 14:07:09 -0700 Subject: [PATCH] Add a Not for Navigation disclaimer --- .../layout/NotForNavigation.module.css | 195 ++++++++++++++++ src/components/layout/NotForNavigation.tsx | 211 ++++++++++++++++++ src/components/layout/TopBar.tsx | 2 + 3 files changed, 408 insertions(+) create mode 100644 src/components/layout/NotForNavigation.module.css create mode 100644 src/components/layout/NotForNavigation.tsx diff --git a/src/components/layout/NotForNavigation.module.css b/src/components/layout/NotForNavigation.module.css new file mode 100644 index 0000000..d060766 --- /dev/null +++ b/src/components/layout/NotForNavigation.module.css @@ -0,0 +1,195 @@ +/* Trigger: anchored to the bottom of the topbar, horizontally centered. */ +.trigger { + position: absolute; + bottom: 1px; + left: 50%; + transform: translateX(-50%); + padding: 0 6px; + background: none; + border: none; + cursor: pointer; + font-size: 9px; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--text-muted); + white-space: nowrap; + line-height: 1.2; + transition: color 0.12s ease; +} + +.trigger:hover { + color: var(--accent); + text-decoration: underline; +} + +/* Full-viewport backdrop (portaled to ). */ +.backdrop { + position: fixed; + inset: 0; + z-index: 10000; + display: flex; + align-items: center; + justify-content: center; + padding: 24px; + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(2px); +} + +.dialog { + position: relative; + display: flex; + flex-direction: column; + width: 100%; + max-width: 560px; + max-height: 85vh; + background: var(--overlay-bg); + border: 1px solid var(--overlay-border); + border-radius: 10px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); + backdrop-filter: blur(8px); + color: var(--text); +} + +.close { + position: absolute; + top: 8px; + right: 10px; + z-index: 1; + width: 28px; + height: 28px; + padding: 0; + background: none; + border: none; + border-radius: 6px; + cursor: pointer; + font-size: 20px; + line-height: 1; + color: var(--text-muted); +} + +.close:hover { + background: var(--hover); + color: var(--text); +} + +/* Scroll region — vertical only, never horizontal. */ +.scroll { + overflow-y: auto; + overflow-x: hidden; + padding: 20px 24px 24px; +} + +.title { + margin: 0 24px 12px 0; + font-size: 18px; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--accent); +} + +.section { + margin-top: 18px; +} + +.section:first-of-type { + margin-top: 0; +} + +.section p { + margin: 0 0 10px; + font-size: 13px; + line-height: 1.5; + color: var(--text); +} + +.section p:last-child { + margin-bottom: 0; +} + +.heading { + margin: 0 0 10px; + padding-bottom: 6px; + border-bottom: 1px solid var(--overlay-border); + font-size: 14px; + font-weight: 700; + color: var(--text); +} + +.diffGroup { + margin-bottom: 14px; +} + +.diffGroup:last-child { + margin-bottom: 0; +} + +.subheading { + margin: 0 0 4px; + font-size: 12px; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--accent); +} + +.list { + margin: 0; + padding-left: 18px; +} + +.list li { + margin-bottom: 5px; + font-size: 12.5px; + line-height: 1.45; + color: var(--text); +} + +.list li:last-child { + margin-bottom: 0; +} + +.sourceList { + margin: 0; + padding-left: 18px; +} + +.sourceList li { + margin-bottom: 7px; + font-size: 13px; + line-height: 1.4; +} + +.sourceList a { + color: var(--accent); + font-weight: 600; + text-decoration: none; +} + +.sourceList a:hover { + text-decoration: underline; +} + +.sourceDetail { + color: var(--text-muted); +} + +/* Mobile: fill the screen for readability. */ +@media (max-width: 640px) { + .backdrop { + padding: 0; + } + + .dialog { + max-width: none; + max-height: 100%; + height: 100%; + border: none; + border-radius: 0; + } + + .scroll { + padding: 18px 18px 28px; + } +} diff --git a/src/components/layout/NotForNavigation.tsx b/src/components/layout/NotForNavigation.tsx new file mode 100644 index 0000000..798d662 --- /dev/null +++ b/src/components/layout/NotForNavigation.tsx @@ -0,0 +1,211 @@ +import { useEffect, useState } from 'react' +import { createPortal } from 'react-dom' +import styles from './NotForNavigation.module.css' + +interface DataSource { + name: string + href: string + detail: string +} + +// Public upstream data sources (see CLAUDE.md "Data sources"). Links open the +// provider's homepage in a new tab. +const DATA_SOURCES: DataSource[] = [ + { + name: 'ADS-B Exchange', + href: 'https://www.adsbexchange.com/', + detail: 'Live aircraft positions', + }, + { + name: 'adsb.lol', + href: 'https://adsb.lol/', + detail: 'Callsign → route lookups', + }, + { + name: 'adsbdb', + href: 'https://www.adsbdb.com/', + detail: 'Callsign → route fallback', + }, + { + name: 'FAA CIFP / Aeronautical Data', + href: 'https://www.faa.gov/air_traffic/flight_info/aeronav/', + detail: 'Procedures, waypoints, MVA/MIA sectors (ARINC 424, 28-day AIRAC cycle)', + }, + { + name: 'FAA Aeronautical Information Services', + href: 'https://www.faa.gov/air_traffic/flight_info/aeronav/aeronautical_data/', + detail: 'Class B/C/D/E airspace boundaries', + }, + { + name: 'atis.info', + href: 'https://atis.info/', + detail: 'Digital ATIS (runways & approaches in use)', + }, + { + name: 'OurAirports', + href: 'https://ourairports.com/', + detail: 'Airport metadata & runway geometry', + }, + { + name: 'Mapbox', + href: 'https://www.mapbox.com/', + detail: 'Base map & terrain rendering', + }, +] + +// Known ways our depiction can differ from the official published sources, +// roughly in priority order (most likely to mislead first). +const KNOWN_DIFFERENCES: { heading: string; items: string[] }[] = [ + { + heading: 'Traffic', + items: [ + 'Only aircraft broadcasting ADS-B appear — non-equipped, blocked, or non-transmitting traffic (including some military) is invisible.', + 'Positions are polled every few seconds and dead-reckoned between polls, so a target on screen is an estimate that lags reality by seconds.', + 'No separation, TCAS, conflict, or wake information of any kind is shown or implied.', + 'Callsign-to-route (origin → destination) is best-effort crowd-sourced data and is frequently missing or wrong.', + ], + }, + { + heading: 'Procedures', + items: [ + 'Routes are drawn from the FAA CIFP coded (ARINC 424) data, not the published chart — geometry, labels, and fixes may differ from the plate.', + 'Maximum holding altitudes are not available in the CIFP, so holds show only the coded crossing constraint (e.g. "at or above"), not the charted maximum.', + 'Glidepath / descent angles are taken from path-point, ILS, or coded VDA data and are inferred where those are absent.', + 'Course reversals, procedure turns, and NoPT applicability are inferred from the coded legs and may not match the charted depiction.', + 'DME arcs are drawn as sampled constant-radius arcs — an approximation of the true ground track.', + 'Only Locator Outer Markers (an FAF collocated with an NDB) are inferred; other marker beacons are not shown because the CIFP contains no marker records.', + 'Missed-approach and lost-comms segments are only partially depicted.', + 'Coverage is limited to US airports with published instrument approaches.', + ], + }, + { + heading: 'Airspace', + items: [ + 'Class B/C/D/E boundaries come from an FAA GIS dataset that can lag current charts and NOTAMs.', + 'Special-use airspace (MOAs, restricted, prohibited, warning areas), TFRs, and NOTAM-driven changes are not shown.', + 'Vertical (floor/ceiling) limits may be simplified or omitted; boundaries are primarily lateral.', + ], + }, + { + heading: 'Terrain & obstacles', + items: [ + 'Terrain is a Mapbox visual rendering for context only — not survey-grade elevation.', + 'MVA/MIA sectors are shown only where FAA AIXM data is published for that facility.', + 'No obstacle data (towers, buildings) and no MSA/emergency-safe-altitude rings are depicted.', + ], + }, +] + +export function NotForNavigation() { + const [open, setOpen] = useState(false) + + useEffect(() => { + if (!open) return + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') setOpen(false) + } + window.addEventListener('keydown', onKey) + // Lock background scroll while the dialog is open. + const prevOverflow = document.body.style.overflow + document.body.style.overflow = 'hidden' + return () => { + window.removeEventListener('keydown', onKey) + document.body.style.overflow = prevOverflow + } + }, [open]) + + return ( + <> + + + {open && + createPortal( +
setOpen(false)} + role="presentation" + > +
e.stopPropagation()} + > + + +
+

+ Not for Navigation +

+ +
+

+ Approach Map is for informational use only. It + exists to help aviation enthusiasts visualize live air traffic + relative to published instrument procedures at their favorite + airports. +

+

+ Data is aggregated from several public sources (listed below) and + may deviate from the official charts and publications. It can be + incomplete, delayed, or inaccurate. +

+

+ + Always refer to official FAA publications for flight planning + and navigation. + {' '} + Never use this app operationally. +

+
+ +
+

Known differences from official sources

+ {KNOWN_DIFFERENCES.map((group) => ( +
+

{group.heading}

+
    + {group.items.map((item, i) => ( +
  • {item}
  • + ))} +
+
+ ))} +
+ +
+

Data sources

+
    + {DATA_SOURCES.map((src) => ( +
  • + + {src.name} + + — {src.detail} +
  • + ))} +
+
+
+
+
, + document.body, + )} + + ) +} diff --git a/src/components/layout/TopBar.tsx b/src/components/layout/TopBar.tsx index 6ec1cfb..845e902 100644 --- a/src/components/layout/TopBar.tsx +++ b/src/components/layout/TopBar.tsx @@ -6,6 +6,7 @@ import { AirspaceToggle } from '../controls/AirspaceToggle' import { TerrainToggle } from '../controls/TerrainToggle' import { ThemeToggle } from '../controls/ThemeToggle' import { CifpStatusBanner } from './CifpStatusBanner' +import { NotForNavigation } from './NotForNavigation' import styles from './TopBar.module.css' export function TopBar() { @@ -24,6 +25,7 @@ export function TopBar() { + ) }