signalk-pypilot-newui 2.0.2 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -39
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/public/app.js +43 -147
- package/public/index.html +30 -62
- package/public/style.css +1 -43
package/CHANGELOG.md
CHANGED
|
@@ -1,44 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 2.0.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
real delta arrived. Now the heading is captured ONCE at the click and
|
|
15
|
-
reused for the POST, the optimistic assign and any later reference in
|
|
16
|
-
the handler.
|
|
17
|
-
|
|
18
|
-
### Changed - compass rose
|
|
19
|
-
|
|
20
|
-
- HDG now shown in a small dark chip that straddles the top of the ring
|
|
21
|
-
(asoma un poco por encima) instead of floating text. Font shrunk from
|
|
22
|
-
18 to 14 to fit inside the chip.
|
|
23
|
-
- Wind arrows redrawn: both solid (no more dashed shaft on TWA), amber
|
|
24
|
-
AWA keeps a big head, sea-green (#5ce8c0) TWA gets a smaller head, and
|
|
25
|
-
each carries its own `AWA` / `TWA` text label rotating with the arrow.
|
|
26
|
-
The magenta is gone.
|
|
27
|
-
- Bottom of rose: `AWS` / `TWS` label small on top and the number large
|
|
28
|
-
below, split into two lines with the S cardinal reubicated so the
|
|
29
|
-
gap between them stays visible for the rose itself.
|
|
30
|
-
- Short tap on a corner mapped to "wind" now flips the label between
|
|
31
|
-
AWA/AWS and TWA/TWS and pins that choice. When the AP mode changes to
|
|
32
|
-
a true-wind mode the label auto-follows to TWS (unless the user has
|
|
33
|
-
already pinned it manually).
|
|
34
|
-
|
|
35
|
-
### Added - rudder angle indicator in the swipe-hint bar
|
|
36
|
-
|
|
37
|
-
The bar under the Control tab now carries a horizontal red-to-green
|
|
38
|
-
gradient (port → stbd) with a moving white marker showing the current
|
|
39
|
-
rudder angle. The central yellow nav-toggle triangle sits at 0 deg,
|
|
40
|
-
doubling as the amidships reference. Scale comes from the pypilot
|
|
41
|
-
catalog's `rudder.range` (default 45 deg when unavailable).
|
|
3
|
+
## 2.0.3 — 2026-08-20 — Rev66 - rollback of 2.0.2
|
|
4
|
+
|
|
5
|
+
Codebase is functionally identical to 2.0.1. Version 2.0.2 introduced
|
|
6
|
+
regressions in the socket auto-reconnect and the new "escalated restart"
|
|
7
|
+
triad that could leave the plugin showing `connected: false` against a
|
|
8
|
+
pypilot_web that is in fact alive and serving other consumers. Rolling
|
|
9
|
+
back to the 2.0.1 behaviour and reintroducing the 2.0.2 UI polish
|
|
10
|
+
(target diamond snapshot, redesigned AWA/TWA arrows, HDG chip, wind
|
|
11
|
+
speed chip, tap wind toggle, rudder bar, debug console, escalated
|
|
12
|
+
restarts) in a later 2.0.4+ release, one item at a time with proper QA
|
|
13
|
+
before publish. 2.0.2 has been deprecated on npm.
|
|
42
14
|
|
|
43
15
|
## 2.0.1 — 2026-08-20 — Rev64
|
|
44
16
|
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ const scanner_1 = require("./scanner");
|
|
|
6
6
|
const autopilot_provider_1 = require("./autopilot-provider");
|
|
7
7
|
// Rev counter bumped on every build so the user can distinguish deploys
|
|
8
8
|
// from the webapp header (feedback_revision_bump_each_build).
|
|
9
|
-
const PLUGIN_REVISION = "
|
|
9
|
+
const PLUGIN_REVISION = "Rev66";
|
|
10
10
|
// Rev59: read package.json once at load time so /status can report the
|
|
11
11
|
// npm package version alongside the internal Rev counter.
|
|
12
12
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "signalk-pypilot-newui",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.3",
|
|
4
4
|
"description": "Signal K friendly UI for the pypilot autopilot, with the capability to spread its functions and buttons to other programs like KIP, WilhelmSK and freeboard. Touch-first mobile visor + full pypilot data (servo, gains, calibration, tack, warnings) as first-class Signal K paths and Simple Switches. Coexists with pypilot-autopilot-provider.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
package/public/app.js
CHANGED
|
@@ -534,20 +534,6 @@
|
|
|
534
534
|
windAngleTrue: null,
|
|
535
535
|
windSpeedTrue: null,
|
|
536
536
|
sog: null, // m/s
|
|
537
|
-
// Rev65 / 2.0.2 - wind-corner display preference. "awa" or "twa".
|
|
538
|
-
// Initial value derives from the browser-stored preference; if
|
|
539
|
-
// absent, defaults to "awa" and later auto-follows the AP mode
|
|
540
|
-
// (see _syncWindCornerFromMode).
|
|
541
|
-
windCornerShow: (() => {
|
|
542
|
-
try { return localStorage.getItem("pypilot-newui.windCornerShow") || "awa"; }
|
|
543
|
-
catch { return "awa"; }
|
|
544
|
-
})(),
|
|
545
|
-
windCornerUserPinned: (() => {
|
|
546
|
-
// User's manual tap "pins" the choice so mode changes stop
|
|
547
|
-
// overwriting it. Persistent per browser.
|
|
548
|
-
try { return localStorage.getItem("pypilot-newui.windCornerPinned") === "1"; }
|
|
549
|
-
catch { return false; }
|
|
550
|
-
})(),
|
|
551
537
|
};
|
|
552
538
|
|
|
553
539
|
const RAD2DEG = 180 / Math.PI;
|
|
@@ -686,7 +672,7 @@
|
|
|
686
672
|
function applyValue(path, value) {
|
|
687
673
|
switch (path) {
|
|
688
674
|
case "steering.autopilot.state": state.apState = value; break;
|
|
689
|
-
case "steering.autopilot.mode": state.mode = value; setSelect("#mode-select", value);
|
|
675
|
+
case "steering.autopilot.mode": state.mode = value; setSelect("#mode-select", value); break;
|
|
690
676
|
case "steering.autopilot.target": state.target = numericOrNull(value); renderTargetArrow(); break;
|
|
691
677
|
case "steering.autopilot.engaged": state.engaged = !!value; renderEngage(); renderTargetArrow(); break;
|
|
692
678
|
case "steering.autopilot.availableActions": state.availableActions = value || []; break;
|
|
@@ -794,12 +780,9 @@
|
|
|
794
780
|
// getComputedTextLength() only works on rendered SVG text nodes.
|
|
795
781
|
// Fall back to a heuristic width if the SVG has not laid out yet
|
|
796
782
|
// (e.g. tab is not visible).
|
|
797
|
-
// Fallback width when SVG has not laid out yet (tab not visible).
|
|
798
|
-
// Rev65: HDG font shrunk from 18 to 14 for the new chip, so the
|
|
799
|
-
// per-char heuristic drops from 12 to 9 units.
|
|
800
783
|
let w;
|
|
801
|
-
try { w = hdgTxt.getComputedTextLength(); } catch { w = s.length *
|
|
802
|
-
if (!isFinite(w) || w <= 0) w = s.length *
|
|
784
|
+
try { w = hdgTxt.getComputedTextLength(); } catch { w = s.length * 12; }
|
|
785
|
+
if (!isFinite(w) || w <= 0) w = s.length * 12;
|
|
803
786
|
hdgDeg.setAttribute("x", (w / 2 + 1).toFixed(1));
|
|
804
787
|
}
|
|
805
788
|
}
|
|
@@ -821,15 +804,9 @@
|
|
|
821
804
|
twa: { label: "TWA", read: () => state.windAngleTrue == null ? "---" : _sgn(state.windAngleTrue * RAD2DEG) + "°" },
|
|
822
805
|
tws: { label: "TWS", read: () => state.windSpeedTrue == null ? "---" : (state.windSpeedTrue * 1.94384).toFixed(1) + " kn" },
|
|
823
806
|
twd: { label: "TWD", read: () => state.windDirTrue == null ? "---" : Math.round(state.windDirTrue * RAD2DEG) + "°" },
|
|
824
|
-
|
|
825
|
-
// preference (state.windCornerShow: "awa" or "twa"), overridable by
|
|
826
|
-
// a short tap on the corner. When the AP mode changes TO true-wind,
|
|
827
|
-
// the preference auto-flips to TWA/TWS; when it changes AWAY from
|
|
828
|
-
// true-wind, it flips back to AWA/AWS. Manual taps after that stick
|
|
829
|
-
// until the mode flips again (see _syncWindCornerFromMode).
|
|
830
|
-
wind: { label: () => (state.windCornerShow === "twa" ? "TWA/TWS" : "AWA/AWS"),
|
|
807
|
+
wind: { label: () => (String(state.mode || "").includes("true") ? "TWA/TWS" : "AWA/AWS"),
|
|
831
808
|
read: () => {
|
|
832
|
-
const useTrue = state.
|
|
809
|
+
const useTrue = String(state.mode || "").includes("true");
|
|
833
810
|
const ang = useTrue ? state.windAngleTrue : state.windAngle;
|
|
834
811
|
const spd = useTrue ? state.windSpeedTrue : state.windSpeed;
|
|
835
812
|
const a = ang == null ? "---" : _sgn(ang * RAD2DEG) + "°";
|
|
@@ -908,32 +885,6 @@
|
|
|
908
885
|
});
|
|
909
886
|
}
|
|
910
887
|
_refreshCustomDashEntries();
|
|
911
|
-
// Rev65 / 2.0.2: short tap on the "wind" corner flips the preference
|
|
912
|
-
// between AWA/AWS and TWA/TWS, and pins it so mode auto-changes stop
|
|
913
|
-
// overwriting it until the user opts back into auto-follow.
|
|
914
|
-
function _toggleWindCornerShow() {
|
|
915
|
-
state.windCornerShow = (state.windCornerShow === "twa") ? "awa" : "twa";
|
|
916
|
-
state.windCornerUserPinned = true;
|
|
917
|
-
try {
|
|
918
|
-
localStorage.setItem("pypilot-newui.windCornerShow", state.windCornerShow);
|
|
919
|
-
localStorage.setItem("pypilot-newui.windCornerPinned", "1");
|
|
920
|
-
} catch {}
|
|
921
|
-
_dashRenderCorners();
|
|
922
|
-
renderWindRose();
|
|
923
|
-
}
|
|
924
|
-
// Called from applyValue whenever the AP mode changes. If the user has
|
|
925
|
-
// not pinned the wind-corner preference, follow the mode: true-wind
|
|
926
|
-
// modes show TWA/TWS, everything else shows AWA/AWS.
|
|
927
|
-
function _syncWindCornerFromMode(mode) {
|
|
928
|
-
if (state.windCornerUserPinned) return;
|
|
929
|
-
const wantTrue = String(mode || "").toLowerCase().includes("true");
|
|
930
|
-
const next = wantTrue ? "twa" : "awa";
|
|
931
|
-
if (state.windCornerShow === next) return;
|
|
932
|
-
state.windCornerShow = next;
|
|
933
|
-
try { localStorage.setItem("pypilot-newui.windCornerShow", next); } catch {}
|
|
934
|
-
_dashRenderCorners();
|
|
935
|
-
renderWindRose();
|
|
936
|
-
}
|
|
937
888
|
function _sgn(n) { return (n > 0 ? "+" : "") + Math.round(n); }
|
|
938
889
|
function _dashLoadCfg() {
|
|
939
890
|
try {
|
|
@@ -1111,10 +1062,9 @@
|
|
|
1111
1062
|
}
|
|
1112
1063
|
|
|
1113
1064
|
// SVG compass rose. The card (cardinals + ticks) rotates so N points to
|
|
1114
|
-
// magnetic north regardless of boat heading; the boat is fixed pointing
|
|
1115
|
-
//
|
|
1116
|
-
//
|
|
1117
|
-
// the bottom AWS/TWS readout follows the user's wind-corner preference.
|
|
1065
|
+
// magnetic north regardless of boat heading; the boat is fixed pointing up;
|
|
1066
|
+
// TWO independent wind arrows (AWA amber solid, TWA magenta dashed) rotate
|
|
1067
|
+
// to their respective angles; wedges are boat-fixed helm guides.
|
|
1118
1068
|
function renderWindRose() {
|
|
1119
1069
|
const card = document.getElementById("rose-card");
|
|
1120
1070
|
if (card && state.heading != null) {
|
|
@@ -1140,29 +1090,22 @@
|
|
|
1140
1090
|
}
|
|
1141
1091
|
}
|
|
1142
1092
|
|
|
1143
|
-
//
|
|
1144
|
-
//
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
const altSpd = useTrue ? state.windSpeed : state.windSpeedTrue;
|
|
1156
|
-
const altTag = useTrue ? "AWS" : "TWS";
|
|
1157
|
-
if (altSpd != null) { windSpeed = altSpd; tag = altTag; }
|
|
1158
|
-
}
|
|
1159
|
-
const speedLabel = document.getElementById("rose-speed-label");
|
|
1160
|
-
const speedText = document.getElementById("rose-speed");
|
|
1161
|
-
if (speedLabel) speedLabel.textContent = windSpeed == null ? "" : tag;
|
|
1093
|
+
// Speed readout at the bottom: prefer the source that is enabled; if
|
|
1094
|
+
// both are enabled, show apparent (the one the crew feels).
|
|
1095
|
+
const showAws = _windArrowShow.awa && state.windSpeed != null;
|
|
1096
|
+
const showTws = _windArrowShow.twa && state.windSpeedTrue != null;
|
|
1097
|
+
let windSpeed = null, tag = "";
|
|
1098
|
+
if (showAws) { windSpeed = state.windSpeed; tag = "AWS"; }
|
|
1099
|
+
else if (showTws) { windSpeed = state.windSpeedTrue; tag = "TWS"; }
|
|
1100
|
+
|
|
1101
|
+
// Rev56: sailing zones (green stbd / red port) are DRAWN STATIC in
|
|
1102
|
+
// the SVG at fixed boat-centerline angles +/-30..60, so nothing to
|
|
1103
|
+
// update here.
|
|
1104
|
+
const speedText = document.getElementById("rose-speed");
|
|
1162
1105
|
if (speedText) {
|
|
1163
1106
|
speedText.textContent = windSpeed == null
|
|
1164
1107
|
? "--- kn"
|
|
1165
|
-
: (windSpeed * 1.94384).toFixed(1) + " kn";
|
|
1108
|
+
: (windSpeed * 1.94384).toFixed(1) + " kn" + (tag ? " " + tag : "");
|
|
1166
1109
|
}
|
|
1167
1110
|
}
|
|
1168
1111
|
|
|
@@ -1257,36 +1200,14 @@
|
|
|
1257
1200
|
}
|
|
1258
1201
|
|
|
1259
1202
|
function renderRudder(rad) {
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
if (rad == null) {
|
|
1263
|
-
if (w) w.setAttribute("hidden", "");
|
|
1264
|
-
// Also center the swipe-hint rudder marker so it looks amidships
|
|
1265
|
-
// when data is missing rather than sticking to some old value.
|
|
1266
|
-
const m = $("#rudder-marker");
|
|
1267
|
-
if (m) m.style.left = "50%";
|
|
1268
|
-
return;
|
|
1269
|
-
}
|
|
1270
|
-
if (w) w.removeAttribute("hidden");
|
|
1203
|
+
if (rad == null) { $("#rudder-widget").setAttribute("hidden", ""); return; }
|
|
1204
|
+
$("#rudder-widget").removeAttribute("hidden");
|
|
1271
1205
|
const deg = rad * RAD2DEG;
|
|
1272
|
-
const
|
|
1206
|
+
const pct = Math.max(-1, Math.min(1, deg / 45));
|
|
1273
1207
|
const fill = $("#rudder-fill");
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
}
|
|
1278
|
-
const val = $("#rudder-value");
|
|
1279
|
-
if (val) val.textContent = deg.toFixed(1) + " deg";
|
|
1280
|
-
// Rev65 / 2.0.2: paint the rudder marker on the swipe-hint bar. Range
|
|
1281
|
-
// comes from the pypilot catalog (`rudder.range` in degrees) when
|
|
1282
|
-
// available, else defaults to 45 deg either side.
|
|
1283
|
-
const marker = $("#rudder-marker");
|
|
1284
|
-
if (marker) {
|
|
1285
|
-
const rangeDeg = Number(state.values?.["steering.autopilot.pypilot.rudder.range"]) || 45;
|
|
1286
|
-
const pct = Math.max(-1, Math.min(1, deg / rangeDeg));
|
|
1287
|
-
// pct = -1 -> left edge (0%), 0 -> center (50%), +1 -> right edge (100%).
|
|
1288
|
-
marker.style.left = (50 + pct * 50).toFixed(1) + "%";
|
|
1289
|
-
}
|
|
1208
|
+
fill.style.width = Math.abs(pct * 50) + "%";
|
|
1209
|
+
fill.style.transform = pct < 0 ? "translateX(-100%)" : "translateX(0%)";
|
|
1210
|
+
$("#rudder-value").textContent = deg.toFixed(1) + " deg";
|
|
1290
1211
|
}
|
|
1291
1212
|
|
|
1292
1213
|
// Rev36 helper: build a slider row with nudge buttons -10/-1/+1/+10.
|
|
@@ -1746,16 +1667,6 @@
|
|
|
1746
1667
|
_apPending = true;
|
|
1747
1668
|
eng.classList.add("pending");
|
|
1748
1669
|
const wasEngaged = state.engaged;
|
|
1749
|
-
// Rev65 / 2.0.2: snapshot the heading the USER IS SEEING at the exact
|
|
1750
|
-
// moment of the click, and use that same value for (a) the target we
|
|
1751
|
-
// send to the server, (b) the optimistic local assign, (c) any later
|
|
1752
|
-
// reference in this handler. Previously we re-read state.heading on
|
|
1753
|
-
// each await point - on a laggy Tailscale link (200-800 ms RTT) the
|
|
1754
|
-
// heading could have drifted a few degrees by the time the POST
|
|
1755
|
-
// executed, so we sent one value to the server, assigned another
|
|
1756
|
-
// locally, and the diamond jumped to a third when the real delta
|
|
1757
|
-
// arrived. Carlos's report: "el diamante no se fija bien".
|
|
1758
|
-
const capturedHeading = state.heading;
|
|
1759
1670
|
// Optimistic: flip the visual state before waiting on the network.
|
|
1760
1671
|
state.engaged = !wasEngaged;
|
|
1761
1672
|
renderEngage();
|
|
@@ -1769,12 +1680,12 @@
|
|
|
1769
1680
|
// does not serialise 2 round-trips before the AP actually engages.
|
|
1770
1681
|
const [rEng, rTgt] = await Promise.all([
|
|
1771
1682
|
_apRetry(apEngage),
|
|
1772
|
-
|
|
1683
|
+
state.heading != null ? _apRetry(() => apSetTargetRad(state.heading)) : Promise.resolve({ ok: true }),
|
|
1773
1684
|
]);
|
|
1774
1685
|
ok = !!(rEng && rEng.ok);
|
|
1775
|
-
if (rTgt && rTgt.ok &&
|
|
1776
|
-
state.target =
|
|
1777
|
-
state.localTargetRad =
|
|
1686
|
+
if (rTgt && rTgt.ok && state.heading != null) {
|
|
1687
|
+
state.target = state.heading;
|
|
1688
|
+
state.localTargetRad = state.heading;
|
|
1778
1689
|
renderControl();
|
|
1779
1690
|
}
|
|
1780
1691
|
}
|
|
@@ -2576,50 +2487,35 @@
|
|
|
2576
2487
|
_activateTab(ids[nextIdx], dir === 1 ? "next" : "prev");
|
|
2577
2488
|
});
|
|
2578
2489
|
});
|
|
2579
|
-
// Rev48
|
|
2580
|
-
//
|
|
2581
|
-
// AWA/AWS and TWA/TWS, pinning the user's choice so subsequent AP
|
|
2582
|
-
// mode changes stop auto-flipping it (see _syncWindCornerFromMode).
|
|
2490
|
+
// Rev48: long-press on any of the 4 corner cells opens the selector
|
|
2491
|
+
// popup so the user assigns whatever data they want per corner.
|
|
2583
2492
|
_dashLoadCfg();
|
|
2584
2493
|
_dashPopulateSelectors();
|
|
2585
2494
|
_dashRenderCorners();
|
|
2586
2495
|
let _dashLongPressTimer = null;
|
|
2587
|
-
let _dashPressStartedAt = 0;
|
|
2588
|
-
let _dashPressWasLong = false;
|
|
2589
2496
|
$$(".dash-rose .dash-cell[data-corner]").forEach((cell) => {
|
|
2590
2497
|
const openSelector = (e) => {
|
|
2591
2498
|
e && e.preventDefault();
|
|
2592
|
-
_dashPressWasLong = true;
|
|
2593
2499
|
_dashPopulateSelectors();
|
|
2594
2500
|
const pop = document.getElementById("dash-help-pop");
|
|
2595
2501
|
if (pop) pop.classList.add("open");
|
|
2596
2502
|
};
|
|
2597
|
-
|
|
2598
|
-
_dashPressStartedAt = Date.now();
|
|
2599
|
-
_dashPressWasLong = false;
|
|
2503
|
+
cell.addEventListener("touchstart", (e) => {
|
|
2600
2504
|
if (_dashLongPressTimer) clearTimeout(_dashLongPressTimer);
|
|
2601
2505
|
_dashLongPressTimer = setTimeout(() => openSelector(e), 500);
|
|
2602
|
-
};
|
|
2603
|
-
const
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
const elapsed = Date.now() - _dashPressStartedAt;
|
|
2608
|
-
if (elapsed >= 500) return;
|
|
2609
|
-
const corner = cell.dataset.corner;
|
|
2610
|
-
if (_dashCornerCfg[corner] === "wind") _toggleWindCornerShow();
|
|
2611
|
-
};
|
|
2612
|
-
cell.addEventListener("touchstart", startPress, { passive: true });
|
|
2613
|
-
cell.addEventListener("touchend", endPress, { passive: true });
|
|
2614
|
-
cell.addEventListener("touchmove", () => { if (_dashLongPressTimer) { clearTimeout(_dashLongPressTimer); _dashLongPressTimer = null; } _dashPressWasLong = true; }, { passive: true });
|
|
2615
|
-
cell.addEventListener("touchcancel",() => { if (_dashLongPressTimer) { clearTimeout(_dashLongPressTimer); _dashLongPressTimer = null; } }, { passive: true });
|
|
2506
|
+
}, { passive: true });
|
|
2507
|
+
const cancel = () => { if (_dashLongPressTimer) { clearTimeout(_dashLongPressTimer); _dashLongPressTimer = null; } };
|
|
2508
|
+
cell.addEventListener("touchend", cancel, { passive: true });
|
|
2509
|
+
cell.addEventListener("touchmove", cancel, { passive: true });
|
|
2510
|
+
cell.addEventListener("touchcancel", cancel, { passive: true });
|
|
2616
2511
|
cell.addEventListener("contextmenu", openSelector);
|
|
2512
|
+
let _mouseTimer = null;
|
|
2617
2513
|
cell.addEventListener("mousedown", (e) => {
|
|
2618
2514
|
if (e.button !== 0) return;
|
|
2619
|
-
|
|
2515
|
+
_mouseTimer = setTimeout(() => openSelector(e), 600);
|
|
2620
2516
|
});
|
|
2621
|
-
cell.addEventListener("mouseup",
|
|
2622
|
-
cell.addEventListener("mouseleave", () => { if (
|
|
2517
|
+
cell.addEventListener("mouseup", () => { if (_mouseTimer) { clearTimeout(_mouseTimer); _mouseTimer = null; } });
|
|
2518
|
+
cell.addEventListener("mouseleave", () => { if (_mouseTimer) { clearTimeout(_mouseTimer); _mouseTimer = null; } });
|
|
2623
2519
|
});
|
|
2624
2520
|
// Rev43: horizontal swipe between tabs on mobile/tablet.
|
|
2625
2521
|
// Rules:
|
package/public/index.html
CHANGED
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
<div class="dash-cell corner-tr" data-corner="tr"></div>
|
|
72
72
|
<div class="dash-cell corner-bl" data-corner="bl"></div>
|
|
73
73
|
<div class="dash-cell corner-br" data-corner="br"></div>
|
|
74
|
-
<svg id="wind-rose" class="rose-svg" viewBox="-100 -100 200
|
|
74
|
+
<svg id="wind-rose" class="rose-svg" viewBox="-100 -100 200 200" preserveAspectRatio="xMidYMid meet">
|
|
75
75
|
<!-- Outer compass ring -->
|
|
76
76
|
<circle cx="0" cy="0" r="94" fill="none" stroke="#2b3542" stroke-width="2"/>
|
|
77
77
|
<!-- Rev58: sailing zones as PIE SLICES from origin (r=0) out
|
|
@@ -106,13 +106,10 @@
|
|
|
106
106
|
<line x1="0" y1="-94" x2="0" y2="-84" transform="rotate(330)"/>
|
|
107
107
|
</g>
|
|
108
108
|
<!-- 4 cardinals: N accent yellow, ESW dim white. -->
|
|
109
|
-
<text x="0"
|
|
110
|
-
<text x="70" y="0"
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
card sits at heading 0. -->
|
|
114
|
-
<text x="0" y="58" text-anchor="middle" dominant-baseline="middle" fill="#e5eaf3" font-size="13" font-weight="800">S</text>
|
|
115
|
-
<text x="-70" y="0" text-anchor="middle" dominant-baseline="middle" fill="#e5eaf3" font-size="13" font-weight="800">W</text>
|
|
109
|
+
<text x="0" y="-70" text-anchor="middle" dominant-baseline="middle" fill="#ffd54a" font-size="14" font-weight="900">N</text>
|
|
110
|
+
<text x="70" y="0" text-anchor="middle" dominant-baseline="middle" fill="#e5eaf3" font-size="13" font-weight="800">E</text>
|
|
111
|
+
<text x="0" y="70" text-anchor="middle" dominant-baseline="middle" fill="#e5eaf3" font-size="13" font-weight="800">S</text>
|
|
112
|
+
<text x="-70" y="0" text-anchor="middle" dominant-baseline="middle" fill="#e5eaf3" font-size="13" font-weight="800">W</text>
|
|
116
113
|
<!-- 8 numeric labels at 30 deg increments (skip cardinals). -->
|
|
117
114
|
<g fill="#8b98a5" font-size="7" font-weight="700" font-family="ui-monospace, monospace" text-anchor="middle" dominant-baseline="middle">
|
|
118
115
|
<text transform="rotate(30) translate(0,-72)">030</text>
|
|
@@ -125,46 +122,30 @@
|
|
|
125
122
|
<text transform="rotate(330) translate(0,-72)">330</text>
|
|
126
123
|
</g>
|
|
127
124
|
</g>
|
|
128
|
-
<!--
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
the digits only; degree glyph placed by JS after the
|
|
136
|
-
digits so "9" and "298" both anchor at x=0. -->
|
|
137
|
-
<g id="rose-hdg-chip">
|
|
138
|
-
<rect x="-24" y="-100" width="48" height="18" rx="4" ry="4"
|
|
139
|
-
fill="rgba(0,0,0,0.72)" stroke="#4a5867" stroke-width="1"/>
|
|
140
|
-
<text id="rose-hdg-value" x="0" y="-90" text-anchor="middle" dominant-baseline="middle" fill="#ffffff" font-size="14" font-weight="900" font-family="ui-monospace, monospace">---</text>
|
|
141
|
-
<text id="rose-hdg-deg" x="0" y="-90" text-anchor="start" dominant-baseline="middle" fill="#ffffff" font-size="10" font-weight="700">°</text>
|
|
142
|
-
</g>
|
|
125
|
+
<!-- Rev58: HDG pushed to y=-82 (right against the top rim);
|
|
126
|
+
wind speed dropped to y=+82 (bottom rim) to keep the
|
|
127
|
+
whole center free for the boat + arrows. Number CENTERED
|
|
128
|
+
on the digits only; the degree symbol is positioned by
|
|
129
|
+
JS just after them so "9" and "298" both anchor at x=0. -->
|
|
130
|
+
<text id="rose-hdg-value" x="0" y="-82" text-anchor="middle" dominant-baseline="middle" fill="#ffffff" font-size="18" font-weight="900" font-family="ui-monospace, monospace">---</text>
|
|
131
|
+
<text id="rose-hdg-deg" x="0" y="-82" text-anchor="start" dominant-baseline="middle" fill="#ffffff" font-size="13" font-weight="700">°</text>
|
|
143
132
|
<!-- Rev50: boat overhead SVG (mareas-ihm), scaled 0.22 so
|
|
144
133
|
the hull spans ~100 units = 50 % of the 200-unit rose
|
|
145
134
|
diameter. Rotated -90 so bow points up. -->
|
|
146
135
|
<g id="rose-boat" transform="rotate(-90) scale(0.22)">
|
|
147
136
|
<image href="boat-cenital.svg" x="-228" y="-76" width="456" height="152"/>
|
|
148
137
|
</g>
|
|
149
|
-
<!--
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
TWA = sea-green (informational), smaller head. Both
|
|
154
|
-
toggled independently from the corner popup. -->
|
|
138
|
+
<!-- Rev58: two independent wind arrows so the crew can watch
|
|
139
|
+
apparent AND true at the same time. AWA in amber (crew
|
|
140
|
+
default), TWA in magenta with a dashed shaft. Each is
|
|
141
|
+
toggled independently from the corner popup (localStorage). -->
|
|
155
142
|
<g id="rose-wind-arrow" style="display:none">
|
|
156
|
-
<line x1="0" y1="-86" x2="0" y2="-
|
|
157
|
-
<polygon points="0,-
|
|
158
|
-
<text x="0" y="-58" text-anchor="middle" dominant-baseline="middle"
|
|
159
|
-
fill="#ffd54a" stroke="#3a2600" stroke-width="0.4" paint-order="stroke fill"
|
|
160
|
-
font-size="8" font-weight="900" letter-spacing="1">AWA</text>
|
|
143
|
+
<line x1="0" y1="-86" x2="0" y2="-36" stroke="#ffd54a" stroke-width="3" stroke-linecap="round"/>
|
|
144
|
+
<polygon points="0,-30 -7,-42 7,-42" fill="#ffd54a" stroke="#8a6a00" stroke-width="0.6"/>
|
|
161
145
|
</g>
|
|
162
146
|
<g id="rose-wind-arrow-twa" style="display:none">
|
|
163
|
-
<line x1="0" y1="-86" x2="0" y2="-
|
|
164
|
-
<polygon points="0,-
|
|
165
|
-
<text x="0" y="-62" text-anchor="middle" dominant-baseline="middle"
|
|
166
|
-
fill="#5ce8c0" stroke="#0f2620" stroke-width="0.4" paint-order="stroke fill"
|
|
167
|
-
font-size="7" font-weight="900" letter-spacing="1">TWA</text>
|
|
147
|
+
<line x1="0" y1="-86" x2="0" y2="-36" stroke="#c780f5" stroke-width="2.5" stroke-linecap="round" stroke-dasharray="4 3"/>
|
|
148
|
+
<polygon points="0,-30 -6,-40 6,-40" fill="#c780f5" stroke="#3f1d5c" stroke-width="0.6"/>
|
|
168
149
|
</g>
|
|
169
150
|
<!-- Rev57: target indicator - CYAN diamond just outside the
|
|
170
151
|
compass ring, only shown when AP is engaged. Rotated to
|
|
@@ -173,17 +154,10 @@
|
|
|
173
154
|
<g id="rose-target-arrow" style="display:none">
|
|
174
155
|
<polygon points="0,-104 -7,-96 0,-88 7,-96" fill="#4dd4ff" stroke="#003b52" stroke-width="1.2"/>
|
|
175
156
|
</g>
|
|
176
|
-
<!--
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
stack sits below the compass ring: label just outside at
|
|
181
|
-
y=+100, number below at y=+108, both fitting inside the
|
|
182
|
-
expanded viewBox (extra 10 units at the bottom). -->
|
|
183
|
-
<text id="rose-speed-label" x="0" y="98" text-anchor="middle" dominant-baseline="middle"
|
|
184
|
-
fill="#8b98a5" font-size="8" font-weight="700" letter-spacing="1.5"></text>
|
|
185
|
-
<text id="rose-speed" x="0" y="108" text-anchor="middle" dominant-baseline="middle"
|
|
186
|
-
fill="#ffffff" font-size="15" font-weight="900" font-family="ui-monospace, monospace">--- kn</text>
|
|
157
|
+
<!-- Rev58: wind speed dropped to y=+82 (bottom rim) to keep
|
|
158
|
+
the boat area clean. Sub-label picks the source with a
|
|
159
|
+
hint (AWS / TWS / both). -->
|
|
160
|
+
<text id="rose-speed" x="0" y="82" text-anchor="middle" fill="#ffffff" font-size="16" font-weight="800">--- kn</text>
|
|
187
161
|
</svg>
|
|
188
162
|
</div>
|
|
189
163
|
</div>
|
|
@@ -277,18 +251,12 @@
|
|
|
277
251
|
<div id="rudder-value" class="value">--</div>
|
|
278
252
|
</div>
|
|
279
253
|
|
|
280
|
-
<!-- Rev53
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
rudder is amidships and slides left/right proportional to
|
|
286
|
-
`state.rudderAngle / state.rudderRange`. The central yellow
|
|
287
|
-
triangle keeps its nav-toggle role AND visually reads as the
|
|
288
|
-
"zero" reference of the rudder bar. -->
|
|
254
|
+
<!-- Rev53: swipe affordance strip with central triangle toggle.
|
|
255
|
+
Left/right arrows navigate to prev/next tab (like a mini
|
|
256
|
+
breadcrumb + swipe hint). Central yellow glow triangle
|
|
257
|
+
hides/shows the top nav bar. This lets the user reclaim
|
|
258
|
+
the full viewport for the rose. Only visible inside Control. -->
|
|
289
259
|
<div class="swipe-hint" id="swipe-hint" aria-hidden="true">
|
|
290
|
-
<div class="rudder-track"></div>
|
|
291
|
-
<div class="rudder-marker" id="rudder-marker" style="left:50%"></div>
|
|
292
260
|
<span class="swipe-hint-arrow" data-swipe-dir="prev">‹</span>
|
|
293
261
|
<span class="swipe-hint-arrow" data-swipe-dir="prev">‹</span>
|
|
294
262
|
<button class="nav-toggle" id="nav-toggle" aria-label="Mostrar / ocultar barra de pestañas" title="Mostrar / ocultar barra de pestañas">
|
|
@@ -918,6 +886,6 @@
|
|
|
918
886
|
</div>
|
|
919
887
|
|
|
920
888
|
<!-- Cache-busting: bump the ?v= on every Rev to force Firefox to fetch fresh app.js -->
|
|
921
|
-
<script src="app.js?v=
|
|
889
|
+
<script src="app.js?v=Rev66"></script>
|
|
922
890
|
</body>
|
|
923
891
|
</html>
|
package/public/style.css
CHANGED
|
@@ -1543,15 +1543,13 @@ main { touch-action: pan-y; overflow-x: hidden; }
|
|
|
1543
1543
|
strip with subtle motion cueing that swipe is available. Also acts
|
|
1544
1544
|
as tappable arrows for tab navigation. */
|
|
1545
1545
|
.swipe-hint {
|
|
1546
|
-
position: relative; /* Rev65: anchor for the rudder track + marker */
|
|
1547
1546
|
display: flex;
|
|
1548
1547
|
align-items: center;
|
|
1549
1548
|
justify-content: space-between;
|
|
1550
1549
|
gap: 10px;
|
|
1551
1550
|
margin-top: 10px;
|
|
1552
1551
|
padding: 8px 12px;
|
|
1553
|
-
background:
|
|
1554
|
-
below paints its own gradient on top. */
|
|
1552
|
+
background: linear-gradient(90deg, rgba(55,182,255,0.14) 0%, rgba(55,182,255,0.05) 50%, rgba(55,182,255,0.14) 100%);
|
|
1555
1553
|
border: 1px dashed rgba(55,182,255,0.35);
|
|
1556
1554
|
border-radius: 10px;
|
|
1557
1555
|
color: rgba(255, 255, 255, 0.65);
|
|
@@ -1561,46 +1559,6 @@ main { touch-action: pan-y; overflow-x: hidden; }
|
|
|
1561
1559
|
user-select: none;
|
|
1562
1560
|
overflow: hidden;
|
|
1563
1561
|
}
|
|
1564
|
-
|
|
1565
|
-
/* Rev65 / 2.0.2 - Rudder angle bar painted inside the swipe-hint strip.
|
|
1566
|
-
.rudder-track is a full-width horizontal gradient (red = port left,
|
|
1567
|
-
green = stbd right) with a dim mid-tone so amidships reads as
|
|
1568
|
-
"neutral". The three foreground layers (arrows, nav-toggle triangle,
|
|
1569
|
-
marker) sit above it via z-index. The nav-toggle's own yellow
|
|
1570
|
-
triangle visually stands in for the 0 deg reference. */
|
|
1571
|
-
.swipe-hint .rudder-track {
|
|
1572
|
-
position: absolute;
|
|
1573
|
-
inset: 4px 8px; /* small inset from the strip edge */
|
|
1574
|
-
border-radius: 8px;
|
|
1575
|
-
background: linear-gradient(
|
|
1576
|
-
90deg,
|
|
1577
|
-
rgba(231, 76, 60, 0.65) 0%, /* PORT: red */
|
|
1578
|
-
rgba(231, 76, 60, 0.20) 40%,
|
|
1579
|
-
rgba(46, 46, 46, 0.30) 50%, /* dim mid = amidships */
|
|
1580
|
-
rgba(46, 204, 113, 0.20) 60%,
|
|
1581
|
-
rgba(46, 204, 113, 0.65) 100% /* STBD: green */
|
|
1582
|
-
);
|
|
1583
|
-
z-index: 0;
|
|
1584
|
-
pointer-events: none;
|
|
1585
|
-
}
|
|
1586
|
-
.swipe-hint .rudder-marker {
|
|
1587
|
-
position: absolute;
|
|
1588
|
-
top: 2px;
|
|
1589
|
-
bottom: 2px;
|
|
1590
|
-
width: 4px;
|
|
1591
|
-
margin-left: -2px; /* center on `left` */
|
|
1592
|
-
background: #ffffff;
|
|
1593
|
-
border-radius: 2px;
|
|
1594
|
-
box-shadow: 0 0 6px rgba(255, 255, 255, 0.55);
|
|
1595
|
-
z-index: 1;
|
|
1596
|
-
transition: left 180ms ease-out;
|
|
1597
|
-
pointer-events: none;
|
|
1598
|
-
}
|
|
1599
|
-
.swipe-hint-arrow,
|
|
1600
|
-
.swipe-hint .nav-toggle {
|
|
1601
|
-
position: relative; /* stack above the rudder track */
|
|
1602
|
-
z-index: 2;
|
|
1603
|
-
}
|
|
1604
1562
|
.swipe-hint-arrow {
|
|
1605
1563
|
font-size: 28px;
|
|
1606
1564
|
line-height: 1;
|