FMS/CDU to manual completeness: FIX page, discontinuities, MOD/EXEC, 2 spd limits

- FIX INFO page (manual p17-20): reference navaid + crossing radial + distance →
  computes a fix waypoint (great-circle dest point) and inserts it into the plan.
- Flight-plan discontinuities (p25-26): coordinate-less / VECTORS legs render as
  "─ DISCONTINUITY ─" in LEGS; insert a waypoint to stitch, or LSK to clear it.
  Distance/track calc now skips disco legs (no NaN).
- MOD/ACT + EXEC light: edits arm the EXEC key (glows) and flip the page title to
  MOD…; EXEC commits/exports and clears it, like the real CDU.
- VNAV CLB/DES now take two speed/alt restrictions each (DEL clears the 2nd),
  per the manual.
- Page keys: added FIX; row relaid to 4×2 (FPLN/LEGS/DEP-ARR/DIR-INTC ·
  FIX/VNAV/PROG/MENU).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 20:40:12 +02:00
parent 95995211a0
commit ad592a7a77
2 changed files with 95 additions and 25 deletions
+90 -23
View File
@@ -22,10 +22,21 @@ function brng(a, b) {
const x = Math.cos(rad(a.lat)) * Math.sin(rad(b.lat)) - Math.sin(rad(a.lat)) * Math.cos(rad(b.lat)) * Math.cos(rad(b.lon - a.lon)); const x = Math.cos(rad(a.lat)) * Math.sin(rad(b.lat)) - Math.sin(rad(a.lat)) * Math.cos(rad(b.lat)) * Math.cos(rad(b.lon - a.lon));
return (deg(Math.atan2(y, x)) + 360) % 360; return (deg(Math.atan2(y, x)) + 360) % 360;
} }
// point at distance dNm along bearing brgDeg from a start lat/lon (great-circle).
function destPoint(lat, lon, brgDeg, dNm) {
const d = dNm / R_NM, t = rad(brgDeg), p1 = rad(lat), l1 = rad(lon);
const p2 = Math.asin(Math.sin(p1) * Math.cos(d) + Math.cos(p1) * Math.sin(d) * Math.cos(t));
const l2 = l1 + Math.atan2(Math.sin(t) * Math.sin(d) * Math.cos(p1), Math.cos(d) - Math.sin(p1) * Math.sin(p2));
return { lat: deg(p2), lon: ((deg(l2) + 540) % 360) - 180 };
}
const PAGE_KEYS = [['fpln', 'FPLN'], ['legs', 'LEGS'], ['deparr', 'DEP/ARR'], ['dir', 'DIR-INTC'], ['vnav', 'VNAV'], ['prog', 'PROG'], ['menu', 'MENU']]; const PAGE_KEYS = [['fpln', 'FPLN'], ['legs', 'LEGS'], ['deparr', 'DEP/ARR'], ['dir', 'DIR-INTC'], ['fix', 'FIX'], ['vnav', 'VNAV'], ['prog', 'PROG'], ['menu', 'MENU']];
const LEG_ROWS = 5; const LEG_ROWS = 5;
const VNAV_PAGES = ['CLB', 'CRZ', 'DES']; const VNAV_PAGES = ['CLB', 'CRZ', 'DES'];
// a flight-plan leg with no coordinates is a discontinuity (e.g. a VECTORS or
// heading-to-altitude procedure leg) — shown as "DISCONTINUITY" and stitched
// over by inserting a waypoint, per the FMS manual (p25-26).
const isDisco = (w) => !w || w.type === 'DISCO' || !isFinite(w.lat) || !isFinite(w.lon);
export default function CDU({ xp, vnav: vnavCfg, onVnav }) { export default function CDU({ xp, vnav: vnavCfg, onVnav }) {
const { flightPlan, fp, exportMsg, command } = xp; const { flightPlan, fp, exportMsg, command } = xp;
@@ -49,12 +60,21 @@ export default function CDU({ xp, vnav: vnavCfg, onVnav }) {
const [crzAlt, setCrzAlt] = useState(''); const [crzAlt, setCrzAlt] = useState('');
const [tgtSpd, setTgtSpd] = useState(''); const [tgtSpd, setTgtSpd] = useState('');
const [transAlt, setTransAlt] = useState('18000'); // CLB transition altitude (manual default) const [transAlt, setTransAlt] = useState('18000'); // CLB transition altitude (manual default)
const [clbLim, setClbLim] = useState('250/10000'); // climb speed/alt restriction const [clbLim, setClbLim] = useState(['250/10000', '']); // up to 2 climb speed/alt restrictions
const [desLim, setDesLim] = useState('250/10000'); // descent speed/alt restriction const [desLim, setDesLim] = useState(['250/10000', '']); // up to 2 descent speed/alt restrictions
const [vnavPage, setVnavPage] = useState(0); // 0 CLB · 1 CRZ · 2 DES const [vnavPage, setVnavPage] = useState(0); // 0 CLB · 1 CRZ · 2 DES
// FIX INFO (manual p17-20): reference navaid + radial/distance → a fix waypoint
const [fixRef, setFixRef] = useState(null); // resolved reference navaid {id,lat,lon}
const [fixRad, setFixRad] = useState(''); // crossing radial (deg)
const [fixDist, setFixDist] = useState(''); // distance along radial (NM)
// MOD/ACT: the EXEC light arms while edits are pending, like the real CDU.
const [mod, setMod] = useState(false);
// saved-plan list (MENU) // saved-plan list (MENU)
const [plans, setPlans] = useState(null); const [plans, setPlans] = useState(null);
// every plan edit goes through here so the EXEC light arms (MOD state).
const editPlan = (plan) => { fp.set(plan); setMod(true); };
const flash = (t) => { setMsg(t); setTimeout(() => setMsg(''), 2000); }; const flash = (t) => { setMsg(t); setTimeout(() => setMsg(''), 2000); };
const type = (ch) => { setMsg(''); setScr((s) => (s + ch).slice(0, 12)); }; 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(''); } }; const clr = () => { if (scr) setScr((s) => s.slice(0, -1)); else { setDel(false); setMsg(''); } };
@@ -71,26 +91,26 @@ export default function CDU({ xp, vnav: vnavCfg, onVnav }) {
const setOrigin = async (ident) => { const setOrigin = async (ident) => {
const h = await resolve(ident); if (!h) return flash('NOT IN DATABASE'); 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 }); editPlan({ name: 'ACTIVE', waypoints: [{ id: h.id, lat: h.lat, lon: h.lon, type: h.type || 'APT', alt: null }], activeLeg: 1 });
setScr(''); setScr('');
}; };
const setDest = async (ident) => { const setDest = async (ident) => {
const h = await resolve(ident); if (!h) return flash('NOT IN DATABASE'); const h = await resolve(ident); if (!h) return flash('NOT IN DATABASE');
const next = wps.filter((w) => w.id !== dest || w.type !== 'APT'); 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 }); 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 }); editPlan({ name: 'ACTIVE', waypoints: next, activeLeg: flightPlan.activeLeg ?? 1 });
setScr(''); setScr('');
}; };
const insertAt = async (ident, index) => { const insertAt = async (ident, index) => {
const h = await resolve(ident); if (!h) return flash('NOT IN DATABASE'); const h = await resolve(ident); if (!h) return flash('NOT IN DATABASE');
const next = wps.slice(); const next = wps.slice();
next.splice(index, 0, { id: h.id, lat: h.lat, lon: h.lon, type: h.type || 'WPT', alt: null }); 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 }); editPlan({ name: 'ACTIVE', waypoints: next, activeLeg: flightPlan.activeLeg ?? 1 });
setScr(''); setScr('');
}; };
const directTo = async (ident) => { const directTo = async (ident) => {
const h = await resolve(ident); if (!h) return flash('NOT IN DATABASE'); const h = await resolve(ident); if (!h) return flash('NOT IN DATABASE');
fp.set({ name: 'ACTIVE', waypoints: [ editPlan({ name: 'ACTIVE', waypoints: [
{ id: 'PPOS', lat: num(xp.values.lat), lon: num(xp.values.lon), type: 'USR' }, { 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' }, { id: h.id, lat: h.lat, lon: h.lon, type: h.type || 'WPT' },
] }); ] });
@@ -106,12 +126,25 @@ export default function CDU({ xp, vnav: vnavCfg, onVnav }) {
if (!legs.length) return flash('NO LEGS'); 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 tagged = t === 'approach' ? legs.map((l) => (l.seg === 'missed' ? { ...l, missed: true } : { ...l, appr: true })) : legs;
const merged = t === 'sid' ? [...tagged, ...wps] : [...wps, ...tagged]; const merged = t === 'sid' ? [...tagged, ...wps] : [...wps, ...tagged];
fp.set({ name: 'ACTIVE', waypoints: merged, activeLeg: t === 'sid' ? 1 : wps.length || 1 }); editPlan({ name: 'ACTIVE', waypoints: merged, activeLeg: t === 'sid' ? 1 : wps.length || 1 });
flash(`${name} LOADED`); setPage('legs'); flash(`${name} LOADED`); setPage('legs');
} catch { flash('PROC ERROR'); } } catch { flash('PROC ERROR'); }
}; };
const exec = () => { if (wps.length >= 2) fp.export(fltNo || 'WEBFPL'); else flash('NEED 2 WAYPOINTS'); }; // FIX INFO: resolve the reference navaid, then build a fix at radial/distance
const setRef = async (ident) => { const h = await resolve(ident); if (!h) return flash('NOT IN DATABASE'); setFixRef({ id: h.id, lat: h.lat, lon: h.lon }); setScr(''); };
const insertFix = () => {
if (!fixRef) return flash('SET REF NAVAID');
const radNum = parseFloat(fixRad); if (!isFinite(radNum)) return flash('SET RADIAL');
const d = parseFloat(fixDist) || 0;
const pt = d > 0 ? destPoint(fixRef.lat, fixRef.lon, radNum, d) : { lat: fixRef.lat, lon: fixRef.lon };
const id = `${fixRef.id}${String(Math.round(radNum)).padStart(3, '0')}`.slice(0, 5);
const next = wps.slice(); next.splice(Math.max(1, wps.length - 1), 0, { id, lat: pt.lat, lon: pt.lon, type: 'FIX' });
editPlan({ name: 'ACTIVE', waypoints: next, activeLeg: flightPlan.activeLeg ?? 1 });
flash(`FIX ${id} INSERTED`);
};
const exec = () => { if (wps.length >= 2) { fp.export(fltNo || 'WEBFPL'); setMod(false); flash('ACTIVATED'); } else flash('NEED 2 WAYPOINTS'); };
const openLoad = async () => setPlans(await fmsList()); const openLoad = async () => setPlans(await fmsList());
// ---- per-page line-select-key handling ---------------------------------- // ---- per-page line-select-key handling ----------------------------------
@@ -127,9 +160,10 @@ export default function CDU({ xp, vnav: vnavCfg, onVnav }) {
if (page === 'legs') { if (page === 'legs') {
const i = legPage * LEG_ROWS + r; const i = legPage * LEG_ROWS + r;
if (side === 'L') { if (side === 'L') {
if (scr) return insertAt(scr, Math.min(i, wps.length)); if (scr) return insertAt(scr, Math.min(i, wps.length)); // insert / stitch a discontinuity
if (del) { if (i < wps.length) fp.remove(i); setDel(false); return; } if (del) { if (i < wps.length) { fp.remove(i); setMod(true); } setDel(false); return; }
if (i >= 1 && i < wps.length) fp.setActive(i); if (i < wps.length && isDisco(wps[i])) { fp.remove(i); setMod(true); return; } // clear discontinuity
if (i >= 1 && i < wps.length) { fp.setActive(i); setMod(true); }
} }
return; return;
} }
@@ -150,17 +184,28 @@ export default function CDU({ xp, vnav: vnavCfg, onVnav }) {
return; return;
} }
if (page === 'dir') { if (scr) return directTo(scr); return; } if (page === 'dir') { if (scr) return directTo(scr); return; }
if (page === 'fix') {
if (side === 'L' && r === 0 && scr) return setRef(scr);
if (side === 'L' && r === 1 && scr) { setFixRad(scr); setScr(''); return; }
if (side === 'L' && r === 2 && scr) { setFixDist(scr); setScr(''); return; }
if (side === 'R' && r === 0) return insertFix();
return;
}
if (page === 'vnav') { if (page === 'vnav') {
if (vnavPage === 0) { // CLB: trans alt (1L), speed/alt limit (2L) if (vnavPage === 0) { // CLB: trans alt (1L), speed/alt limits (2L, 3L)
if (side === 'L' && r === 0 && scr) { setTransAlt(scr); setScr(''); return; } if (side === 'L' && r === 0 && scr) { setTransAlt(scr); setScr(''); return; }
if (side === 'L' && r === 1 && scr) { setClbLim(scr); setScr(''); return; } if (side === 'L' && r === 1 && scr) { setClbLim((a) => [scr, a[1]]); setScr(''); return; }
if (side === 'L' && r === 2 && scr) { setClbLim((a) => [a[0], scr]); setScr(''); return; }
if (side === 'L' && r === 2 && del) { setClbLim((a) => [a[0], '']); setDel(false); return; }
} else if (vnavPage === 1) { // CRZ: cruise alt (1L), target speed (1R) } else if (vnavPage === 1) { // CRZ: cruise alt (1L), target speed (1R)
if (side === 'L' && r === 0 && scr) { setCrzAlt(scr); setScr(''); return; } 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 === 0 && scr) { setTgtSpd(scr.replace('/', '')); setScr(''); return; }
} else { // DES: VPA (1R), speed/alt limit (2L), target speed (1L) } else { // DES: target speed (1L), VPA (1R), speed/alt limits (2L, 3L)
if (side === 'R' && r === 0 && scr && onVnav) { const v = parseFloat(scr); if (v >= 2 && v <= 6) onVnav((c) => ({ ...c, fpa: v })); setScr(''); return; } if (side === 'R' && r === 0 && scr && onVnav) { const v = parseFloat(scr); if (v >= 2 && v <= 6) onVnav((c) => ({ ...c, fpa: v })); setScr(''); return; }
if (side === 'L' && r === 0 && scr) { setTgtSpd(scr.replace('/', '')); setScr(''); return; } if (side === 'L' && r === 0 && scr) { setTgtSpd(scr.replace('/', '')); setScr(''); return; }
if (side === 'L' && r === 1 && scr) { setDesLim(scr); setScr(''); return; } if (side === 'L' && r === 1 && scr) { setDesLim((a) => [scr, a[1]]); setScr(''); return; }
if (side === 'L' && r === 2 && scr) { setDesLim((a) => [a[0], scr]); setScr(''); return; }
if (side === 'L' && r === 2 && del) { setDesLim((a) => [a[0], '']); setDel(false); return; }
} }
return; return;
} }
@@ -195,13 +240,19 @@ export default function CDU({ xp, vnav: vnavCfg, onVnav }) {
const rows = []; const rows = [];
for (let r = 0; r < LEG_ROWS; r++) { for (let r = 0; r < LEG_ROWS; r++) {
const i = legPage * 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 }); } if (i < wps.length) {
const w = wps[i], prev = wps[i - 1];
if (isDisco(w)) { rows.push({ disco: true }); continue; }
const linkable = prev && !isDisco(prev);
rows.push({ id: w.id, type: w.type, dtk: linkable ? Math.round(brng(prev, w)) : null, d: linkable ? distNm(prev, w) : 0, orig: i === 0, act: i === active, missed: w.missed });
}
else if (i === wps.length) rows.push({ add: true }); else if (i === wps.length) rows.push({ add: true });
else rows.push({ blank: true }); else rows.push({ blank: true });
} }
body = (<div className="cdu-cols2">{rows.map((row, r) => ( body = (<div className="cdu-cols2">{rows.map((row, r) => (
<div className={`cdu-row ${row.act ? 'act' : ''} ${row.missed ? 'dim' : ''}`} key={r}> <div className={`cdu-row ${row.act ? 'act' : ''} ${row.missed ? 'dim' : ''} ${row.disco ? 'disco' : ''}`} key={r}>
{row.blank ? <span className="cdu-empty">·</span> : row.add ? <span className="cdu-add">&lt;----- ENTER WPT</span> {row.blank ? <span className="cdu-empty">·</span> : row.add ? <span className="cdu-add">&lt;----- ENTER WPT</span>
: row.disco ? <span className="cdu-add"> DISCONTINUITY </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></>)} : (<><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>); </div>))}</div>);
} else if (page === 'deparr') { } else if (page === 'deparr') {
@@ -255,6 +306,17 @@ export default function CDU({ xp, vnav: vnavCfg, onVnav }) {
{wps.length >= 2 && <div className="cdu-note small">aktiv: {wps[active]?.id}</div>} {wps.length >= 2 && <div className="cdu-note small">aktiv: {wps[active]?.id}</div>}
</div> </div>
); );
} else if (page === 'fix') {
title = 'FIX INFO';
body = (
<div className="cdu-vnav">
<div className="cdu-fl"><label>REF</label><b>{fixRef?.id || '____'}</b></div>
<div className="cdu-fl"><label>RAD CROSS</label><b>{fixRad ? `${fixRad}°` : '---°'}</b></div>
<div className="cdu-fl"><label>DIST</label><b>{fixDist ? `${fixDist} NM` : '--- NM'}</b></div>
<div className="cdu-fl r"><label>INSERT</label><b>FIX&gt;</b></div>
<div className="cdu-note small">REF:1L · RADIAL:2L · DIST:3L · INSERT:1R</div>
</div>
);
} else if (page === 'vnav') { } else if (page === 'vnav') {
title = `ACT VNAV ${VNAV_PAGES[vnavPage]}`; pageNo = `${vnavPage + 1}/3`; title = `ACT VNAV ${VNAV_PAGES[vnavPage]}`; pageNo = `${vnavPage + 1}/3`;
if (vnavPage === 0) { // CLB (manual p21-22) if (vnavPage === 0) { // CLB (manual p21-22)
@@ -262,8 +324,9 @@ export default function CDU({ xp, vnav: vnavCfg, onVnav }) {
<div className="cdu-vnav"> <div className="cdu-vnav">
<div className="cdu-fl"><label>TRANS ALT</label><b>{transAlt || '18000'}</b></div> <div className="cdu-fl"><label>TRANS ALT</label><b>{transAlt || '18000'}</b></div>
<div className="cdu-fl r"><label>TGT SPD</label><b>{tgtSpd || '290/.74'}</b></div> <div className="cdu-fl r"><label>TGT SPD</label><b>{tgtSpd || '290/.74'}</b></div>
<div className="cdu-fl"><label>SPD/ALT LIMIT</label><b>{clbLim}</b></div> <div className="cdu-fl"><label>SPD/ALT LIM 1</label><b>{clbLim[0] || '-----'}</b></div>
<div className="cdu-note small">TRANS ALT: 1L · SPD/ALT LIMIT: 2L · NEXTCRZ</div> <div className="cdu-fl"><label>SPD/ALT LIM 2</label><b>{clbLim[1] || '-----'}</b></div>
<div className="cdu-note small">TRANS ALT:1L · SPD/ALT LIM:2L/3L (DEL clears) · NEXTCRZ</div>
</div> </div>
); );
} else if (vnavPage === 1) { // CRZ (manual p23) } else if (vnavPage === 1) { // CRZ (manual p23)
@@ -279,8 +342,9 @@ export default function CDU({ xp, vnav: vnavCfg, onVnav }) {
<div className="cdu-vnav"> <div className="cdu-vnav">
<div className="cdu-fl"><label>TGT SPD</label><b>{tgtSpd || '/200'}</b></div> <div className="cdu-fl"><label>TGT SPD</label><b>{tgtSpd || '/200'}</b></div>
<div className="cdu-fl r"><label>VPA</label><b>{(cfg.fpa || 3).toFixed(1)}°</b></div> <div className="cdu-fl r"><label>VPA</label><b>{(cfg.fpa || 3).toFixed(1)}°</b></div>
<div className="cdu-fl"><label>SPD/ALT LIMIT</label><b>{desLim}</b></div> <div className="cdu-fl"><label>SPD/ALT LIM 1</label><b>{desLim[0] || '-----'}</b></div>
<div className="cdu-note small">TGT SPD: 1L · VPA: 1R (2.0-6.0°) · SPD/ALT: 2L</div> <div className="cdu-fl"><label>SPD/ALT LIM 2</label><b>{desLim[1] || '-----'}</b></div>
<div className="cdu-note small">TGT SPD:1L · VPA:1R(2.0-6.0°) · SPD/ALT LIM:2L/3L</div>
</div> </div>
); );
} }
@@ -297,6 +361,9 @@ export default function CDU({ xp, vnav: vnavCfg, onVnav }) {
); );
} }
// MOD/ACT: the title and EXEC light reflect whether edits are pending.
if (title.startsWith('ACT ')) title = (mod ? 'MOD ' : 'ACT ') + title.slice(4);
const Lsk = ({ side, r }) => <button className={`cdu-lsk ${side}`} onClick={() => onLsk(side, 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 ( return (
@@ -328,7 +395,7 @@ export default function CDU({ xp, vnav: vnavCfg, onVnav }) {
<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)); else if (page === 'vnav') setVnavPage((p) => Math.min(2, p + 1)); }}>NEXT</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)); else if (page === 'vnav') setVnavPage((p) => Math.min(2, p + 1)); }}>NEXT</button>
<button className={`cdu-k fn ${del ? 'arm' : ''}`} onClick={() => { setDel((d) => !d); setScr(''); }}>DEL</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" onClick={clr}>CLR</button>
<button className="cdu-k fn exec" onClick={exec}>EXEC</button> <button className={`cdu-k fn exec ${mod ? 'arm' : ''}`} onClick={exec}>EXEC</button>
</div> </div>
<div className="cdu-pad"> <div className="cdu-pad">
+5 -2
View File
@@ -666,9 +666,12 @@ body {
.cdu-k:hover { border-color: #4a525b; } .cdu-k:active { transform: translateY(1px); background: #146b34; color: #fff; } .cdu-k:hover { border-color: #4a525b; } .cdu-k:active { transform: translateY(1px); background: #146b34; color: #fff; }
.cdu-k.fn { font-size: 12px; letter-spacing: .5px; color: #9fb0bd; } .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.arm { background: #7d5a10; color: #fff; border-color: #ffd24a; }
.cdu-k.fn.exec { background: linear-gradient(#1f8f47, #146b34); color: #fff; border-color: #2ee06a; } .cdu-k.fn.exec { background: linear-gradient(#173a25, #0f2a1a); color: #6f9a7e; border-color: #265a39; }
.cdu-k.fn.exec.arm { background: linear-gradient(#1f8f47, #146b34); color: #fff; border-color: #2ee06a; box-shadow: 0 0 9px rgba(46,224,106,.6); }
.cdu-row.disco { background: rgba(255,176,0,.10); }
.cdu-row.disco .cdu-add { color: #ffb000; }
/* page-key row + multi-page FMS bodies */ /* page-key row + multi-page FMS bodies */
.cdu-pages { display: grid; grid-template-columns: repeat(6, 1fr); gap: 6px; margin: 2px 0 6px; } .cdu-pages { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; margin: 2px 0 6px; }
.cdu-k.pg { font-size: 11px; letter-spacing: .3px; color: #9fb0bd; padding: 8px 2px; } .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-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-body { flex: 1; display: flex; flex-direction: column; min-height: 168px; padding-top: 4px; }