diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index 2ccd62e..449e99c 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -5900,7 +5900,7 @@ dependencies = [ [[package]] name = "xplane-cockpit" -version = "0.1.3" +version = "0.1.4" dependencies = [ "local-ip-address", "serde", diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index a7a2b9c..54446cb 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "xplane-cockpit" -version = "0.1.3" +version = "0.1.4" description = "Desktop launcher for the X-Plane G1000 web cockpit" authors = ["karim"] edition = "2021" diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index f363e17..27defc9 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "X-Plane Cockpit", - "version": "0.1.3", + "version": "0.1.4", "identifier": "ch.kgva.xplanecockpit", "build": { "frontendDist": "../ui" diff --git a/web/src/App.jsx b/web/src/App.jsx index 0aaee80..7464b80 100644 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -45,9 +45,14 @@ export default function App() { const [navWide, setNavWide] = useState(() => localStorage.getItem('navWide') === '1'); const go = (id) => { setTab(id); history.replaceState(null, '', `#${id}`); }; const toggleNav = () => setNavWide((w) => { localStorage.setItem('navWide', w ? '0' : '1'); return !w; }); + // Knob interaction: 'arrows' (visible ˄‹›˅, touch-friendly) or 'zones' (click + // the knob face). Settable in the settings panel, remembered. + const [knobMode, setKnobMode] = useState(() => localStorage.getItem('knobMode') || 'arrows'); + const [settings, setSettings] = useState(false); + const setKnob = (m) => { localStorage.setItem('knobMode', m); setKnobMode(m); }; // Synthetic-terrain (3D) vs. classic blue/brown attitude — toggled by the // PFD → SYN TERR softkey, exactly like the real XPLANE 1000. - const [svt3d, setSvt3d] = useState(true); + const [svt3d, setSvt3d] = useState(false); // The PFD INSET map (bottom-left) is off by default and toggled by its softkey. const [inset, setInset] = useState(false); // INSET map options (base layer + declutter), set from the INSET submenu. @@ -82,6 +87,13 @@ export default function App() { ))} +
{connText} @@ -90,7 +102,7 @@ export default function App() {
{tab === 'pfd' && ( - setSvt3d((v) => !v)} + setSvt3d((v) => !v)} inset={inset} onSetInset={setInset} insetMode={insetMode} onInsetMode={setInsetMode} nrst={nrst} onToggleNrst={() => setNrst((v) => !v)} onDirect={() => setDto(true)} tmr={tmr} onToggleTmr={() => setTmr((v) => !v)} onProc={() => setProc(true)}> @@ -99,17 +111,33 @@ export default function App() { )} {tab === 'mfd' && ( - setDto(true)} onProc={() => setProc(true)}> + setDto(true)} onProc={() => setProc(true)}> )} {tab === 'map' && } {tab === 'fms' && } - {tab === 'vfr' && } + {tab === 'vfr' && } {tab === 'ap' && }
{dto && setDto(false)} />} {proc && setProc(false)} />} + {settings && ( +
setSettings(false)}> +
e.stopPropagation()} style={{ minWidth: 360 }}> +
EINSTELLUNGEN
+
+
Knopf-Bedienung
+
+ + +
+
Pfeiltasten sind touch-freundlich. Klickzonen: oben/unten = grob, links/rechts = fein, Mitte = PUSH.
+
+
+
+
+ )}
); } diff --git a/web/src/components/Bezel.jsx b/web/src/components/Bezel.jsx index 3196b28..daaaac4 100644 --- a/web/src/components/Bezel.jsx +++ b/web/src/components/Bezel.jsx @@ -34,7 +34,7 @@ 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, onDirect, onProc, mapMode, onMapMode, children }) { +export default function Bezel({ variant = 'pfd', xp, svt3d, onToggleSvt, inset, onSetInset, insetMode, onInsetMode, nrst, onToggleNrst, tmr, onToggleTmr, onDirect, onProc, mapMode, onMapMode, 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 @@ -105,12 +105,12 @@ export default function Bezel({ variant = 'pfd', xp, svt3d, onToggleSvt, inset, return (
- - {variant === 'mfd' && xp && } -
@@ -133,18 +133,18 @@ export default function Bezel({ variant = 'pfd', xp, svt3d, onToggleSvt, inset,
- - -
- D→MENU - FPLPROC - CLRENT + D→MENU + FPLPROC + CLRENT
-
@@ -186,30 +186,43 @@ function APController({ xp }) { // the mouse wheel; the inner ring via the top/bottom arrows (˄ ˅) and shift+wheel. // Clicking the knob centre fires the push action (PUSH …). The RANGE knob also // pans with a directional cross. -function Knob({ label, sub, outer, inner, push, big, joy, pan, fire }) { +function Knob({ label, sub, outer, inner, push, big, joy, pan, fire, mode = 'arrows' }) { const onWheel = (e) => { if (!outer) return; e.preventDefault(); const set = (e.shiftKey && inner) ? inner : outer; fire(e.deltaY < 0 ? set[0] : set[1]); }; + const zoneClick = (e) => { + const r = e.currentTarget.getBoundingClientRect(); + const dx = e.clientX - (r.left + r.width / 2); + const dy = e.clientY - (r.top + r.height / 2); + const rel = Math.hypot(dx, dy) / (r.width / 2); + if (rel < 0.42 && push) { fire(push); return; } // centre → PUSH + if (Math.abs(dy) >= Math.abs(dx)) { if (outer) fire(dy < 0 ? outer[0] : outer[1]); } + else if (inner) fire(dx > 0 ? inner[0] : inner[1]); + else if (outer) fire(dx > 0 ? outer[0] : outer[1]); + }; + const zones = mode === 'zones'; return (
{label} -
- {inner && } - {outer && } +
+ {/* arrows mode (touch-friendly): visible ˄‹›˅ buttons. zones mode: click + the knob face itself (top/bottom = outer, left/right = inner). */} + {!zones && inner && } + {!zones && outer && } - {outer && } - {inner && } + {!zones && outer && } + {!zones && inner && }
{pan && (
diff --git a/web/src/components/KAP140.jsx b/web/src/components/KAP140.jsx new file mode 100644 index 0000000..e5b4f7b --- /dev/null +++ b/web/src/components/KAP140.jsx @@ -0,0 +1,68 @@ +import React from 'react'; +import { num } from '../api/useXplane.js'; + +// Bendix/King KAP 140 — the panel-mounted autopilot in the steam-gauge Cessna +// 172. A green segment LCD annunciates the active modes + armed altitude, with a +// row of buttons (AP HDG NAV APR REV ALT, UP/DN, BARO) and the ALT knob. Buttons +// fire X-Plane's own autopilot commands; annunciation comes from autopilot_state. +const BITS = { fd: 1 << 0, hdg: 1 << 1, vs: 1 << 4, flc: 1 << 6, nav: 1 << 8, apr: 1 << 9, alt: 1 << 14, bc: 1 << 18 }; +const on = (s, b) => (num(s) & b) !== 0; + +export default function KAP140({ xp }) { + const { values: V, command, setDataref } = xp; + const s = num(V.apState), eng = num(V.apEngaged) > 0; + const lat = on(s, BITS.apr) ? 'APR' : on(s, BITS.nav) ? 'NAV' : on(s, BITS.bc) ? 'REV' : on(s, BITS.hdg) ? 'HDG' : 'ROL'; + const vert = on(s, BITS.alt) ? 'ALT' : on(s, BITS.vs) ? 'VS' : ''; + const selAlt = Math.round(num(V.apAltBug)); + const vs = Math.round(num(V.apVsBug)); + + const Btn = ({ label, cmd }) => ( + + ); + // A rotary knob: click the upper half to step up, lower half to step down + // (also scroll). No +/- buttons. + const turn = (e, fn) => { + const r = e.currentTarget.getBoundingClientRect(); + fn(((e.clientY - r.top) < r.height / 2) ? +1 : -1); + }; + const altStep = (d) => setDataref('apAltBug', selAlt + d * 100); + + return ( +
+
KAP 140
+
+
+ AP + {lat} + {vert} +
+
+ {selAlt}FT + {vs >= 0 ? '▲' : '▼'} {Math.abs(vs)}FPM +
+
+
+ + + + + + +
+ + +
+ +
+
+
turn(e, altStep)} + onWheel={(e) => { e.preventDefault(); altStep(e.deltaY < 0 ? 1 : -1); }}> + + +
+ ALT +
+
+ ); +} diff --git a/web/src/components/MFD.jsx b/web/src/components/MFD.jsx index 3f44aaf..fdea8b2 100644 --- a/web/src/components/MFD.jsx +++ b/web/src/components/MFD.jsx @@ -73,8 +73,12 @@ function EisStrip({ V }) { const rpm = arr(V.engRpm); const ffGph = (arr(V.fuelFlow) * 3600) / KG_PER_GAL; const oilPsi = arr(V.oilPress); - const oilF = arr(V.oilTemp) * 9 / 5 + 32; - const egtF = arr(V.egt) * 9 / 5 + 32; + // X-Plane's temperature indicator datarefs may already honor the user's unit + // (°F) despite the "_deg_C" name. Auto-detect: only convert if it still looks + // like Celsius, so we don't double-convert (which pegged the gauges red). + const oilT = arr(V.oilTemp), egtT = arr(V.egt); + const oilF = oilT > 150 ? oilT : oilT * 9 / 5 + 32; + const egtF = egtT > 900 ? egtT : egtT * 9 / 5 + 32; const fuelL = arr(V.fuelQty, 0) / KG_PER_GAL; const fuelR = arr(V.fuelQty, 1) / KG_PER_GAL; const volts = arr(V.volts, 0, 28); diff --git a/web/src/components/PFD.jsx b/web/src/components/PFD.jsx index 72c4354..3cffc21 100644 --- a/web/src/components/PFD.jsx +++ b/web/src/components/PFD.jsx @@ -1,4 +1,4 @@ -import React, { useRef, useState, useLayoutEffect, Suspense, lazy } from 'react'; +import React, { useRef, useState, useEffect, useLayoutEffect, Suspense, lazy } from 'react'; import { num } from '../api/useXplane.js'; import MapView from './MapView.jsx'; import Nearest from './Nearest.jsx'; @@ -191,15 +191,15 @@ function RadioBar({ V }) { BRG {/* COM1 / COM2 (right) */} - {comF(V.com1)} - {swap(848, 28)} - - {comF(V.com1Sb)} - COM1 - {comF(V.com2)} - {swap(848, 60)} - {comF(V.com2Sb)} - COM2 + {comF(V.com1)} + {swap(844, 28)} + + {comF(V.com1Sb)} + COM1 + {comF(V.com2)} + {swap(844, 60)} + {comF(V.com2Sb)} + COM2 ); } @@ -209,16 +209,39 @@ function Attitude({ V, svt }) { const pitch = num(V.pitch), roll = num(V.roll), slip = num(V.slip); const fdP = num(V.fdPitch), fdR = num(V.fdRoll); const cx = W / 2, cy = 270; - const off = pitch * PITCH_PX; + const rollRef = useRef(null), pitchRef = useRef(null), fdRef = useRef(null), bankRef = useRef(null); + // Target attitude (updated every render); a rAF loop eases the displayed + // transforms toward it at 60 fps — decoupled from X-Plane's ~20 Hz samples, + // so the horizon glides instead of stepping. + const tgt = useRef({ p: 0, r: 0, fp: 0, fr: 0 }); + tgt.current = { p: pitch, r: roll, fp: pitch - fdP, fr: roll - fdR }; + useEffect(() => { + let raf; const d = { ...tgt.current }; + const loop = () => { + const t = tgt.current, k = 0.4; + d.p += (t.p - d.p) * k; d.r += (t.r - d.r) * k; + d.fp += (t.fp - d.fp) * k; d.fr += (t.fr - d.fr) * k; + rollRef.current?.setAttribute('transform', `rotate(${-d.r} ${cx} ${cy})`); + pitchRef.current?.setAttribute('transform', `translate(0 ${d.p * PITCH_PX})`); + bankRef.current?.setAttribute('transform', `rotate(${-d.r} ${cx} ${cy})`); + fdRef.current?.setAttribute('transform', `translate(0 ${d.fp * PITCH_PX}) rotate(${d.fr} ${cx} ${cy})`); + raf = requestAnimationFrame(loop); + }; + raf = requestAnimationFrame(loop); + return () => cancelAnimationFrame(raf); + }, []); // eslint-disable-line return ( - + {/* full-screen attitude, exactly like the SVT box: blue/brown fills the + ENTIRE PFD below the radio bar (behind the tapes, HSI and data strip), + same region as the 3D terrain so both look identical full-screen. */} + - - + + {/* sky/ground only when SVT is off — otherwise the 3D terrain shows */} {!svt && } {!svt && } @@ -226,17 +249,19 @@ function Attitude({ V, svt }) { {pitchLadder(cx, cy)} - {/* flight director command bars (magenta) */} - - + {/* flight director command bars — magenta filled chevron (single cue) */} + + + - {rollArc(cx, cy, roll, slip)} - {/* fixed aircraft reference (yellow) */} - - - + {rollArc(cx, cy, slip, bankRef)} + {/* fixed aircraft reference — yellow chevron (single cue) + side wing markers */} + + + + + {/* flight path marker (green) — track/AOA based; offset approximated */} {(() => { @@ -250,7 +275,6 @@ function Attitude({ V, svt }) { ); })()} - {!svt && } ); } @@ -273,7 +297,7 @@ function pitchLadder(cx, cy) { return {m}; } -function rollArc(cx, cy, roll, slip) { +function rollArc(cx, cy, slip, bankRef) { const r = 165; const ticks = [-60, -45, -30, -20, -10, 0, 10, 20, 30, 45, 60]; return ( @@ -286,7 +310,7 @@ function rollArc(cx, cy, roll, slip) { x2={cx + r2 * Math.cos(a)} y2={cy + r2 * Math.sin(a)} stroke="#fff" strokeWidth={big ? 3 : 2} />; })} - + @@ -300,13 +324,14 @@ function rollArc(cx, cy, roll, slip) { const VSPEEDS = [{ s: 74, l: 'Y' }, { s: 62, l: 'X' }, { s: 68, l: 'G' }]; function AirspeedTape({ V }) { const ias = num(V.airspeed), tas = num(V.tas), spdBug = num(V.apSpdBug); - const x = 60, top = 95, h = 350, cy = top + h / 2, px = 3.6; + const x = 60, top = 110, h = 350, cy = top + h / 2, px = 3.6; const W2 = 84, sx = x + W2 - 7; // colour strip at the right inner edge const ticks = []; const lo = Math.floor((ias - 50) / 10) * 10; for (let s = lo; s <= ias + 50; s += 10) { if (s < 0) continue; const y = cy + (ias - s) * px; + if (y < top + 2 || y > top + h - 2) continue; // keep ticks inside the tape ticks.push( {s}); } @@ -316,7 +341,7 @@ function AirspeedTape({ V }) { const valid = ias >= 20; return ( - + {/* V-speed colour strip (white flap arc, green normal, yellow caution, red Vne) */} {band(33, 85, '#e8e8e8')} {band(48, 129, '#16c116')} @@ -329,18 +354,9 @@ function AirspeedTape({ V }) { {valid ? Math.round(ias) : '- - -'} - {/* V-speed reference list below the tape */} - {VSPEEDS.map((v, i) => ( - - {v.s} - - {v.l} - - ))} - {/* TAS box at the very bottom */} - - TAS - {Math.round(tas)} + {/* TAS readout directly below the tape, like the real G1000 */} + TAS + {Math.round(tas)}KT ); } @@ -348,11 +364,12 @@ function AirspeedTape({ V }) { /* ---------------- altitude tape + VSI + baro ---------------- */ function AltitudeTape({ V }) { const alt = num(V.altitude), vs = num(V.vspeed), altBug = num(V.apAltBug), baro = num(V.baro, 29.92); - const x = W - 70 - 84, W2 = 84, top = 95, h = 350, cy = top + h / 2, px = 0.42; + const x = W - 70 - 84, W2 = 84, top = 110, h = 350, cy = top + h / 2, px = 0.42; const ticks = []; const lo = Math.floor((alt - 420) / 100) * 100; for (let a = lo; a <= alt + 420; a += 100) { const y = cy + (alt - a) * px; + if (y < top + 2 || y > top + h - 2) continue; // keep ticks inside the tape ticks.push( {a}); } @@ -372,7 +389,7 @@ function AltitudeTape({ V }) { {/* selected altitude (cyan) above the tape */} {selStr} - + {ticks} {/* selected-altitude bug (cyan) on the tape */} @@ -574,7 +591,6 @@ function DataStrip({ V }) { const lcl = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`; return ( - {/* OAT + ISA (left) */} OAT {oatF}°F diff --git a/web/src/components/VFR.jsx b/web/src/components/VFR.jsx index 36bc079..8187aef 100644 --- a/web/src/components/VFR.jsx +++ b/web/src/components/VFR.jsx @@ -1,5 +1,6 @@ import React from 'react'; import { num } from '../api/useXplane.js'; +import KAP140 from './KAP140.jsx'; // Classic analog "six-pack" VFR panel: airspeed, attitude, altimeter, turn // coordinator, heading indicator, vertical speed — round steam gauges driven by @@ -276,10 +277,12 @@ function Clock({ V }) { ); } -export default function VFR({ values: V }) { +export default function VFR({ xp }) { + const V = xp.values; const fuelL = arr0(V.fuelQty, 0) / KG_GAL, fuelR = (Array.isArray(V.fuelQty) ? num(V.fuelQty[1]) : 0) / KG_GAL; - const oilF = arr0(V.oilTemp) * 9 / 5 + 32, oilP = arr0(V.oilPress); - const egtF = arr0(V.egt) * 9 / 5 + 32, ffGph = (arr0(V.fuelFlow) * 3600) / KG_GAL; + const oilT = arr0(V.oilTemp), egtT = arr0(V.egt); + const oilF = oilT > 150 ? oilT : oilT * 9 / 5 + 32, oilP = arr0(V.oilPress); + const egtF = egtT > 900 ? egtT : egtT * 9 / 5 + 32, ffGph = (arr0(V.fuelFlow) * 3600) / KG_GAL; const amps = arr0(V.amps); return (
@@ -290,13 +293,14 @@ export default function VFR({ values: V }) { +
-
+
diff --git a/web/src/styles.css b/web/src/styles.css index c71de41..eb136f7 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -100,6 +100,39 @@ body { .vfr-gauge svg, .vfr-sg svg { width: 100%; height: auto; filter: drop-shadow(0 4px 12px rgba(0,0,0,.55)); } .vfr-name, .vfr-sname { font-family: var(--ui-font); letter-spacing: 1.2px; color: #c9d0d7; font-weight: 600; } .vfr-name { font-size: 11px; } .vfr-sname { font-size: 9px; } +.vfr-ap { display: flex; justify-content: center; margin-top: clamp(8px, 1.5vw, 18px); } +/* KAP 140 autopilot (steam-gauge C172) */ +.kap140 { display: flex; align-items: center; gap: 12px; background: linear-gradient(#2a2c30, #161719); + border: 1px solid #0a0a0a; border-top: 1px solid #4a4d52; border-radius: 10px; padding: 11px 14px; font-family: var(--ui-font); + box-shadow: 0 8px 24px rgba(0,0,0,.6), inset 0 1px 0 rgba(255,255,255,.06); } +.kap-brand { color: #8893a0; font-size: 9px; font-weight: 700; letter-spacing: 1px; writing-mode: vertical-rl; transform: rotate(180deg); } +.kap-lcd { background: #06160b; border: 1px solid #0a4d24; border-radius: 4px; padding: 7px 11px; min-width: 168px; + box-shadow: inset 0 0 16px rgba(0,90,35,.5); font-family: 'Saira Semi Condensed', monospace; } +.kap-l1 { display: flex; gap: 12px; align-items: center; } +.kap-l1 .an { color: #0c3a1e; font-weight: 700; font-size: 14px; letter-spacing: 1px; } +.kap-l1 .an.on { color: #3bff6e; text-shadow: 0 0 8px rgba(59,255,110,.5); } +.kap-l2 { display: flex; gap: 14px; align-items: baseline; margin-top: 3px; color: #3bff6e; } +.kap-l2 .big { font-size: 22px; font-weight: 700; } +.kap-l2 .u { font-size: 10px; color: #1f9d52; margin-left: 2px; } +.kap-l2 .vs { font-size: 14px; } +.kap-keys { display: flex; gap: 7px; align-items: stretch; } +/* physical, G1000-style buttons */ +.kap-btn { background: linear-gradient(#3b3e44, #23262b); color: #eef2f6; border: 1px solid #08090b; border-top: 1px solid #5c6168; + border-radius: 7px; padding: 13px 11px; font-family: var(--ui-font); font-size: 12px; font-weight: 700; letter-spacing: .3px; cursor: pointer; min-width: 44px; + box-shadow: 0 2px 4px rgba(0,0,0,.55), inset 0 1px 0 rgba(255,255,255,.10); } +.kap-btn:hover { background: linear-gradient(#454951, #2a2d33); } +.kap-btn:active { transform: translateY(1px); background: linear-gradient(#1a8f44, #136b32); color: #fff; box-shadow: inset 0 2px 5px rgba(0,0,0,.6); } +.kap-btn.sm { padding: 6px 9px; font-size: 10px; min-width: 0; } +.kap-updn { display: flex; flex-direction: column; gap: 4px; } +/* clickable rotary: top half = up, bottom half = down */ +.kap-knob { display: flex; flex-direction: column; align-items: center; gap: 3px; } +.kap-dial { position: relative; width: 50px; height: 50px; border-radius: 50%; cursor: pointer; + background: radial-gradient(circle at 38% 30%, #4e535b, #14161a 72%); border: 1px solid #08090b; + box-shadow: 0 2px 6px rgba(0,0,0,.6), inset 0 1px 0 rgba(255,255,255,.14); + display: flex; flex-direction: column; align-items: center; justify-content: space-between; padding: 3px 0; } +.kap-dial .kdir { color: #aeb6bf; font-size: 11px; line-height: 1; user-select: none; } +.kap-dial:hover .kdir { color: #fff; } +.kap-knoblbl { color: #8893a0; font-size: 9px; font-weight: 700; letter-spacing: 1px; } .vfr-clock { background: #0c0d0f; border: 1px solid #2a2f36; border-radius: 6px; padding: 8px 10px; display: flex; flex-direction: column; gap: 4px; } .vc-row { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; } .vc-row b { font-family: 'Saira Condensed', monospace; color: #46e0c0; font-size: 18px; } @@ -233,16 +266,15 @@ body { .bezel-core { flex: 1; display: flex; flex-direction: column; min-width: 0; } .bezel-title { text-align: center; color: #c9ced3; font-size: 14px; font-weight: 700; letter-spacing: 3px; padding: 2px 0 6px; } .bezel-screen { - flex: 1; background: #000; border-radius: 6px; overflow: hidden; position: relative; - border: 2px solid #0a0a0a; box-shadow: inset 0 0 18px #000, inset 0 0 2px #1a3a5a; + flex: 1; background: #000; overflow: hidden; position: relative; display: flex; min-height: 0; } .bezel-screen > * { width: 100%; height: 100%; } -.softkeys { display: grid; grid-template-columns: repeat(12, 1fr); gap: 6px; padding: 8px 2px 2px; } +.softkeys { display: grid; grid-template-columns: repeat(12, 1fr); gap: 4px; padding: 4px 2px 1px; } .softkey { - height: 30px; display: flex; align-items: center; justify-content: center; + height: 20px; display: flex; align-items: center; justify-content: center; background: linear-gradient(#202224, #131416); border: 1px solid #000; border-top: 1px solid #45474b; - border-radius: 4px; color: #cfd6dc; font-size: 12px; font-weight: 600; letter-spacing: .3px; + border-radius: 3px; color: #cfd6dc; font-size: 10.5px; font-weight: 600; letter-spacing: .2px; box-shadow: 0 1px 2px #000; cursor: pointer; font-family: inherit; } .softkey:not(.empty):hover { background: linear-gradient(#2a2c2f, #1a1b1e); border-top-color: #5a5d61; } @@ -258,10 +290,10 @@ body { .knob-sub { color: #8b9197; font-size: 8.5px; font-weight: 600; letter-spacing: .3px; text-align: center; } .knob-extra { position: absolute; right: -10px; top: 6px; width: 20px; height: 16px; background: #1a1b1e; border: 1px solid #000; border-radius: 3px; color: #cfd6dc; font-size: 11px; text-align: center; line-height: 16px; } .knob.outer { - width: 50px; height: 50px; border-radius: 50%; display: flex; align-items: center; justify-content: center; position: relative; + width: 60px; height: 60px; border-radius: 50%; display: flex; align-items: center; justify-content: center; position: relative; background: radial-gradient(circle at 35% 30%, #55585d, #2a2c2f 70%); box-shadow: 0 2px 5px #000, inset 0 1px 0 #6a6d72; } -.knob-wrap.big .knob.outer { width: 58px; height: 58px; } +.knob-wrap.big .knob.outer { width: 68px; height: 68px; } .knob.inner { width: 26px; height: 26px; border-radius: 50%; background: radial-gradient(circle at 35% 30%, #44474b, #1c1e20); box-shadow: inset 0 1px 0 #5a5d61; } .knob.joy .joy-cross { position: absolute; color: #6a6d72; font-size: 22px; font-weight: 700; pointer-events: none; } .knob.outer { cursor: pointer; border: none; padding: 0; } @@ -278,6 +310,12 @@ body { .knob-arrow:active { background: #000; } .knob-arrow.left { left: -2px; } .knob-arrow.right { right: -2px; } .knob-arrow.top { top: -2px; } .knob-arrow.bottom { bottom: -2px; } +.knob-cluster.zones { padding: 5px; } +/* settings panel */ +.set-lbl { color: var(--c-mut); font-size: 12px; font-weight: 700; letter-spacing: .5px; margin-bottom: 8px; font-family: var(--ui-font); } +.set-opt { display: flex; gap: 8px; } +.set-opt .fbtn { flex: 1; } +.set-hint { color: var(--c-mut); font-size: 11px; margin-top: 10px; line-height: 1.45; font-family: var(--ui-font); } .pan-pad { display: grid; grid-template-columns: repeat(2, 14px); gap: 2px; margin-top: 3px; } .pan-pad button {