FMS: build out the CDU into a multi-page airliner FMS
Expands the single-page CDU into a Collins/Boeing-style FMS per the X-Plane FMS manual: FPLN (origin/dest/flt-no), LEGS (waypoints, insert/delete/activate), DEP/ARR (SID/STAR/approach from the CIFP parser, with transitions), DIR (direct-to), VNAV (cruise/target-speed/path-angle; VPA feeds the shared descent profile), and MENU (load/store .fms). Page keys + scratchpad + LSKs + keypad. All edits flow through the shared flight plan, which fms-sync.lua mirrors two-way into the in-sim FMS — so the app CDU and the aircraft CDU stay synchronized (no new Lua needed; reuses the existing sync + procedures.js). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+208
-71
@@ -1,17 +1,17 @@
|
||||
import React, { useState } from 'react';
|
||||
import { num, navSearch } from '../api/useXplane.js';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { num, navSearch, fmsList } from '../api/useXplane.js';
|
||||
|
||||
// FMS as an X-Plane-style CDU/FMC: a green screen showing the active flight plan
|
||||
// as legs, six line-select keys per side, a scratchpad, and an alphanumeric
|
||||
// keypad. Edits go through the shared flight plan (the same one the PFD/MFD use).
|
||||
// Airliner-style FMS / CDU (Collins/Boeing-like, per the X-Plane FMS manual).
|
||||
// A green screen with six line-select keys (LSK) per side, a scratch pad, page
|
||||
// keys and an alphanumeric keypad. Everything edits the SHARED flight plan (the
|
||||
// same one the PFD/MFD/map use), which the FlyWithLua fms-sync mirrors two-way
|
||||
// into the in-sim FMS — so the app CDU and the aircraft CDU stay synchronized.
|
||||
//
|
||||
// LSK (left, per row):
|
||||
// • scratchpad has an ident → insert that waypoint at the row
|
||||
// • DEL armed → delete the leg at the row
|
||||
// • otherwise → make that leg the active (magenta) leg (Direct-To)
|
||||
// EXEC exports the plan to X-Plane as an .fms file.
|
||||
// Pages: FPLN (origin/dest/flt-no) · LEGS (waypoints) · DEP/ARR (SID/STAR/APPR
|
||||
// via the CIFP parser) · DIR (direct-to) · VNAV (cruise/speed/path-angle) ·
|
||||
// MENU (load/store .fms).
|
||||
|
||||
const R_NM = 3440.065, rad = (d) => d * Math.PI / 180, deg = (r) => r * 180 / Math.PI;
|
||||
const R_NM = 3440.065, rad = (d) => (d * Math.PI) / 180, deg = (r) => (r * 180) / Math.PI;
|
||||
function distNm(a, b) {
|
||||
const dLat = rad(b.lat - a.lat), dLon = rad(b.lon - a.lon);
|
||||
const s = Math.sin(dLat / 2) ** 2 + Math.cos(rad(a.lat)) * Math.cos(rad(b.lat)) * Math.sin(dLon / 2) ** 2;
|
||||
@@ -23,63 +23,209 @@ function brng(a, b) {
|
||||
return (deg(Math.atan2(y, x)) + 360) % 360;
|
||||
}
|
||||
|
||||
const ROWS = 5; // legs visible per page
|
||||
const PAGE_KEYS = [['fpln', 'FPLN'], ['legs', 'LEGS'], ['deparr', 'DEP/ARR'], ['dir', 'DIR'], ['vnav', 'VNAV'], ['menu', 'MENU']];
|
||||
const LEG_ROWS = 5;
|
||||
|
||||
export default function CDU({ xp }) {
|
||||
const { flightPlan, fp, exportMsg } = xp;
|
||||
export default function CDU({ xp, vnav: vnavCfg, onVnav }) {
|
||||
const { flightPlan, fp, exportMsg, command } = xp;
|
||||
const wps = flightPlan.waypoints || [];
|
||||
const active = Math.max(1, Math.min(wps.length - 1, flightPlan.activeLeg ?? 1));
|
||||
const dest = [...wps].reverse().find((w) => w.type === 'APT')?.id || '';
|
||||
|
||||
const [page, setPage] = useState('fpln');
|
||||
const [scr, setScr] = useState('');
|
||||
const [del, setDel] = useState(false);
|
||||
const [msg, setMsg] = useState('');
|
||||
const [page, setPage] = useState(0);
|
||||
const [legPage, setLegPage] = useState(0);
|
||||
const [fltNo, setFltNo] = useState('');
|
||||
// DEP/ARR
|
||||
const [procs, setProcs] = useState(null);
|
||||
const [cat, setCat] = useState('sids'); // sids | stars | approaches
|
||||
const [procPage, setProcPage] = useState(0);
|
||||
// VNAV perf (FMS init values; VPA feeds the shared descent profile)
|
||||
const cfg = vnavCfg || { fpa: 3, offsetNm: 0, enabled: true };
|
||||
const [crzAlt, setCrzAlt] = useState('');
|
||||
const [tgtSpd, setTgtSpd] = useState('');
|
||||
// saved-plan list (MENU)
|
||||
const [plans, setPlans] = useState(null);
|
||||
|
||||
const pages = Math.max(1, Math.ceil((wps.length + 1) / ROWS));
|
||||
const start = page * ROWS;
|
||||
|
||||
const type = (ch) => { setMsg(''); setScr((s) => (s + ch).slice(0, 8)); };
|
||||
const flash = (t) => { setMsg(t); setTimeout(() => setMsg(''), 2000); };
|
||||
const type = (ch) => { setMsg(''); setScr((s) => (s + ch).slice(0, 12)); };
|
||||
const clr = () => { if (scr) setScr((s) => s.slice(0, -1)); else { setDel(false); setMsg(''); } };
|
||||
|
||||
// resolve an ident and splice it into the plan at `index`
|
||||
const insertAt = async (ident, index) => {
|
||||
const hits = await navSearch(ident);
|
||||
const hit = hits[0];
|
||||
if (!hit) { setMsg('NOT IN DATABASE'); return; }
|
||||
const next = wps.slice();
|
||||
next.splice(index, 0, { id: hit.id, lat: hit.lat, lon: hit.lon, type: hit.type || 'WPT', alt: null });
|
||||
// fetch destination procedures when entering DEP/ARR
|
||||
useEffect(() => {
|
||||
if (page !== 'deparr' || !dest) { return; }
|
||||
let alive = true;
|
||||
fetch(`/api/nav/procs?icao=${dest}`).then((r) => (r.ok ? r.json() : null)).then((d) => { if (alive) setProcs(d); }).catch(() => {});
|
||||
return () => { alive = false; };
|
||||
}, [page, dest]);
|
||||
|
||||
const resolve = async (ident) => (await navSearch(ident))[0] || null;
|
||||
|
||||
const setOrigin = async (ident) => {
|
||||
const h = await resolve(ident); if (!h) return flash('NOT IN DATABASE');
|
||||
fp.set({ name: 'ACTIVE', waypoints: [{ id: h.id, lat: h.lat, lon: h.lon, type: h.type || 'APT', alt: null }], activeLeg: 1 });
|
||||
setScr('');
|
||||
};
|
||||
const setDest = async (ident) => {
|
||||
const h = await resolve(ident); if (!h) return flash('NOT IN DATABASE');
|
||||
const next = wps.filter((w) => w.id !== dest || w.type !== 'APT');
|
||||
next.push({ id: h.id, lat: h.lat, lon: h.lon, type: h.type || 'APT', alt: null });
|
||||
fp.set({ name: 'ACTIVE', waypoints: next, activeLeg: flightPlan.activeLeg ?? 1 });
|
||||
setScr('');
|
||||
};
|
||||
|
||||
const lsk = (rowIdx) => {
|
||||
const i = start + rowIdx;
|
||||
if (scr) { insertAt(scr, Math.min(i, wps.length)); return; }
|
||||
if (del) { if (i < wps.length) fp.remove(i); setDel(false); return; }
|
||||
if (i >= 1 && i < wps.length) fp.setActive(i);
|
||||
const insertAt = async (ident, index) => {
|
||||
const h = await resolve(ident); if (!h) return flash('NOT IN DATABASE');
|
||||
const next = wps.slice();
|
||||
next.splice(index, 0, { id: h.id, lat: h.lat, lon: h.lon, type: h.type || 'WPT', alt: null });
|
||||
fp.set({ name: 'ACTIVE', waypoints: next, activeLeg: flightPlan.activeLeg ?? 1 });
|
||||
setScr('');
|
||||
};
|
||||
const directTo = async (ident) => {
|
||||
const h = await resolve(ident); if (!h) return flash('NOT IN DATABASE');
|
||||
fp.set({ name: 'ACTIVE', waypoints: [
|
||||
{ id: 'PPOS', lat: num(xp.values.lat), lon: num(xp.values.lon), type: 'USR' },
|
||||
{ id: h.id, lat: h.lat, lon: h.lon, type: h.type || 'WPT' },
|
||||
] });
|
||||
command && command('direct');
|
||||
setScr(''); flash(`DIRECT ${h.id}`);
|
||||
};
|
||||
// load a SID/STAR/approach's legs (CIFP) into the plan
|
||||
const loadProc = async (name, trans) => {
|
||||
const t = { sids: 'sid', stars: 'star', approaches: 'approach' }[cat];
|
||||
try {
|
||||
const r = await fetch(`/api/nav/proc?icao=${procs.icao}&type=${t}&name=${encodeURIComponent(name)}&trans=${encodeURIComponent(trans || '')}`);
|
||||
const legs = r.ok ? await r.json() : [];
|
||||
if (!legs.length) return flash('NO LEGS');
|
||||
const tagged = t === 'approach' ? legs.map((l) => (l.seg === 'missed' ? { ...l, missed: true } : { ...l, appr: true })) : legs;
|
||||
const merged = t === 'sid' ? [...tagged, ...wps] : [...wps, ...tagged];
|
||||
fp.set({ name: 'ACTIVE', waypoints: merged, activeLeg: t === 'sid' ? 1 : wps.length || 1 });
|
||||
flash(`${name} LOADED`); setPage('legs');
|
||||
} catch { flash('PROC ERROR'); }
|
||||
};
|
||||
|
||||
const exec = () => { if (wps.length >= 2) fp.export('WEBFPL'); else setMsg('NEED 2 WAYPOINTS'); };
|
||||
const exec = () => { if (wps.length >= 2) fp.export(fltNo || 'WEBFPL'); else flash('NEED 2 WAYPOINTS'); };
|
||||
const openLoad = async () => setPlans(await fmsList());
|
||||
|
||||
// build the visible rows
|
||||
const rows = [];
|
||||
for (let r = 0; r < ROWS; r++) {
|
||||
const i = start + r;
|
||||
if (i < wps.length) {
|
||||
const w = wps[i], prev = wps[i - 1];
|
||||
const d = prev ? distNm(prev, w) : 0;
|
||||
const dtk = prev ? Math.round(brng(prev, w)) : null;
|
||||
rows.push({ i, id: w.id, type: w.type, d, dtk, orig: i === 0, act: i === active });
|
||||
} else if (i === wps.length) {
|
||||
rows.push({ i, empty: true });
|
||||
} else {
|
||||
rows.push({ i, blank: true });
|
||||
// ---- per-page line-select-key handling ----------------------------------
|
||||
const onLsk = (side, r) => {
|
||||
if (page === 'fpln') {
|
||||
if (side === 'L' && r === 0 && scr) return setOrigin(scr);
|
||||
if (side === 'R' && r === 0 && scr) return setDest(scr);
|
||||
if (side === 'R' && r === 1) { if (scr) { setFltNo(scr); setScr(''); } return; }
|
||||
if (side === 'L' && r === 4) return setPage('menu');
|
||||
if (side === 'R' && r === 4) return setPage('vnav');
|
||||
return;
|
||||
}
|
||||
if (page === 'legs') {
|
||||
const i = legPage * LEG_ROWS + r;
|
||||
if (side === 'L') {
|
||||
if (scr) return insertAt(scr, Math.min(i, wps.length));
|
||||
if (del) { if (i < wps.length) fp.remove(i); setDel(false); return; }
|
||||
if (i >= 1 && i < wps.length) fp.setActive(i);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (page === 'deparr') {
|
||||
if (side === 'R' && r < 3) { setCat(['sids', 'stars', 'approaches'][r]); setProcPage(0); return; }
|
||||
if (side === 'L') {
|
||||
const list = (procs && procs[cat]) || [];
|
||||
const p = list[procPage * 5 + r];
|
||||
if (p) return loadProc(p.name, p.transitions && p.transitions[0]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (page === 'dir') { if (scr) return directTo(scr); return; }
|
||||
if (page === 'vnav') {
|
||||
if (side === 'L' && r === 0 && scr) { setCrzAlt(scr); setScr(''); return; }
|
||||
if (side === 'R' && r === 0 && scr) { setTgtSpd(scr.replace('/', '')); setScr(''); return; }
|
||||
if (side === 'R' && r === 2 && scr && onVnav) { const v = parseFloat(scr); if (v >= 2 && v <= 6) onVnav((c) => ({ ...c, fpa: v })); setScr(''); return; }
|
||||
return;
|
||||
}
|
||||
if (page === 'menu') {
|
||||
if (side === 'L' && r === 0) return openLoad();
|
||||
if (side === 'L' && r === 1) return exec();
|
||||
if (plans && side === 'L') { const n = plans[(r - 2)]; if (n) { fp.load(n); setPlans(null); flash(`${n} LOADED`); } }
|
||||
}
|
||||
};
|
||||
|
||||
// ---- page body (12 lines, mapped to LSK 1L..6L / 1R..6R) ----------------
|
||||
const A = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
|
||||
const KEYS = [A.slice(0, 7), A.slice(7, 14), A.slice(14, 21), A.slice(21, 26).concat(['/', ' ']), ['1', '2', '3', '4', '5'], ['6', '7', '8', '9', '0']];
|
||||
|
||||
const legPages = Math.max(1, Math.ceil((wps.length + 1) / LEG_ROWS));
|
||||
const procList = (procs && procs[cat]) || [];
|
||||
const procPages = Math.max(1, Math.ceil(procList.length / 5));
|
||||
|
||||
let title = 'ACT FPLN', pageNo = '1/1', body = null;
|
||||
if (page === 'fpln') {
|
||||
body = (
|
||||
<div className="cdu-fpln">
|
||||
<div className="cdu-fl"><label>ORIGIN</label><b>{wps[0]?.id || '____'}</b></div>
|
||||
<div className="cdu-fl r"><label>DEST</label><b>{dest || '____'}</b></div>
|
||||
<div className="cdu-fl r"><label>FLT NO</label><b>{fltNo || '--------'}</b></div>
|
||||
<div className="cdu-fl bot"><span className="cdu-link"><ROUTE MENU</span><span className="cdu-link r">VNAV></span></div>
|
||||
</div>
|
||||
);
|
||||
} else if (page === 'legs') {
|
||||
title = del ? 'DELETE' : 'ACT LEGS'; pageNo = `${legPage + 1}/${legPages}`;
|
||||
const rows = [];
|
||||
for (let r = 0; r < LEG_ROWS; r++) {
|
||||
const i = legPage * LEG_ROWS + r;
|
||||
if (i < wps.length) { const w = wps[i], prev = wps[i - 1]; rows.push({ id: w.id, type: w.type, dtk: prev ? Math.round(brng(prev, w)) : null, d: prev ? distNm(prev, w) : 0, orig: i === 0, act: i === active, missed: w.missed }); }
|
||||
else if (i === wps.length) rows.push({ add: true });
|
||||
else rows.push({ blank: true });
|
||||
}
|
||||
body = (<div className="cdu-cols2">{rows.map((row, r) => (
|
||||
<div className={`cdu-row ${row.act ? 'act' : ''} ${row.missed ? 'dim' : ''}`} key={r}>
|
||||
{row.blank ? <span className="cdu-empty">·</span> : row.add ? <span className="cdu-add"><----- ENTER WPT</span>
|
||||
: (<><span className="cdu-wpt">{row.id}<i>{row.type}</i></span><span className="cdu-dtk">{row.dtk == null ? '---' : `${String(row.dtk).padStart(3, '0')}°`}</span><span className="cdu-dist">{row.orig ? 'ORIG' : row.d.toFixed(1)}</span></>)}
|
||||
</div>))}</div>);
|
||||
} else if (page === 'deparr') {
|
||||
title = `${dest || '----'} ${cat === 'sids' ? 'DEPART' : cat === 'stars' ? 'ARRIVAL' : 'APPROACH'}`; pageNo = `${procPage + 1}/${procPages}`;
|
||||
const shown = procList.slice(procPage * 5, procPage * 5 + 5);
|
||||
body = (
|
||||
<div className="cdu-deparr">
|
||||
<div className="cdu-tabs"><span className={cat === 'sids' ? 'on' : ''}>SID></span><span className={cat === 'stars' ? 'on' : ''}>STAR></span><span className={cat === 'approaches' ? 'on' : ''}>APPR></span></div>
|
||||
{!procs && <div className="cdu-note">{dest ? 'loading…' : 'set DEST on FPLN'}</div>}
|
||||
{procs && shown.length === 0 && <div className="cdu-note">none</div>}
|
||||
{shown.map((p, i) => <div className="cdu-prow" key={p.name + i}><span><{p.name}</span>{p.transitions?.length ? <i>{p.transitions.length} TR</i> : null}</div>)}
|
||||
</div>
|
||||
);
|
||||
} else if (page === 'dir') {
|
||||
title = 'DIRECT TO';
|
||||
body = (
|
||||
<div className="cdu-dir">
|
||||
<div className="cdu-note">Ident eingeben, dann LSK 1L</div>
|
||||
<div className="cdu-fl"><label>DIRECT TO</label><b>{scr || '____'}</b></div>
|
||||
{wps.length >= 2 && <div className="cdu-note small">aktiv: {wps[active]?.id}</div>}
|
||||
</div>
|
||||
);
|
||||
} else if (page === 'vnav') {
|
||||
title = 'ACT VNAV';
|
||||
body = (
|
||||
<div className="cdu-vnav">
|
||||
<div className="cdu-fl"><label>CRZ ALT</label><b>{crzAlt || '-----'}</b></div>
|
||||
<div className="cdu-fl r"><label>TGT SPD</label><b>{tgtSpd || '---'}</b></div>
|
||||
<div className="cdu-fl r"><label>FPA / VPA</label><b>{(cfg.fpa || 3).toFixed(1)}°</b></div>
|
||||
<div className="cdu-note small">CRZ ALT: LSK1L · TGT SPD: LSK1R · VPA: LSK3R</div>
|
||||
</div>
|
||||
);
|
||||
} else if (page === 'menu') {
|
||||
title = plans ? 'CO ROUTE LIST' : 'ROUTE MENU';
|
||||
body = plans ? (
|
||||
<div className="cdu-menu">{plans.length === 0 ? <div className="cdu-note">keine .fms</div> : plans.slice(0, 5).map((n) => <div className="cdu-prow" key={n}><span><{n}</span><i>.fms</i></div>)}</div>
|
||||
) : (
|
||||
<div className="cdu-menu">
|
||||
<div className="cdu-prow"><span><LOAD (CO ROUTE)</span></div>
|
||||
<div className="cdu-prow"><span><STORE / EXPORT</span></div>
|
||||
<div className="cdu-note small">STORE schreibt {fltNo || 'WEBFPL'}.fms</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const A = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
|
||||
const KEYS = [A.slice(0, 7), A.slice(7, 14), A.slice(14, 21), A.slice(21, 26).concat([' ']), ['1', '2', '3', '4', '5'], ['6', '7', '8', '9', '0']];
|
||||
|
||||
const Lsk = ({ side, r }) => <button className={`cdu-lsk ${side}`} onClick={() => lsk(r)} aria-label={`LSK ${r + 1}${side}`} />;
|
||||
const Lsk = ({ side, r }) => <button className={`cdu-lsk ${side}`} onClick={() => onLsk(side, r)} aria-label={`LSK${r + 1}${side}`} />;
|
||||
|
||||
return (
|
||||
<div className="cdu">
|
||||
@@ -87,22 +233,8 @@ export default function CDU({ xp }) {
|
||||
<div className="cdu-screenwrap">
|
||||
<div className="cdu-lsks left">{[0, 1, 2, 3, 4].map((r) => <Lsk key={r} side="L" r={r} />)}</div>
|
||||
<div className="cdu-screen">
|
||||
<div className="cdu-hdr">
|
||||
<span>{del ? 'DELETE' : 'ACT FPL'}</span>
|
||||
<span>{page + 1}/{pages}</span>
|
||||
</div>
|
||||
<div className="cdu-cols"><span>WPT</span><span>DTK</span><span>DIST</span></div>
|
||||
{rows.map((row) => (
|
||||
<div className={`cdu-row ${row.act ? 'act' : ''}`} key={row.i}>
|
||||
{row.blank ? <span className="cdu-empty">·</span>
|
||||
: row.empty ? <span className="cdu-add"><------ ENTER WPT</span>
|
||||
: (<>
|
||||
<span className="cdu-wpt">{row.id}<i>{row.type}</i></span>
|
||||
<span className="cdu-dtk">{row.dtk == null ? '---' : `${String(row.dtk).padStart(3, '0')}°`}</span>
|
||||
<span className="cdu-dist">{row.orig ? 'ORIG' : `${row.d.toFixed(1)}`}</span>
|
||||
</>)}
|
||||
</div>
|
||||
))}
|
||||
<div className="cdu-hdr"><span>{title}</span><span>{pageNo}</span></div>
|
||||
<div className="cdu-body">{body}</div>
|
||||
<div className="cdu-scratch">
|
||||
<span className="cdu-sp">{scr || (del ? 'DELETE—SEL LEG' : '')}</span>
|
||||
{msg && <span className="cdu-msg">{msg}</span>}
|
||||
@@ -112,9 +244,16 @@ export default function CDU({ xp }) {
|
||||
<div className="cdu-lsks right">{[0, 1, 2, 3, 4].map((r) => <Lsk key={r} side="R" r={r} />)}</div>
|
||||
</div>
|
||||
|
||||
{/* page keys */}
|
||||
<div className="cdu-pages">
|
||||
{PAGE_KEYS.map(([id, lbl]) => (
|
||||
<button key={id} className={`cdu-k pg ${page === id ? 'on' : ''}`} onClick={() => { setPage(id); setScr(''); setDel(false); setPlans(null); }}>{lbl}</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="cdu-fn">
|
||||
<button className="cdu-k fn" onClick={() => setPage((p) => Math.max(0, p - 1))}>PREV</button>
|
||||
<button className="cdu-k fn" onClick={() => setPage((p) => Math.min(pages - 1, p + 1))}>NEXT</button>
|
||||
<button className="cdu-k fn" onClick={() => { if (page === 'legs') setLegPage((p) => Math.max(0, p - 1)); else if (page === 'deparr') setProcPage((p) => Math.max(0, p - 1)); }}>PREV</button>
|
||||
<button className="cdu-k fn" onClick={() => { if (page === 'legs') setLegPage((p) => Math.min(legPages - 1, p + 1)); else if (page === 'deparr') setProcPage((p) => Math.min(procPages - 1, p + 1)); }}>NEXT</button>
|
||||
<button className={`cdu-k fn ${del ? 'arm' : ''}`} onClick={() => { setDel((d) => !d); setScr(''); }}>DEL</button>
|
||||
<button className="cdu-k fn" onClick={clr}>CLR</button>
|
||||
<button className="cdu-k fn exec" onClick={exec}>EXEC</button>
|
||||
@@ -123,9 +262,7 @@ export default function CDU({ xp }) {
|
||||
<div className="cdu-pad">
|
||||
{KEYS.map((rowK, ri) => (
|
||||
<div className="cdu-padrow" key={ri}>
|
||||
{rowK.map((k) => (
|
||||
<button key={k} className="cdu-k" onClick={() => type(k === ' ' ? ' ' : k)}>{k === ' ' ? 'SP' : k}</button>
|
||||
))}
|
||||
{rowK.map((k) => <button key={k} className="cdu-k" onClick={() => type(k)}>{k === ' ' ? 'SP' : k}</button>)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -667,6 +667,26 @@ body {
|
||||
.cdu-k.fn { font-size: 12px; letter-spacing: .5px; color: #9fb0bd; }
|
||||
.cdu-k.fn.arm { background: #7d5a10; color: #fff; border-color: #ffd24a; }
|
||||
.cdu-k.fn.exec { background: linear-gradient(#1f8f47, #146b34); color: #fff; border-color: #2ee06a; }
|
||||
/* page-key row + multi-page FMS bodies */
|
||||
.cdu-pages { display: grid; grid-template-columns: repeat(6, 1fr); gap: 6px; margin: 2px 0 6px; }
|
||||
.cdu-k.pg { font-size: 11px; letter-spacing: .3px; color: #9fb0bd; padding: 8px 2px; }
|
||||
.cdu-k.pg.on { background: linear-gradient(#0f5a2c, #0b3f1f); color: #9fffc0; border-color: #2ee06a; }
|
||||
.cdu-body { flex: 1; display: flex; flex-direction: column; min-height: 168px; padding-top: 4px; }
|
||||
.cdu-cols2 { display: flex; flex-direction: column; }
|
||||
.cdu-row.dim .cdu-wpt { color: #6f9a7e; } .cdu-row.dim .cdu-wpt i { color: #14502a; }
|
||||
.cdu-fpln, .cdu-vnav { display: flex; flex-direction: column; gap: 10px; }
|
||||
.cdu-fl { display: flex; flex-direction: column; }
|
||||
.cdu-fl.r { align-items: flex-end; }
|
||||
.cdu-fl label { color: #1f9d52; font-size: 11px; letter-spacing: 1px; }
|
||||
.cdu-fl b { color: #fff; font-size: 19px; font-weight: 600; }
|
||||
.cdu-fl.bot { margin-top: auto; flex-direction: row; justify-content: space-between; }
|
||||
.cdu-link { color: #34e06a; font-size: 13px; } .cdu-link.r { text-align: right; }
|
||||
.cdu-deparr, .cdu-dir, .cdu-menu { display: flex; flex-direction: column; gap: 4px; }
|
||||
.cdu-tabs { display: flex; justify-content: flex-end; gap: 12px; margin-bottom: 4px; }
|
||||
.cdu-tabs span { color: #1f9d52; font-size: 12px; } .cdu-tabs span.on { color: #9fffc0; font-weight: 700; }
|
||||
.cdu-prow { display: flex; justify-content: space-between; align-items: baseline; font-size: 16px; color: #fff; padding: 3px 0; border-bottom: 1px solid #06250f; }
|
||||
.cdu-prow i { color: #1f9d52; font-style: normal; font-size: 11px; }
|
||||
.cdu-note { color: #1f9d52; font-size: 13px; padding: 4px 0; } .cdu-note.small { color: #167d3f; font-size: 11px; margin-top: auto; }
|
||||
|
||||
/* MFD */
|
||||
.mfd { display: flex; gap: 24px; align-items: center; flex-wrap: wrap; justify-content: center; width: 100%; }
|
||||
|
||||
Reference in New Issue
Block a user