G1000: manual-accurate radios, baro units, declutter, minimums, OBS, audio panel
Aligned to the official X-Plane 1000 manual: - NAV radio: active RIGHT / standby LEFT (boxed) per S.12 (COM already correct) - ALT UNIT softkey (IN / HPA) in the PFD submenu, baro readout converts (S.20) - DCLTR cycles 3 levels (land / +NDB / flight-plan only) with DCLTR-n label (S.56) - TOPO and TERRAIN are now independent toggles (relief vs awareness overlay) (S.57) - Barometric MINIMUMS: BARO MIN bug + readout on the altimeter, amber "MINIMUMS" annunciation at/below the decision altitude; set via TMR/REF (lifted to App) - OBS mode: HSI course follows the CRS knob (magenta "OBS"), sequencing suspended - New Audio Panel tab (COM mic/receive, MKR/DME/ADF, intercom, Display Backup) (S.91) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+17
-5
@@ -10,6 +10,7 @@ import Bezel from './components/Bezel.jsx';
|
||||
import DirectTo from './components/DirectTo.jsx';
|
||||
import Proc from './components/Proc.jsx';
|
||||
import FplPage from './components/FplPage.jsx';
|
||||
import AudioPanel from './components/AudioPanel.jsx';
|
||||
|
||||
// Compact line icons for the nav rail (stroke = currentColor).
|
||||
const ICONS = {
|
||||
@@ -19,6 +20,7 @@ const ICONS = {
|
||||
fms: 'M4 6h14M4 11h14M4 16h9',
|
||||
ap: 'M11 4a7 7 0 100 14 7 7 0 000-14zM11 4v3M11 15v3M4 11h3M15 11h3',
|
||||
vfr: 'M11 4a7 7 0 100 14 7 7 0 000-14zM11 11l4.5-3',
|
||||
audio: 'M11 4a6 6 0 00-6 6v5M17 15v-5a6 6 0 00-6-6M4 14h2.5v4.5H4zM15.5 14H18v4.5h-2.5z',
|
||||
};
|
||||
function Icon({ name }) {
|
||||
return (
|
||||
@@ -37,6 +39,7 @@ const TABS = [
|
||||
{ id: 'fms', label: 'FMS' },
|
||||
{ id: 'vfr', label: 'VFR' },
|
||||
{ id: 'ap', label: 'Autopilot' },
|
||||
{ id: 'audio', label: 'Audio' },
|
||||
];
|
||||
|
||||
export default function App() {
|
||||
@@ -65,8 +68,14 @@ export default function App() {
|
||||
const toggleWin = (id) => setWin((w) => (w === id ? null : id));
|
||||
const nrst = win === 'nrst', tmr = win === 'tmr', dme = win === 'dme', alerts = win === 'alerts';
|
||||
const fpl = win === 'fpl', dto = win === 'dto', proc = win === 'proc';
|
||||
// MFD map mode (base layer), switched via the Map-Opt softkeys.
|
||||
// MFD map mode (base layer + overlays), switched via the Map-Opt softkeys.
|
||||
const [mapMode, setMapMode] = useState({ base: 'topo' });
|
||||
// Altimeter barometric units (false = inHg, true = hectopascal) — PFD ALT UNIT softkey.
|
||||
const [baroHpa, setBaroHpa] = useState(false);
|
||||
// Barometric minimums (set in TMR/REF) — shown on the PFD altimeter as BARO MIN.
|
||||
const [minimums, setMinimums] = useState({ on: false, ft: 500 });
|
||||
// OBS (omni-bearing select) mode — suspends GPS sequencing, course set by CRS knob.
|
||||
const [obs, setObs] = useState(false);
|
||||
// MFD page group (MAP / FPL / NRST) — selected by the FMS knob, like the real G1000.
|
||||
const MFD_PAGES = ['map', 'fpl', 'nrst'];
|
||||
const [mfdPage, setMfdPage] = useState('map');
|
||||
@@ -129,15 +138,17 @@ export default function App() {
|
||||
inset={inset} onSetInset={setInset} insetMode={insetMode} onInsetMode={setInsetMode}
|
||||
nrst={nrst} onToggleNrst={() => toggleWin('nrst')} onDirect={() => toggleWin('dto')}
|
||||
tmr={tmr} onToggleTmr={() => toggleWin('tmr')} dme={dme} onToggleDme={() => toggleWin('dme')}
|
||||
alerts={alerts} onToggleAlerts={() => toggleWin('alerts')} onProc={() => toggleWin('proc')} onFpl={() => toggleWin('fpl')}>
|
||||
<PFD values={xp.values} command={xp.command} svt={svt3d} inset={inset} insetMode={insetMode} nrst={nrst} onCloseNrst={() => setWin(null)}
|
||||
alerts={alerts} onToggleAlerts={() => toggleWin('alerts')} onProc={() => toggleWin('proc')} onFpl={() => toggleWin('fpl')} onClr={() => setWin(null)}
|
||||
altHpa={baroHpa} onAltUnit={setBaroHpa} obs={obs} onObs={() => setObs((v) => !v)}>
|
||||
<PFD values={xp.values} command={xp.command} connected={xp.xpConnected} svt={svt3d} inset={inset} insetMode={insetMode} nrst={nrst} onCloseNrst={() => setWin(null)}
|
||||
tmr={tmr} onCloseTmr={() => setWin(null)} dme={dme} onCloseDme={() => setWin(null)}
|
||||
alerts={alerts} onCloseAlerts={() => setWin(null)} flightPlan={xp.flightPlan} fp={xp.fp} />
|
||||
alerts={alerts} onCloseAlerts={() => setWin(null)} baroHpa={baroHpa} obs={obs}
|
||||
minimums={minimums} onMinimums={setMinimums} flightPlan={xp.flightPlan} fp={xp.fp} />
|
||||
{dialogs}
|
||||
</Bezel>
|
||||
)}
|
||||
{tab === 'mfd' && (
|
||||
<Bezel variant="mfd" xp={xp} knobMode={knobMode} mapMode={mapMode} onMapMode={setMapMode} onDirect={() => toggleWin('dto')} onProc={() => toggleWin('proc')} onFms={cycleMfd} onFpl={() => setMfdPage('fpl')}>
|
||||
<Bezel variant="mfd" xp={xp} knobMode={knobMode} mapMode={mapMode} onMapMode={setMapMode} onDirect={() => toggleWin('dto')} onProc={() => toggleWin('proc')} onFms={cycleMfd} onFpl={() => setMfdPage('fpl')} onClr={() => setWin(null)}>
|
||||
<MFD values={xp.values} flightPlan={xp.flightPlan} fp={xp.fp} mapMode={mapMode} page={mfdPage} onCycle={cycleMfd} xp={xp} />
|
||||
{dialogs}
|
||||
</Bezel>
|
||||
@@ -146,6 +157,7 @@ export default function App() {
|
||||
{tab === 'fms' && <CDU xp={xp} />}
|
||||
{tab === 'vfr' && <VFR xp={xp} />}
|
||||
{tab === 'ap' && <AutopilotPanel xp={xp} />}
|
||||
{tab === 'audio' && <AudioPanel xp={xp} />}
|
||||
</main>
|
||||
{settings && (
|
||||
<div className="dlg-backdrop" onClick={() => setSettings(false)}>
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
// X1000 Audio Panel (Manual S.91). Selects which radios are heard, which COM is
|
||||
// used to transmit (MIC), marker/DME/ADF Morse audio, intercom, and the Display
|
||||
// Backup (reversionary) key. Selections are local state with authentic lit keys.
|
||||
//
|
||||
// COM MIC is single-select (one transmit radio); the receive/audio keys and the
|
||||
// Morse keys toggle independently — exactly like the real unit.
|
||||
export default function AudioPanel({ xp }) {
|
||||
const [mic, setMic] = useState('com1'); // transmit radio: com1 | com2 | tel
|
||||
const [recv, setRecv] = useState({ com1: true }); // receive/audio selections
|
||||
const [hiSens, setHiSens] = useState(false);
|
||||
const [crew, setCrew] = useState('pilot');
|
||||
const [vol, setVol] = useState(60);
|
||||
|
||||
const r = (k) => !!recv[k];
|
||||
const toggle = (k) => setRecv((s) => ({ ...s, [k]: !s[k] }));
|
||||
// a single audio key: lit green for MIC (transmit), cyan for receive/Morse
|
||||
const Key = ({ k, label, sub, on, kind = 'recv', onClick }) => (
|
||||
<button className={`apk ${kind} ${on ? 'on' : ''}`} onClick={onClick}>
|
||||
<span className="apk-l">{label}</span>{sub && <span className="apk-s">{sub}</span>}
|
||||
</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="audio-panel">
|
||||
<div className="apnl">
|
||||
<div className="apnl-title">AUDIO PANEL</div>
|
||||
|
||||
<div className="apnl-grp">
|
||||
<div className="apnl-h">COM</div>
|
||||
<div className="apnl-row">
|
||||
<Key label="COM1 MIC" kind="mic" on={mic === 'com1'} onClick={() => setMic('com1')} />
|
||||
<Key label="COM1" on={r('com1')} onClick={() => toggle('com1')} />
|
||||
</div>
|
||||
<div className="apnl-row">
|
||||
<Key label="COM2 MIC" kind="mic" on={mic === 'com2'} onClick={() => setMic('com2')} />
|
||||
<Key label="COM2" on={r('com2')} onClick={() => toggle('com2')} />
|
||||
</div>
|
||||
<div className="apnl-row">
|
||||
<Key label="COM 1/2" on={false} onClick={() => setMic((m) => (m === 'com1' ? 'com2' : 'com1'))} />
|
||||
<Key label="TEL" kind="mic" on={mic === 'tel'} onClick={() => setMic('tel')} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="apnl-grp">
|
||||
<div className="apnl-h">CABIN / SPEAKER</div>
|
||||
<div className="apnl-row">
|
||||
<Key label="PA" on={r('pa')} onClick={() => toggle('pa')} />
|
||||
<Key label="SPKR" on={r('spkr')} onClick={() => toggle('spkr')} />
|
||||
</div>
|
||||
<div className="apnl-row">
|
||||
<Key label="MKR / MUTE" on={r('mkr')} onClick={() => toggle('mkr')} />
|
||||
<Key label="HI SENS" on={hiSens} onClick={() => setHiSens((v) => !v)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="apnl-grp">
|
||||
<div className="apnl-h">NAV</div>
|
||||
<div className="apnl-row">
|
||||
<Key label="DME" on={r('dme')} onClick={() => toggle('dme')} />
|
||||
<Key label="NAV1" on={r('nav1')} onClick={() => toggle('nav1')} />
|
||||
</div>
|
||||
<div className="apnl-row">
|
||||
<Key label="ADF" on={r('adf')} onClick={() => toggle('adf')} />
|
||||
<Key label="NAV2" on={r('nav2')} onClick={() => toggle('nav2')} />
|
||||
</div>
|
||||
<div className="apnl-row">
|
||||
<Key label="AUX" on={r('aux')} onClick={() => toggle('aux')} />
|
||||
<Key label="MAN SQ" on={r('msq')} onClick={() => toggle('msq')} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="apnl-grp">
|
||||
<div className="apnl-h">CREW · ICS</div>
|
||||
<div className="apnl-row">
|
||||
<Key label="PILOT" kind="mic" on={crew === 'pilot'} onClick={() => setCrew('pilot')} />
|
||||
<Key label="COPLT" kind="mic" on={crew === 'copilot'} onClick={() => setCrew('copilot')} />
|
||||
</div>
|
||||
<div className="apnl-vol">
|
||||
<span>PILOT INTERCOM VOL</span>
|
||||
<input type="range" min="0" max="100" value={vol} onChange={(e) => setVol(+e.target.value)} />
|
||||
<b>{vol}</b>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button className="apnl-backup" onClick={() => xp && xp.command && xp.command('mfd_softkey1')} title="Display Backup (reversionary)">
|
||||
DISPLAY BACKUP
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -14,7 +14,9 @@ import { num, systemAlerts } from '../api/useXplane.js';
|
||||
// SYN TERR toggles the 3D synthetic-vision terrain on/off.
|
||||
const PFD_MENU = {
|
||||
root: ['', 'INSET', '', 'PFD', '', 'CDI', 'DME', 'XPDR', 'IDENT', 'TMR/REF', 'NRST', 'CAUTION'],
|
||||
pfd: ['PATHWAY', 'SYN TERR', 'HRZN HDG', 'APTSIGNS', '', '', '', '', '', '', '', 'BACK'],
|
||||
pfd: ['PATHWAY', 'SYN TERR', 'HRZN HDG', 'APTSIGNS', 'ALT UNIT', '', '', '', '', '', '', 'BACK'],
|
||||
// ALT UNIT submenu: barometric pressure units (inHg / hectopascal), like the manual.
|
||||
altunit: ['IN', 'HPA', '', '', '', '', '', '', '', '', '', 'BACK'],
|
||||
// XPDR submenu: standby/on/alt modes, VFR (1200), CODE entry, IDENT.
|
||||
xpdr: ['STBY', 'ON', 'ALT', 'VFR', '', 'CODE', 'IDENT', '', '', '', '', 'BACK'],
|
||||
// CODE entry turns the softkeys into the octal squawk keypad (digits 0–7).
|
||||
@@ -34,12 +36,11 @@ const MFD_MENU = {
|
||||
// autopilot_state bitfield (best-effort; tweak per aircraft)
|
||||
const AP_BITS = { fd: 1 << 0, hdg: 1 << 1, vs: 1 << 4, flc: 1 << 6, nav: 1 << 8, apr: 1 << 9, vnav: 1 << 11, altHold: 1 << 14, bc: 1 << 18 };
|
||||
|
||||
export default function Bezel({ variant = 'pfd', xp, svt3d, onToggleSvt, inset, onSetInset, insetMode, onInsetMode, nrst, onToggleNrst, tmr, onToggleTmr, dme, onToggleDme, alerts, onToggleAlerts, onDirect, onProc, onFpl, onFms, mapMode, onMapMode, knobMode = 'arrows', children }) {
|
||||
export default function Bezel({ variant = 'pfd', xp, svt3d, onToggleSvt, inset, onSetInset, insetMode, onInsetMode, nrst, onToggleNrst, tmr, onToggleTmr, dme, onToggleDme, alerts, onToggleAlerts, onDirect, onProc, onFpl, onClr, onFms, mapMode, onMapMode, altHpa, onAltUnit, obs, onObs, knobMode = 'arrows', children }) {
|
||||
const u = variant === 'mfd' ? 'mfd' : 'pfd'; // command prefix
|
||||
const fire = (suffix) => xp && xp.command(`${u}_${suffix}`);
|
||||
const [page, setPage] = useState('root'); // softkey menu page
|
||||
const [squawk, setSquawk] = useState(''); // XPDR code being typed
|
||||
const [obs, setObs] = useState(false); // OBS (suspend) mode
|
||||
|
||||
const menu = variant === 'mfd' ? MFD_MENU : PFD_MENU;
|
||||
let keys = menu[page] || menu.root;
|
||||
@@ -62,31 +63,36 @@ export default function Bezel({ variant = 'pfd', xp, svt3d, onToggleSvt, inset,
|
||||
|
||||
const onSoftkey = (i, label) => {
|
||||
fire(`softkey${i + 1}`); // mirror to the in-sim G1000
|
||||
// declutter cycles through 4 levels (0=all … 3=flight plan only), like the manual
|
||||
const cycleDcltr = (setter) => setter && setter((m) => ({ ...m, dcltr: (((m.dcltr || 0) + 1) % 4) }));
|
||||
if (variant === 'mfd') {
|
||||
if (label === 'MAP') setPage('mapopt');
|
||||
else if (label === 'ENGINE') setPage('engine');
|
||||
else if (label === 'BACK') setPage('root');
|
||||
else if (label === 'TOPO') setBase('topo');
|
||||
else if (label === 'TERRAIN') setBase('terrain');
|
||||
else if (label === 'TOPO') setBase('topo'); // relief on/off
|
||||
else if (label === 'TERRAIN') onMapMode && onMapMode((m) => ({ ...m, terrain: !m.terrain })); // awareness overlay (independent)
|
||||
else if (label === 'OSM') setBase('osm');
|
||||
else if (label === 'DCLTR') onMapMode && onMapMode((m) => ({ ...m, dcltr: m.dcltr ? 0 : 1 }));
|
||||
else if (label === 'DCLTR') cycleDcltr(onMapMode);
|
||||
else if (label === 'AIRWAYS') onMapMode && onMapMode((m) => ({ ...m, airways: !m.airways }));
|
||||
} else {
|
||||
if (label === 'PFD') setPage('pfd');
|
||||
else if (label === 'BACK') setPage(page === 'xpdrcode' ? 'xpdr' : 'root');
|
||||
else if (label === 'BACK') setPage({ xpdrcode: 'xpdr', altunit: 'pfd' }[page] || 'root');
|
||||
else if (label === 'SYN TERR') onToggleSvt && onToggleSvt();
|
||||
else if (label === 'ALT UNIT') setPage('altunit');
|
||||
else if (label === 'IN') { onAltUnit && onAltUnit(false); setPage('pfd'); }
|
||||
else if (label === 'HPA') { onAltUnit && onAltUnit(true); setPage('pfd'); }
|
||||
else if (label === 'INSET') {
|
||||
if (page === 'root') { onSetInset && onSetInset(true); setPage('inset'); }
|
||||
else onSetInset && onSetInset(!inset); // toggle from within the submenu
|
||||
}
|
||||
else if (label === 'OFF') { onSetInset && onSetInset(false); setPage('root'); }
|
||||
else if (label === 'DCLTR') onInsetMode && onInsetMode((m) => ({ ...m, dcltr: m.dcltr ? 0 : 1 }));
|
||||
else if (label === 'TOPO') onInsetMode && onInsetMode((m) => ({ ...m, base: 'topo' }));
|
||||
else if (label === 'TERRAIN') onInsetMode && onInsetMode((m) => ({ ...m, base: 'terrain' }));
|
||||
else if (label === 'DCLTR') cycleDcltr(onInsetMode);
|
||||
else if (label === 'TOPO') onInsetMode && onInsetMode((m) => ({ ...m, base: m.base === 'topo' ? 'dark' : 'topo' }));
|
||||
else if (label === 'TERRAIN') onInsetMode && onInsetMode((m) => ({ ...m, terrain: !m.terrain }));
|
||||
else if (label === 'NRST') onToggleNrst && onToggleNrst();
|
||||
else if (label === 'TMR/REF') onToggleTmr && onToggleTmr();
|
||||
else if (label === 'DME') onToggleDme && onToggleDme();
|
||||
else if (label === 'OBS') setObs((v) => !v); // suspend / OBS mode (also fires the sim softkey above)
|
||||
else if (label === 'OBS') onObs && onObs(); // suspend / OBS mode (also fires the sim softkey above)
|
||||
else if (label === 'CAUTION') onToggleAlerts && onToggleAlerts();
|
||||
else if (label === 'XPDR') setPage('xpdr');
|
||||
else if (label === 'STBY') setMode(1);
|
||||
@@ -102,13 +108,14 @@ export default function Bezel({ variant = 'pfd', xp, svt3d, onToggleSvt, inset,
|
||||
// which softkey is "lit" right now
|
||||
const isOn = (label) => {
|
||||
if (variant === 'mfd') return (label === 'TOPO' && mapMode?.base === 'topo')
|
||||
|| (label === 'TERRAIN' && mapMode?.base === 'terrain') || (label === 'OSM' && mapMode?.base === 'osm')
|
||||
|| (label === 'TERRAIN' && mapMode?.terrain) || (label === 'OSM' && mapMode?.base === 'osm')
|
||||
|| (label === 'DCLTR' && mapMode?.dcltr > 0) || (label === 'AIRWAYS' && mapMode?.airways);
|
||||
return (label === 'SYN TERR' && svt3d) || (label === 'INSET' && inset) || (label === 'NRST' && nrst) || (label === 'TMR/REF' && tmr)
|
||||
|| (label === 'DME' && dme) || (label === 'OBS' && obs) || (label === 'CAUTION' && (alerts || hasAlerts))
|
||||
|| (label === 'STBY' && xpdrMode === 1) || (label === 'ON' && xpdrMode === 2) || (label === 'ALT' && xpdrMode === 3)
|
||||
|| (label === 'IN' && !altHpa) || (label === 'HPA' && altHpa)
|
||||
|| (page === 'inset' && label === 'TOPO' && insetMode?.base === 'topo')
|
||||
|| (page === 'inset' && label === 'TERRAIN' && insetMode?.base === 'terrain')
|
||||
|| (page === 'inset' && label === 'TERRAIN' && insetMode?.terrain)
|
||||
|| (label === 'DCLTR' && insetMode?.dcltr > 0);
|
||||
};
|
||||
|
||||
@@ -135,7 +142,9 @@ export default function Bezel({ variant = 'pfd', xp, svt3d, onToggleSvt, inset,
|
||||
{/* softkey LABELS on the display (lowest line), like the real G1000 */}
|
||||
<div className="sk-labels">
|
||||
{keys.map((s, i) => (
|
||||
<span key={i} className={`skl ${s ? '' : 'empty'} ${s === 'CAUTION' ? 'caution' : ''} ${isOn(s) ? 'on' : ''}`}>{s}</span>
|
||||
<span key={i} className={`skl ${s ? '' : 'empty'} ${s === 'CAUTION' ? 'caution' : ''} ${isOn(s) ? 'on' : ''}`}>{
|
||||
(s === 'DCLTR' && (mapMode?.dcltr || insetMode?.dcltr)) ? `DCLTR-${mapMode?.dcltr || insetMode?.dcltr}` : s
|
||||
}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -159,7 +168,7 @@ export default function Bezel({ variant = 'pfd', xp, svt3d, onToggleSvt, inset,
|
||||
<div className="bezel-grid">
|
||||
<BtnG fire={fire} mode={knobMode} cmd="direct" onClick={onDirect}>D→</BtnG><BtnG fire={fire} mode={knobMode} cmd="menu">MENU</BtnG>
|
||||
<BtnG fire={fire} mode={knobMode} cmd="fpl" onClick={onFpl}>FPL</BtnG><BtnG fire={fire} mode={knobMode} cmd="proc" onClick={onProc}>PROC</BtnG>
|
||||
<BtnG fire={fire} mode={knobMode} cmd="clr">CLR</BtnG><BtnG fire={fire} mode={knobMode} cmd="ent">ENT</BtnG>
|
||||
<BtnG fire={fire} mode={knobMode} cmd="clr" onClick={onClr}>CLR</BtnG><BtnG fire={fire} mode={knobMode} cmd="ent">ENT</BtnG>
|
||||
</div>
|
||||
<Knob label="FMS" sub="PUSH CRSR" big fire={fire} mode={knobMode}
|
||||
outer={['fms_outer_up', 'fms_outer_down']} inner={['fms_inner_up', 'fms_inner_down']} push="cursor"
|
||||
|
||||
@@ -53,41 +53,36 @@ export default function DirectTo({ xp, onClose }) {
|
||||
<div className="dlg dto" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="dlg-head">DIRECT TO</div>
|
||||
<div className="dto-body">
|
||||
{/* ident line (cyan, edited like the FMS knob) + resolved name below */}
|
||||
<input
|
||||
ref={inputRef}
|
||||
className="dto-input"
|
||||
className="dto-ident"
|
||||
value={entry}
|
||||
onChange={(e) => { setEntry(e.target.value.toUpperCase()); setSel(null); }}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter' && sel) activate(); if (e.key === 'Escape') onClose(); }}
|
||||
placeholder="IDENT (z.B. KSEA, SEA, ELN)"
|
||||
placeholder="_ _ _ _"
|
||||
autoCapitalize="characters" autoCorrect="off" spellCheck="false"
|
||||
/>
|
||||
{hits.length > 0 && (
|
||||
<div className="dto-name">{sel ? (sel.name || sel.type) : ' '}</div>
|
||||
{hits.length > 0 && !sel && (
|
||||
<div className="dto-hits">
|
||||
{hits.map((h) => (
|
||||
<button key={h.id + h.lat} className={sel && sel.id === h.id ? 'on' : ''}
|
||||
onClick={() => { setSel(h); setEntry(h.id); setHits([]); }}>
|
||||
<b>{h.id}</b><i>{h.type}</i><span>{h.lat.toFixed(2)}, {h.lon.toFixed(2)}</span>
|
||||
<button key={h.id + h.lat} onClick={() => { setSel(h); setEntry(h.id); setHits([]); }}>
|
||||
<b>{h.id}</b><span>{h.name || h.type}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{sel && (
|
||||
<>
|
||||
<div className="dto-tgt"><span className="dto-id">{sel.id}</span><span className="dto-type">{sel.type}</span></div>
|
||||
<div className="dto-grid">
|
||||
<b>ALT</b><span>_____FT</span><b>OFFSET</b><span>+0NM</span>
|
||||
<b>BRG</b><span>{preview ? `${String(Math.round(preview.brg)).padStart(3, '0')}°` : '___°'}</span>
|
||||
<b>DIS</b><span>{preview ? `${preview.dist.toFixed(1)}NM` : '__._NM'}</span>
|
||||
<b>CRS</b><span>{preview ? `${String(Math.round(preview.brg)).padStart(3, '0')}°` : '___°'}</span>
|
||||
<span /><span />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="dlg-actions">
|
||||
<button className="fbtn" onClick={onClose}>CANCEL</button>
|
||||
<button className="fbtn add" disabled={!sel} onClick={activate}>ACTIVATE</button>
|
||||
<div className="dto-grid">
|
||||
<b>ALT</b><span>_____FT</span><b>OFFSET</b><span>+0NM</span>
|
||||
<b>BRG</b><span>{preview ? `${String(Math.round(preview.brg)).padStart(3, '0')}°` : '___°'}</span>
|
||||
<b>DIS</b><span>{preview ? `${preview.dist.toFixed(1)}NM` : '__._NM'}</span>
|
||||
<b>CRS</b><span>{preview ? `${String(Math.round(preview.brg)).padStart(3, '0')}°` : '___°'}</span>
|
||||
<span /><span />
|
||||
</div>
|
||||
<div className="dto-foot">
|
||||
<button className="dto-act" disabled={!sel} onClick={activate}>ACTIVATE</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -80,15 +80,15 @@ function MfdTopBar({ V, fp }) {
|
||||
<rect x="0" y="0" width="1000" height="70" fill="#000" />
|
||||
{[300, 660].map((x) => <line key={x} x1={x} y1="2" x2={x} y2="68" stroke="#333" strokeWidth="1.5" />)}
|
||||
<line x1="0" y1="70" x2="1000" y2="70" stroke="#3a3a3a" strokeWidth="2" />
|
||||
{/* NAV1 / NAV2 */}
|
||||
{/* NAV1 / NAV2 — standby LEFT (cyan, boxed), active RIGHT (white) per manual */}
|
||||
<text x="10" y="27" fill="#fff" fontSize="13">NAV1</text>
|
||||
<rect x="50" y="11" width="80" height="21" fill="none" stroke="#0ff" strokeWidth="1.3" />
|
||||
<text x="126" y="27" fill="#0ff" fontSize="17" textAnchor="end">{navF(V.nav1)}</text>
|
||||
<text x="126" y="27" fill="#0ff" fontSize="17" textAnchor="end">{navF(V.nav1Sb)}</text>
|
||||
{swap(150, 27)}
|
||||
<text x="174" y="27" fill="#fff" fontSize="17">{navF(V.nav1Sb)}</text>
|
||||
<text x="174" y="27" fill="#fff" fontSize="17">{navF(V.nav1)}</text>
|
||||
<text x="10" y="58" fill="#fff" fontSize="13">NAV2</text>
|
||||
<text x="126" y="58" fill="#fff" fontSize="17" textAnchor="end">{navF(V.nav2)}</text>
|
||||
<text x="174" y="58" fill="#fff" fontSize="17">{navF(V.nav2Sb)}</text>
|
||||
<text x="126" y="58" fill="#0ff" fontSize="17" textAnchor="end">{navF(V.nav2Sb)}</text>
|
||||
<text x="174" y="58" fill="#fff" fontSize="17">{navF(V.nav2)}</text>
|
||||
{/* centre: GS/DTK/TRK/ETE + active mode line */}
|
||||
<text x="312" y="27" fill="#fff" fontSize="13">GS</text>
|
||||
<text x="350" y="27" fill="#e040fb" fontSize="15" fontWeight="bold">{gs}</text>
|
||||
|
||||
@@ -152,10 +152,13 @@ export default function MapView({ values, flightPlan, fp, inset = false, hud = t
|
||||
const layer = navLayerRef.current;
|
||||
if (!layer) return;
|
||||
const z = map.getZoom();
|
||||
if (z < 6 || dcltrRef.current > 0) { layer.clearLayers(); return; }
|
||||
const types = z >= 10 ? 'apt,vor,ndb,fix' : z >= 8 ? 'apt,vor,ndb' : 'apt';
|
||||
const dc = dcltrRef.current || 0; // 0 all · 1 drop fixes · 2 drop fixes+NDB · 3 flight-plan only
|
||||
if (z < 6 || dc >= 3) { layer.clearLayers(); return; }
|
||||
let types = z >= 10 ? ['apt', 'vor', 'ndb', 'fix'] : z >= 8 ? ['apt', 'vor', 'ndb'] : ['apt'];
|
||||
if (dc >= 1) types = types.filter((t) => t !== 'fix');
|
||||
if (dc >= 2) types = types.filter((t) => t !== 'ndb');
|
||||
const b = map.getBounds();
|
||||
const url = `/api/nav/bbox?s=${b.getSouth()}&w=${b.getWest()}&n=${b.getNorth()}&e=${b.getEast()}&types=${types}&limit=${z >= 10 ? 500 : 250}`;
|
||||
const url = `/api/nav/bbox?s=${b.getSouth()}&w=${b.getWest()}&n=${b.getNorth()}&e=${b.getEast()}&types=${types.join(',')}&limit=${z >= 10 ? 500 : 250}`;
|
||||
try {
|
||||
navAbortRef.current?.abort();
|
||||
navAbortRef.current = new AbortController();
|
||||
@@ -208,7 +211,7 @@ export default function MapView({ values, flightPlan, fp, inset = false, hud = t
|
||||
const map = mapRef.current;
|
||||
if (!map) return;
|
||||
const t = terrain;
|
||||
const show = base === 'terrain' && t && Array.isArray(t.elev) && t.elev.length === t.rows * t.cols;
|
||||
const show = !!mapMode?.terrain && t && Array.isArray(t.elev) && t.elev.length === t.rows * t.cols;
|
||||
if (!show) {
|
||||
if (terrRef.current) { map.removeLayer(terrRef.current); terrRef.current = null; }
|
||||
return;
|
||||
@@ -234,7 +237,7 @@ export default function MapView({ values, flightPlan, fp, inset = false, hud = t
|
||||
terrRef.current.setBounds(bounds);
|
||||
terrRef.current.setUrl(cv.toDataURL());
|
||||
}
|
||||
}, [terrain, base]); // eslint-disable-line
|
||||
}, [terrain, mapMode?.terrain]); // eslint-disable-line
|
||||
|
||||
// G1000 UI sync: follow the in-sim map range (centre→top-edge NM). Inverse of
|
||||
// reportView: solve for the zoom that yields the target range at this latitude
|
||||
|
||||
+70
-17
@@ -132,7 +132,7 @@ function useEasedAngle(target, tau = 0.08) {
|
||||
return v;
|
||||
}
|
||||
|
||||
export default function PFD({ values: V, command, svt = true, inset = false, insetMode, nrst = false, onCloseNrst, tmr = false, onCloseTmr, dme = false, onCloseDme, alerts = false, onCloseAlerts, flightPlan, fp }) {
|
||||
export default function PFD({ values: V, command, connected = true, svt = true, inset = false, insetMode, nrst = false, onCloseNrst, tmr = false, onCloseTmr, dme = false, onCloseDme, alerts = false, onCloseAlerts, baroHpa = false, obs = false, minimums, onMinimums, flightPlan, fp }) {
|
||||
const wrapRef = useRef(null);
|
||||
const svgRef = useRef(null);
|
||||
const [box, setBox] = useState(null);
|
||||
@@ -151,6 +151,15 @@ export default function PFD({ values: V, command, svt = true, inset = false, ins
|
||||
const map = (b) => ({ left: offX + b.x * scale, top: offY + b.y * scale, width: b.w * scale, height: b.h * scale });
|
||||
setBox(map(SVT_BOX));
|
||||
setInsetBox(map(INSET_BOX));
|
||||
// Window zone (lower-right quadrant): right = altitude tape's right edge
|
||||
// (x≈938), bottom just above the XPDR strip (y≈736), left clear of the HSI
|
||||
// rose (x≈650), top below the baro box (y≈502). Exposed as CSS vars so the
|
||||
// windows sit embedded and never cover the HSI or the baro readout.
|
||||
const root = document.documentElement;
|
||||
root.style.setProperty('--gwin-right', `${Math.max(8, wr.width - (offX + 938 * scale))}px`);
|
||||
root.style.setProperty('--gwin-bottom', `${Math.max(8, wr.height - (offY + 736 * scale))}px`);
|
||||
root.style.setProperty('--gwin-maxw', `${(938 - 650) * scale}px`);
|
||||
root.style.setProperty('--gwin-maxh', `${(736 - 502) * scale}px`);
|
||||
};
|
||||
measure();
|
||||
const ro = new ResizeObserver(measure);
|
||||
@@ -199,15 +208,25 @@ export default function PFD({ values: V, command, svt = true, inset = false, ins
|
||||
<AFCS V={V} />
|
||||
<Marker V={V} />
|
||||
<AirspeedTape V={V} ias={iasS} />
|
||||
<AltitudeTape V={V} alt={altS} vs={vsS} />
|
||||
<AltitudeTape V={V} alt={altS} vs={vsS} baroHpa={baroHpa} minimums={minimums} />
|
||||
<GlideSlope V={V} />
|
||||
<HSI V={V} nav={nav} hdg={hdgS} />
|
||||
<HSI V={V} nav={nav} hdg={hdgS} obs={obs} />
|
||||
<HdgCrsBoxes V={V} nav={nav} />
|
||||
<Wind V={V} />
|
||||
<DataStrip V={V} />
|
||||
{/* sensor-failure flags (red X) when X-Plane isn't feeding data — the GDU
|
||||
blanks the affected display and shows a red X, like the real unit */}
|
||||
{!connected && (
|
||||
<g>
|
||||
<RedX x={150} y={113} w={700} h={322} label="AHRS" />
|
||||
<RedX x={60} y={110} w={84} h={350} />
|
||||
<RedX x={W - 154} y={110} w={84} h={350} />
|
||||
<RedX x={W / 2 - 140} y={500} w={280} h={260} label="HDG" />
|
||||
</g>
|
||||
)}
|
||||
</svg>
|
||||
{nrst && <Nearest values={V} onClose={onCloseNrst} />}
|
||||
{tmr && <TimerRef values={V} onClose={onCloseTmr} />}
|
||||
{tmr && <TimerRef values={V} onClose={onCloseTmr} minimums={minimums} onMinimums={onMinimums} />}
|
||||
{dme && <DmeWindow V={V} onClose={onCloseDme} />}
|
||||
{alerts && <AlertsWindow V={V} onClose={onCloseAlerts} />}
|
||||
{tune && <RadioTuner values={V} command={command} radio={tune} onClose={() => setTune(null)} />}
|
||||
@@ -228,16 +247,16 @@ function RadioBar({ V, onTune }) {
|
||||
{[330, 560, 690].map((x) => <line key={x} x1={x} y1="2" x2={x} y2="72" stroke="#333" strokeWidth="1.5" />)}
|
||||
<line x1="0" y1="74" x2={W} y2="74" stroke="#3a3a3a" strokeWidth="2" />
|
||||
|
||||
{/* NAV1 / NAV2 (left) */}
|
||||
{/* NAV1 / NAV2 — per manual: standby LEFT (cyan, boxed/tunable), active RIGHT (white) */}
|
||||
<text x="14" y="28" fill="#fff" fontSize="14">NAV1</text>
|
||||
<rect x="58" y="11" width="92" height="22" fill="none" stroke="#0ff" strokeWidth="1.4" />
|
||||
<text x="146" y="28" fill="#0ff" fontSize="19" textAnchor="end">{navF(V.nav1)}</text>
|
||||
<text x="146" y="28" fill="#0ff" fontSize="19" textAnchor="end">{navF(V.nav1Sb)}</text>
|
||||
{swap(176, 28)}
|
||||
<text x="206" y="28" fill="#fff" fontSize="19">{navF(V.nav1Sb)}</text>
|
||||
<text x="206" y="28" fill="#fff" fontSize="19">{navF(V.nav1)}</text>
|
||||
<text x="14" y="60" fill="#fff" fontSize="14">NAV2</text>
|
||||
<text x="146" y="60" fill="#0ff" fontSize="19" textAnchor="end">{navF(V.nav2)}</text>
|
||||
<text x="146" y="60" fill="#0ff" fontSize="19" textAnchor="end">{navF(V.nav2Sb)}</text>
|
||||
{swap(176, 60)}
|
||||
<text x="206" y="60" fill="#fff" fontSize="19">{navF(V.nav2Sb)}</text>
|
||||
<text x="206" y="60" fill="#fff" fontSize="19">{navF(V.nav2)}</text>
|
||||
|
||||
{/* centre: active leg + DIS/BRG */}
|
||||
<text x="430" y="26" fill="#e040fb" fontSize="20" textAnchor="middle">{'→'}</text>
|
||||
@@ -272,6 +291,20 @@ function RadioBar({ V, onTune }) {
|
||||
);
|
||||
}
|
||||
|
||||
// Red-X failure flag over a blanked instrument region (no valid sensor data).
|
||||
function RedX({ x, y, w, h, label }) {
|
||||
return (
|
||||
<g>
|
||||
<rect x={x} y={y} width={w} height={h} fill="#0a0d10" opacity="0.82" />
|
||||
<line x1={x} y1={y} x2={x + w} y2={y + h} stroke="#e01010" strokeWidth="4" />
|
||||
<line x1={x + w} y1={y} x2={x} y2={y + h} stroke="#e01010" strokeWidth="4" />
|
||||
{label && (
|
||||
<text x={x + w / 2} y={y + h / 2 + 6} textAnchor="middle" fill="#ffce46" fontSize="22" fontWeight="bold" fontFamily="monospace">{label}</text>
|
||||
)}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- DME tuning window (DME softkey) ---------------- */
|
||||
// Mirrors the G1000 DME window: source (NAV1/NAV2/HOLD), frequency, slant
|
||||
// distance, groundspeed and time-to-station — all from the sim's DME datarefs.
|
||||
@@ -555,7 +588,7 @@ function AirspeedTape({ V, ias: iasProp }) {
|
||||
}
|
||||
|
||||
/* ---------------- altitude tape + VSI + baro ---------------- */
|
||||
function AltitudeTape({ V, alt: altProp, vs: vsProp }) {
|
||||
function AltitudeTape({ V, alt: altProp, vs: vsProp, baroHpa = false, minimums }) {
|
||||
const alt = altProp != null ? altProp : num(V.altitude);
|
||||
const vs = vsProp != null ? vsProp : num(V.vspeed);
|
||||
const altBug = num(V.apAltBug), baro = num(V.baro, 29.92);
|
||||
@@ -624,7 +657,21 @@ function AltitudeTape({ V, alt: altProp, vs: vsProp }) {
|
||||
</g>
|
||||
{/* baro */}
|
||||
<rect x={x} y={top + h + 10} width={W2} height={26} fill="#000" stroke="#3a3a3a" />
|
||||
<text x={x + W2 / 2} y={top + h + 29} textAnchor="middle" fill="#0ff" fontSize="16">{baro.toFixed(2)} IN</text>
|
||||
<text x={x + W2 / 2} y={top + h + 29} textAnchor="middle" fill="#0ff" fontSize="16">{baroHpa ? `${Math.round(baro * 33.8639)} HPA` : `${baro.toFixed(2)} IN`}</text>
|
||||
{/* barometric minimums (BARO MIN): cyan bug on the tape + readout, amber
|
||||
"MINIMUMS" annunciation when at/below the decision altitude */}
|
||||
{minimums?.on && (
|
||||
<g fontFamily="monospace">
|
||||
{(() => { const my = Math.max(top, Math.min(top + h, cy + (alt - minimums.ft) * px)); return (
|
||||
<g><path d={`M${x} ${my} l-9 -6 v12 z`} fill="#19b8e6" /><text x={x - 12} y={my + 5} textAnchor="end" fill="#19b8e6" fontSize="11" fontWeight="bold">B</text></g>
|
||||
); })()}
|
||||
<rect x={x} y={top + h + 40} width={W2} height="22" fill="#000" stroke="#19395a" />
|
||||
<text x={x + 4} y={top + h + 56} fill="#19b8e6" fontSize="12">BARO {minimums.ft}<tspan fill="#0c9" fontSize="10">FT</tspan></text>
|
||||
{alt > 0 && alt <= minimums.ft && (
|
||||
<text x={x + W2 / 2} y={cy + 46} textAnchor="middle" fill="#ffce46" fontSize="15" fontWeight="bold">MINIMUMS</text>
|
||||
)}
|
||||
</g>
|
||||
)}
|
||||
{/* VSI to the right */}
|
||||
<VSI x={x + W2 + 34} cy={cy} h={h} vs={vs} bug={num(V.apVsBug)} />
|
||||
</g>
|
||||
@@ -651,7 +698,7 @@ function VSI({ x, cy, h, vs, bug }) {
|
||||
}
|
||||
|
||||
/* ---------------- HSI compass rose ---------------- */
|
||||
function HSI({ V, nav, hdg: hdgProp }) {
|
||||
function HSI({ V, nav, hdg: hdgProp, obs = false }) {
|
||||
const hdg = hdgProp != null ? hdgProp : ((num(V.heading) % 360) + 360) % 360;
|
||||
const bug = num(V.apHdgBug);
|
||||
// CDI source mirrors the in-sim G1000: 2 = GPS (magenta), 0/1 = VLOC1/2 (green).
|
||||
@@ -659,12 +706,18 @@ function HSI({ V, nav, hdg: hdgProp }) {
|
||||
// track + cross-track); on VLOC it follows the sim's VOR/LOC needle.
|
||||
const src = Math.round(num(V.cdiSrc, 2)); // default GPS when unknown
|
||||
const isGps = src === 2;
|
||||
const useNav = isGps && !!nav;
|
||||
// OBS mode (GPS): the course is the pilot-set OBS course (CRS knob); cross-track
|
||||
// is measured against the OBS radial through the active waypoint. Sequencing is
|
||||
// suspended (the leg does not auto-advance).
|
||||
const obsActive = obs && isGps && !!nav;
|
||||
const useNav = isGps && !!nav && !obsActive;
|
||||
const C = isGps ? '#e040fb' : '#00d800'; // magenta GPS / green VLOC
|
||||
const srcLabel = isGps ? 'GPS' : (src === 1 ? 'VLOC2' : 'VLOC1');
|
||||
const crs = useNav ? nav.dtk : num(V.obsCrs, 360);
|
||||
const def = useNav ? nav.def : num(V.hsiDef);
|
||||
const toFrom = useNav ? 1 : num(V.hsiToFrom);
|
||||
const srcLabel = obsActive ? 'OBS' : isGps ? 'GPS' : (src === 1 ? 'VLOC2' : 'VLOC1');
|
||||
const obsCrs = num(V.obsCrs, 360);
|
||||
const obsDef = obsActive ? Math.max(-2.5, Math.min(2.5, -(nav.dist * Math.sin((nav.brg - obsCrs) * D2R)) / 1.0)) : 0;
|
||||
const crs = obsActive ? obsCrs : useNav ? nav.dtk : obsCrs;
|
||||
const def = obsActive ? obsDef : useNav ? nav.def : num(V.hsiDef);
|
||||
const toFrom = (useNav || obsActive) ? 1 : num(V.hsiToFrom);
|
||||
const cx = W / 2, cy = 630, r = 130;
|
||||
|
||||
const ticks = [];
|
||||
|
||||
@@ -19,14 +19,16 @@ function fmt(sec) {
|
||||
return h > 0 ? `${pad(h)}:${pad(m)}:${pad(ss)}` : `${pad(m)}:${pad(ss)}`;
|
||||
}
|
||||
|
||||
export default function TimerRef({ values, onClose }) {
|
||||
export default function TimerRef({ values, onClose, minimums = { on: false, ft: 500 }, onMinimums }) {
|
||||
const [dir, setDir] = useState('up'); // 'up' | 'dn'
|
||||
const [running, setRunning] = useState(false);
|
||||
const [elapsed, setElapsed] = useState(0); // seconds
|
||||
const [target, setTarget] = useState(300); // count-down start (s)
|
||||
const [vbugs, setVbugs] = useState({}); // key -> bool (shown on tape, future)
|
||||
const [minsOn, setMinsOn] = useState(false);
|
||||
const [mins, setMins] = useState(500); // baro minimums (ft)
|
||||
// Minimums are lifted to App so the PFD altimeter can show the BARO MIN bug.
|
||||
const minsOn = minimums.on, mins = minimums.ft;
|
||||
const setMins = (fn) => onMinimums && onMinimums((m) => ({ ...m, ft: Math.max(0, typeof fn === 'function' ? fn(m.ft) : fn) }));
|
||||
const setMinsOn = (fn) => onMinimums && onMinimums((m) => ({ ...m, on: typeof fn === 'function' ? fn(m.on) : fn }));
|
||||
const tickRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
+47
-21
@@ -162,7 +162,8 @@ body {
|
||||
/* G1000 windows: flat opaque rectangles embedded in the display — no shadow,
|
||||
no rounded corners, thin light border. They look part of the screen. */
|
||||
.nrst-window {
|
||||
position: absolute; z-index: 4; right: 2%; top: 50%; bottom: 11%; width: 31%; max-width: 320px;
|
||||
position: absolute; z-index: 4; right: var(--gwin-right, 4%); bottom: var(--gwin-bottom, 6%); top: auto;
|
||||
width: var(--gwin-maxw, 290px); max-height: var(--gwin-maxh, 44%);
|
||||
display: flex; flex-direction: column;
|
||||
background: #05080b; border: 1px solid #7e8a94; border-radius: 0;
|
||||
color: #fff; font-family: 'Roboto Mono', monospace;
|
||||
@@ -200,10 +201,12 @@ body {
|
||||
.nrst-list { max-height: 62vh; overflow-y: auto; }
|
||||
/* DME + ALERTS popups (PFD DME / CAUTION softkeys) — left side, G1000 style */
|
||||
.pfd-pop {
|
||||
position: absolute; z-index: 4; top: 13%; left: 1.5%; width: 30%; max-width: 270px;
|
||||
position: absolute; z-index: 4; right: var(--gwin-right, 4%); bottom: var(--gwin-bottom, 6%); left: auto; top: auto;
|
||||
width: var(--gwin-maxw, 290px); max-height: var(--gwin-maxh, 44%);
|
||||
background: #05080b; border: 1px solid #7e8a94; border-radius: 0;
|
||||
color: #fff; font-family: 'Roboto Mono', monospace;
|
||||
}
|
||||
.pfd-pop.alerts { top: auto; }
|
||||
.pop-grid { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; padding: 8px 12px; font-size: 15px; }
|
||||
.pop-grid b { color: #6f808d; font-weight: normal; }
|
||||
.pop-grid span { color: #fff; text-align: right; }
|
||||
@@ -213,7 +216,6 @@ body {
|
||||
/* airway name labels on the MFD map */
|
||||
.awy-divicon { background: none; border: none; }
|
||||
.awy-lbl { color: #8fd0f0; font: 10px 'Roboto Mono', monospace; background: rgba(0,0,0,0.45); padding: 0 2px; border-radius: 2px; white-space: nowrap; }
|
||||
.pfd-pop.alerts { top: 46%; } /* below the DME window so both can be open */
|
||||
/* altitude alerter: flash the selected-altitude box (approaching / deviation) */
|
||||
.alt-alert { animation: altflash 1s steps(1, end) infinite; }
|
||||
@keyframes altflash { 50% { opacity: 0.25; } }
|
||||
@@ -288,7 +290,8 @@ body {
|
||||
.squawk-entry b { color: #19ff19; font-size: 18px; letter-spacing: 5px; margin-left: 6px; }
|
||||
/* TMR/REF window — left side of the PFD */
|
||||
.tmr-window {
|
||||
position: absolute; z-index: 4; bottom: 11%; right: 2%; width: 31%; max-width: 320px;
|
||||
position: absolute; z-index: 4; right: var(--gwin-right, 4%); bottom: var(--gwin-bottom, 6%);
|
||||
width: var(--gwin-maxw, 290px); max-height: var(--gwin-maxh, 44%); overflow-y: auto;
|
||||
background: #05080b; border: 1px solid #7e8a94; border-radius: 0;
|
||||
color: #fff; font-family: 'Roboto Mono', monospace;
|
||||
}
|
||||
@@ -317,30 +320,35 @@ body {
|
||||
.dlg-backdrop { position: fixed; inset: 0; z-index: 20; background: rgba(0,0,0,0.55); display: flex; align-items: center; justify-content: center; }
|
||||
/* G1000 side-window dialogs (PROC / Direct-To / FPL): compact panels in the
|
||||
display's lower-right, no screen dimming — like the real unit. */
|
||||
.gwin-backdrop { position: absolute; inset: 0; z-index: 20; background: transparent; display: flex; align-items: flex-end; justify-content: flex-end; padding: 0 2% 11% 0; }
|
||||
.dlg { background: #05080b; border: 1px solid #7e8a94; border-radius: 0; min-width: 280px; color: #fff; font-family: 'Roboto Mono', monospace; }
|
||||
.gwin-backdrop { position: absolute; inset: 0; z-index: 20; background: transparent; display: flex; align-items: flex-end; justify-content: flex-end; padding: 0 var(--gwin-right, 4%) var(--gwin-bottom, 6%) 0; }
|
||||
.dlg { background: #05080b; border: 1px solid #7e8a94; border-radius: 0; min-width: 0; color: #fff; font-family: 'Roboto Mono', monospace; }
|
||||
/* G1000 side-windows fill the lower-right zone (clear of HSI + baro box) */
|
||||
.gwin-backdrop .dlg, .fpl.win { width: var(--gwin-maxw, 290px); max-width: var(--gwin-maxw, 290px); max-height: var(--gwin-maxh, 44%); }
|
||||
.dlg-head { background: #0a0f14; padding: 6px 12px; border-bottom: 1px solid #2c343c; color: #36d2ff; font-weight: bold; letter-spacing: 2px; text-align: center; border-radius: 2px 2px 0 0; }
|
||||
.dto-arrow { color: #e040fb; margin-right: 8px; }
|
||||
.dto-body { padding: 12px; }
|
||||
.dto-lbl { color: #6f808d; font-size: 11px; display: block; margin-bottom: 4px; }
|
||||
.dto-input { width: 100%; box-sizing: border-box; background: #05080b; border: 1px solid #2c343c; color: #0ff; font: inherit; font-size: 20px; letter-spacing: 3px; padding: 6px 10px; text-transform: uppercase; }
|
||||
.dto-hits { display: flex; flex-direction: column; gap: 3px; margin-top: 6px; }
|
||||
.dto-hits button { display: flex; align-items: baseline; gap: 8px; background: #141a20; border: 1px solid #222b33; color: #cfd6dd; font: inherit; padding: 5px 8px; cursor: pointer; text-align: left; }
|
||||
.dto-hits button.on { border-color: #36d2ff; background: #0d2c38; }
|
||||
.dto-hits button b { color: #0ff; } .dto-hits button i { color: #0a8; font-style: normal; font-size: 11px; } .dto-hits button span { color: #6f808d; font-size: 11px; margin-left: auto; }
|
||||
.dto-tgt { display: flex; align-items: baseline; gap: 10px; margin-top: 10px; }
|
||||
.dto-tgt .dto-id { color: #36d2ff; font-size: 22px; font-weight: bold; letter-spacing: 1px; }
|
||||
.dto-tgt .dto-type { color: #6f808d; font-size: 11px; }
|
||||
.dto-grid { display: grid; grid-template-columns: auto 1fr auto 1fr; align-items: baseline; gap: 6px 8px; margin-top: 10px; padding-top: 8px; border-top: 1px solid #222; }
|
||||
.dto-body { padding: 10px 12px 12px; }
|
||||
.dto-ident { display: block; width: 100%; box-sizing: border-box; background: none; border: none; border-bottom: 1px solid #2c343c;
|
||||
color: #36d2ff; font: inherit; font-size: 24px; font-weight: bold; letter-spacing: 3px; padding: 2px 2px 4px; text-transform: uppercase; outline: none; }
|
||||
.dto-ident::placeholder { color: #2c4a57; }
|
||||
.dto-name { color: #cdd6dd; font-size: 13px; min-height: 17px; padding: 3px 2px 0; }
|
||||
.dto-hits { display: flex; flex-direction: column; gap: 2px; margin-top: 5px; }
|
||||
.dto-hits button { display: flex; align-items: baseline; gap: 10px; background: #0c1116; border: 1px solid #1c242c; color: #cfd6dd; font: inherit; padding: 4px 8px; cursor: pointer; text-align: left; }
|
||||
.dto-hits button:hover { background: #13202a; border-color: #36d2ff; }
|
||||
.dto-hits button b { color: #36d2ff; } .dto-hits button span { color: #6f808d; font-size: 11px; margin-left: auto; }
|
||||
.dto-grid { display: grid; grid-template-columns: auto 1fr auto 1fr; align-items: baseline; gap: 7px 8px; margin-top: 10px; padding-top: 9px; border-top: 1px solid #222; }
|
||||
.dto-grid b { color: #6f808d; font-weight: normal; font-size: 12px; }
|
||||
.dto-grid span { color: #fff; font-size: 15px; }
|
||||
.dto-foot { display: flex; justify-content: flex-end; margin-top: 12px; }
|
||||
.dto-act { background: #0c1116; border: 1px solid #7e8a94; color: #36d2ff; font: inherit; font-weight: bold; letter-spacing: 1px; font-size: 14px; padding: 5px 14px; cursor: pointer; }
|
||||
.dto-act:hover:not(:disabled) { background: #19b8e6; color: #042230; border-color: #19b8e6; }
|
||||
.dto-act:disabled { opacity: .4; cursor: default; }
|
||||
.dlg-actions { display: flex; gap: 8px; padding: 10px 12px; border-top: 1px solid #2c343c; }
|
||||
.dlg-actions .fbtn { flex: 1; }
|
||||
/* PROC dialog */
|
||||
.dlg.proc { width: 400px; max-width: 38%; }
|
||||
.dlg.proc.menu { width: 300px; }
|
||||
.dlg.proc, .dlg.proc.menu { width: var(--gwin-maxw, 290px); max-width: var(--gwin-maxw, 290px); display: flex; flex-direction: column; }
|
||||
.dlg.proc .proc-body { flex: 1; min-height: 0; display: flex; flex-direction: column; }
|
||||
.proc-menu { display: flex; flex-direction: column; padding: 4px 0; }
|
||||
.proc-menu-i { background: none; border: none; border-bottom: 1px solid #11161b; color: #d7e2ea; font: inherit; font-family: 'Roboto Mono', monospace; font-size: 14px; text-align: left; padding: 8px 12px; cursor: pointer; letter-spacing: .5px; }
|
||||
.proc-menu-i { background: none; border: none; border-bottom: 1px solid #11161b; color: #d7e2ea; font: inherit; font-family: 'Roboto Mono', monospace; font-size: 13px; text-align: left; padding: 6px 12px; cursor: pointer; letter-spacing: .5px; }
|
||||
.proc-menu-i:hover { background: #11161b; }
|
||||
.proc-menu-i.sel { background: #19b8e6; color: #042230; font-weight: bold; }
|
||||
.proc-back { background: #1c242c; border: 1px solid #2c343c; color: #36d2ff; font: inherit; font-size: 14px; line-height: 1; padding: 4px 9px; cursor: pointer; border-radius: 2px; }
|
||||
@@ -353,7 +361,7 @@ body {
|
||||
.proc-tabs { display: flex; gap: 4px; margin: 10px 0 6px; }
|
||||
.proc-tabs button { flex: 1; background: #1c242c; color: #9fb0bd; border: 1px solid #2c343c; font: inherit; font-size: 12px; padding: 5px; cursor: pointer; }
|
||||
.proc-tabs button.on { background: #19b8e6; color: #042230; font-weight: bold; border-color: #19b8e6; }
|
||||
.proc-cols { display: grid; grid-template-columns: 1fr 1fr 1.3fr; gap: 5px; height: 220px; }
|
||||
.proc-cols { display: grid; grid-template-columns: 1fr 1fr 1.3fr; gap: 5px; flex: 1; min-height: 0; }
|
||||
.proc-list, .proc-preview { background: #05080b; border: 1px solid #1c242c; overflow-y: auto; display: flex; flex-direction: column; }
|
||||
.proc-coltitle { position: sticky; top: 0; background: #11161b; color: #6f808d; font-size: 10px; padding: 4px 8px; border-bottom: 1px solid #222; }
|
||||
.proc-list button { background: none; border: none; border-bottom: 1px solid #11161b; color: #cfd6dd; font: inherit; font-size: 14px; text-align: left; padding: 6px 8px; cursor: pointer; }
|
||||
@@ -705,3 +713,21 @@ body {
|
||||
.fms-export { margin-top: 8px; font-size: 13px; padding: 8px; border-radius: 6px; }
|
||||
.fms-export.ok { background: #06330f; color: #9f9; }
|
||||
.fms-export.err { background: #330606; color: #f99; }
|
||||
|
||||
/* ---------------- Audio Panel (X1000) ---------------- */
|
||||
.audio-panel { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: var(--c-bg, #0f0f0f); }
|
||||
.apnl { width: min(420px, 94%); background: #15191e; border: 1px solid #2c343c; border-radius: 10px; padding: 14px 16px 18px; box-shadow: 0 8px 30px rgba(0,0,0,.5); font-family: var(--ui-font, 'Inter', system-ui); }
|
||||
.apnl-title { text-align: center; color: #36d2ff; font-weight: 700; letter-spacing: 2px; font-size: 14px; margin-bottom: 12px; }
|
||||
.apnl-grp { margin-bottom: 12px; }
|
||||
.apnl-h { color: #6f808d; font-size: 10px; letter-spacing: 1px; margin-bottom: 5px; }
|
||||
.apnl-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 8px; }
|
||||
.apk { display: flex; flex-direction: column; align-items: center; gap: 2px; background: #1c2229; border: 1px solid #313a44; border-radius: 6px; color: #c9d3db; font: inherit; font-size: 12px; font-weight: 600; padding: 9px 6px; cursor: pointer; box-shadow: inset 0 1px 0 rgba(255,255,255,.04); }
|
||||
.apk:hover { background: #232a32; }
|
||||
.apk .apk-s { font-size: 9px; color: #6f808d; font-weight: 400; }
|
||||
.apk.on { border-color: #19b8e6; background: #0d2c38; color: #7fe0ff; box-shadow: 0 0 0 1px #19b8e6, 0 0 10px rgba(25,184,230,.25); }
|
||||
.apk.mic.on { border-color: #16c116; background: #0c2a0c; color: #7bf07b; box-shadow: 0 0 0 1px #16c116, 0 0 10px rgba(22,193,22,.25); }
|
||||
.apnl-vol { display: flex; align-items: center; gap: 8px; margin-top: 6px; color: #9fb0bd; font-size: 11px; }
|
||||
.apnl-vol input { flex: 1; accent-color: #19b8e6; }
|
||||
.apnl-vol b { color: #fff; font-size: 12px; min-width: 26px; text-align: right; }
|
||||
.apnl-backup { width: 100%; margin-top: 6px; background: #3a0d0d; border: 1px solid #b53333; border-radius: 8px; color: #ff8a8a; font: inherit; font-weight: 700; letter-spacing: 1px; font-size: 12px; padding: 11px; cursor: pointer; }
|
||||
.apnl-backup:hover { background: #5a1414; color: #ffb0b0; }
|
||||
|
||||
Reference in New Issue
Block a user