signalk-pypilot-newui 2.0.0 → 2.0.2
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 +47 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/public/app.js +147 -43
- package/public/index.html +62 -30
- package/public/style.css +43 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.0.2 — 2026-08-20 — Rev65
|
|
4
|
+
|
|
5
|
+
Bug fix + UI polish batch.
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **Target diamond drifted after tapping AP on laggy links** (Tailscale,
|
|
10
|
+
4G). We were re-reading `state.heading` inside the parallel promise
|
|
11
|
+
chain, so between the click and the actual POST the heading could have
|
|
12
|
+
moved a few degrees. Consequence: we sent one target to the server,
|
|
13
|
+
asigned another locally, and the diamond jumped to a third when the
|
|
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).
|
|
42
|
+
|
|
43
|
+
## 2.0.1 — 2026-08-20 — Rev64
|
|
44
|
+
|
|
45
|
+
Patch release to validate the GitHub Actions publish pipeline with
|
|
46
|
+
[npm Trusted Publishing](https://docs.npmjs.com/trusted-publishers)
|
|
47
|
+
(OIDC-based, no NPM_TOKEN secret in the repo). No functional changes
|
|
48
|
+
versus 2.0.0. Look for the "verified provenance" badge on the npm page.
|
|
49
|
+
|
|
3
50
|
## 2.0.0 — 2026-08-20 — Rev63
|
|
4
51
|
|
|
5
52
|
**Breaking release addressing feedback from Sean D'Epagnier (pypilot author,
|
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 = "Rev65";
|
|
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.2",
|
|
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,6 +534,20 @@
|
|
|
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
|
+
})(),
|
|
537
551
|
};
|
|
538
552
|
|
|
539
553
|
const RAD2DEG = 180 / Math.PI;
|
|
@@ -672,7 +686,7 @@
|
|
|
672
686
|
function applyValue(path, value) {
|
|
673
687
|
switch (path) {
|
|
674
688
|
case "steering.autopilot.state": state.apState = value; break;
|
|
675
|
-
case "steering.autopilot.mode": state.mode = value; setSelect("#mode-select", value); break;
|
|
689
|
+
case "steering.autopilot.mode": state.mode = value; setSelect("#mode-select", value); _syncWindCornerFromMode(value); break;
|
|
676
690
|
case "steering.autopilot.target": state.target = numericOrNull(value); renderTargetArrow(); break;
|
|
677
691
|
case "steering.autopilot.engaged": state.engaged = !!value; renderEngage(); renderTargetArrow(); break;
|
|
678
692
|
case "steering.autopilot.availableActions": state.availableActions = value || []; break;
|
|
@@ -780,9 +794,12 @@
|
|
|
780
794
|
// getComputedTextLength() only works on rendered SVG text nodes.
|
|
781
795
|
// Fall back to a heuristic width if the SVG has not laid out yet
|
|
782
796
|
// (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.
|
|
783
800
|
let w;
|
|
784
|
-
try { w = hdgTxt.getComputedTextLength(); } catch { w = s.length *
|
|
785
|
-
if (!isFinite(w) || w <= 0) w = s.length *
|
|
801
|
+
try { w = hdgTxt.getComputedTextLength(); } catch { w = s.length * 9; }
|
|
802
|
+
if (!isFinite(w) || w <= 0) w = s.length * 9;
|
|
786
803
|
hdgDeg.setAttribute("x", (w / 2 + 1).toFixed(1));
|
|
787
804
|
}
|
|
788
805
|
}
|
|
@@ -804,9 +821,15 @@
|
|
|
804
821
|
twa: { label: "TWA", read: () => state.windAngleTrue == null ? "---" : _sgn(state.windAngleTrue * RAD2DEG) + "°" },
|
|
805
822
|
tws: { label: "TWS", read: () => state.windSpeedTrue == null ? "---" : (state.windSpeedTrue * 1.94384).toFixed(1) + " kn" },
|
|
806
823
|
twd: { label: "TWD", read: () => state.windDirTrue == null ? "---" : Math.round(state.windDirTrue * RAD2DEG) + "°" },
|
|
807
|
-
|
|
824
|
+
// Rev65 / 2.0.2: the "wind" corner now honours a user-toggleable
|
|
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"),
|
|
808
831
|
read: () => {
|
|
809
|
-
const useTrue =
|
|
832
|
+
const useTrue = state.windCornerShow === "twa";
|
|
810
833
|
const ang = useTrue ? state.windAngleTrue : state.windAngle;
|
|
811
834
|
const spd = useTrue ? state.windSpeedTrue : state.windSpeed;
|
|
812
835
|
const a = ang == null ? "---" : _sgn(ang * RAD2DEG) + "°";
|
|
@@ -885,6 +908,32 @@
|
|
|
885
908
|
});
|
|
886
909
|
}
|
|
887
910
|
_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
|
+
}
|
|
888
937
|
function _sgn(n) { return (n > 0 ? "+" : "") + Math.round(n); }
|
|
889
938
|
function _dashLoadCfg() {
|
|
890
939
|
try {
|
|
@@ -1062,9 +1111,10 @@
|
|
|
1062
1111
|
}
|
|
1063
1112
|
|
|
1064
1113
|
// SVG compass rose. The card (cardinals + ticks) rotates so N points to
|
|
1065
|
-
// magnetic north regardless of boat heading; the boat is fixed pointing
|
|
1066
|
-
// TWO independent wind arrows (AWA amber solid, TWA
|
|
1067
|
-
// to their respective angles; wedges are boat-fixed helm guides
|
|
1114
|
+
// magnetic north regardless of boat heading; the boat is fixed pointing
|
|
1115
|
+
// up; TWO independent wind arrows (AWA amber solid, TWA sea-green solid)
|
|
1116
|
+
// rotate to their respective angles; wedges are boat-fixed helm guides;
|
|
1117
|
+
// the bottom AWS/TWS readout follows the user's wind-corner preference.
|
|
1068
1118
|
function renderWindRose() {
|
|
1069
1119
|
const card = document.getElementById("rose-card");
|
|
1070
1120
|
if (card && state.heading != null) {
|
|
@@ -1090,22 +1140,29 @@
|
|
|
1090
1140
|
}
|
|
1091
1141
|
}
|
|
1092
1142
|
|
|
1093
|
-
//
|
|
1094
|
-
//
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1143
|
+
// Rev65 / 2.0.2: bottom AWS/TWS readout now uses TWO separate text
|
|
1144
|
+
// nodes (small dim label on top, big white number below) and follows
|
|
1145
|
+
// `state.windCornerShow` so a tap on the wind corner also swaps the
|
|
1146
|
+
// readout at the bottom of the rose. Falls back gracefully if the
|
|
1147
|
+
// chosen source has no value yet.
|
|
1148
|
+
const useTrue = state.windCornerShow === "twa";
|
|
1149
|
+
const primarySpd = useTrue ? state.windSpeedTrue : state.windSpeed;
|
|
1150
|
+
const primaryTag = useTrue ? "TWS" : "AWS";
|
|
1151
|
+
let windSpeed = primarySpd, tag = primaryTag;
|
|
1152
|
+
if (windSpeed == null) {
|
|
1153
|
+
// Fall back to the other source so the number is not "---" when only
|
|
1154
|
+
// one wind flavour is available (typical on boats without SOG).
|
|
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;
|
|
1105
1162
|
if (speedText) {
|
|
1106
1163
|
speedText.textContent = windSpeed == null
|
|
1107
1164
|
? "--- kn"
|
|
1108
|
-
: (windSpeed * 1.94384).toFixed(1) + " kn"
|
|
1165
|
+
: (windSpeed * 1.94384).toFixed(1) + " kn";
|
|
1109
1166
|
}
|
|
1110
1167
|
}
|
|
1111
1168
|
|
|
@@ -1200,14 +1257,36 @@
|
|
|
1200
1257
|
}
|
|
1201
1258
|
|
|
1202
1259
|
function renderRudder(rad) {
|
|
1203
|
-
|
|
1204
|
-
$("#rudder-widget")
|
|
1260
|
+
// Legacy widget (still hidden by default) - kept in case someone wires it.
|
|
1261
|
+
const w = $("#rudder-widget");
|
|
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");
|
|
1205
1271
|
const deg = rad * RAD2DEG;
|
|
1206
|
-
const
|
|
1272
|
+
const pct45 = Math.max(-1, Math.min(1, deg / 45));
|
|
1207
1273
|
const fill = $("#rudder-fill");
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1274
|
+
if (fill) {
|
|
1275
|
+
fill.style.width = Math.abs(pct45 * 50) + "%";
|
|
1276
|
+
fill.style.transform = pct45 < 0 ? "translateX(-100%)" : "translateX(0%)";
|
|
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
|
+
}
|
|
1211
1290
|
}
|
|
1212
1291
|
|
|
1213
1292
|
// Rev36 helper: build a slider row with nudge buttons -10/-1/+1/+10.
|
|
@@ -1667,6 +1746,16 @@
|
|
|
1667
1746
|
_apPending = true;
|
|
1668
1747
|
eng.classList.add("pending");
|
|
1669
1748
|
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;
|
|
1670
1759
|
// Optimistic: flip the visual state before waiting on the network.
|
|
1671
1760
|
state.engaged = !wasEngaged;
|
|
1672
1761
|
renderEngage();
|
|
@@ -1680,12 +1769,12 @@
|
|
|
1680
1769
|
// does not serialise 2 round-trips before the AP actually engages.
|
|
1681
1770
|
const [rEng, rTgt] = await Promise.all([
|
|
1682
1771
|
_apRetry(apEngage),
|
|
1683
|
-
|
|
1772
|
+
capturedHeading != null ? _apRetry(() => apSetTargetRad(capturedHeading)) : Promise.resolve({ ok: true }),
|
|
1684
1773
|
]);
|
|
1685
1774
|
ok = !!(rEng && rEng.ok);
|
|
1686
|
-
if (rTgt && rTgt.ok &&
|
|
1687
|
-
state.target =
|
|
1688
|
-
state.localTargetRad =
|
|
1775
|
+
if (rTgt && rTgt.ok && capturedHeading != null) {
|
|
1776
|
+
state.target = capturedHeading;
|
|
1777
|
+
state.localTargetRad = capturedHeading;
|
|
1689
1778
|
renderControl();
|
|
1690
1779
|
}
|
|
1691
1780
|
}
|
|
@@ -2487,35 +2576,50 @@
|
|
|
2487
2576
|
_activateTab(ids[nextIdx], dir === 1 ? "next" : "prev");
|
|
2488
2577
|
});
|
|
2489
2578
|
});
|
|
2490
|
-
// Rev48: long-press
|
|
2491
|
-
//
|
|
2579
|
+
// Rev48/65: long-press opens the corner-selector popup. Short tap on
|
|
2580
|
+
// a corner whose current mapping is "wind" flips the label between
|
|
2581
|
+
// AWA/AWS and TWA/TWS, pinning the user's choice so subsequent AP
|
|
2582
|
+
// mode changes stop auto-flipping it (see _syncWindCornerFromMode).
|
|
2492
2583
|
_dashLoadCfg();
|
|
2493
2584
|
_dashPopulateSelectors();
|
|
2494
2585
|
_dashRenderCorners();
|
|
2495
2586
|
let _dashLongPressTimer = null;
|
|
2587
|
+
let _dashPressStartedAt = 0;
|
|
2588
|
+
let _dashPressWasLong = false;
|
|
2496
2589
|
$$(".dash-rose .dash-cell[data-corner]").forEach((cell) => {
|
|
2497
2590
|
const openSelector = (e) => {
|
|
2498
2591
|
e && e.preventDefault();
|
|
2592
|
+
_dashPressWasLong = true;
|
|
2499
2593
|
_dashPopulateSelectors();
|
|
2500
2594
|
const pop = document.getElementById("dash-help-pop");
|
|
2501
2595
|
if (pop) pop.classList.add("open");
|
|
2502
2596
|
};
|
|
2503
|
-
|
|
2597
|
+
const startPress = (e) => {
|
|
2598
|
+
_dashPressStartedAt = Date.now();
|
|
2599
|
+
_dashPressWasLong = false;
|
|
2504
2600
|
if (_dashLongPressTimer) clearTimeout(_dashLongPressTimer);
|
|
2505
2601
|
_dashLongPressTimer = setTimeout(() => openSelector(e), 500);
|
|
2506
|
-
}
|
|
2507
|
-
const
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2602
|
+
};
|
|
2603
|
+
const endPress = () => {
|
|
2604
|
+
if (_dashLongPressTimer) { clearTimeout(_dashLongPressTimer); _dashLongPressTimer = null; }
|
|
2605
|
+
// Short tap on a wind-corner cell -> toggle AWS/TWS.
|
|
2606
|
+
if (_dashPressWasLong) return;
|
|
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 });
|
|
2511
2616
|
cell.addEventListener("contextmenu", openSelector);
|
|
2512
|
-
let _mouseTimer = null;
|
|
2513
2617
|
cell.addEventListener("mousedown", (e) => {
|
|
2514
2618
|
if (e.button !== 0) return;
|
|
2515
|
-
|
|
2619
|
+
startPress(e);
|
|
2516
2620
|
});
|
|
2517
|
-
cell.addEventListener("mouseup",
|
|
2518
|
-
cell.addEventListener("mouseleave", () => { if (
|
|
2621
|
+
cell.addEventListener("mouseup", endPress);
|
|
2622
|
+
cell.addEventListener("mouseleave", () => { if (_dashLongPressTimer) { clearTimeout(_dashLongPressTimer); _dashLongPressTimer = null; } });
|
|
2519
2623
|
});
|
|
2520
2624
|
// Rev43: horizontal swipe between tabs on mobile/tablet.
|
|
2521
2625
|
// 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 210" 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,10 +106,13 @@
|
|
|
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
|
-
|
|
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
|
+
<!-- Rev65: S moved inward from y=70 to y=58 so the AWS/TWS
|
|
112
|
+
bottom-of-rose label at y=70 does not overlap it when the
|
|
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>
|
|
113
116
|
<!-- 8 numeric labels at 30 deg increments (skip cardinals). -->
|
|
114
117
|
<g fill="#8b98a5" font-size="7" font-weight="700" font-family="ui-monospace, monospace" text-anchor="middle" dominant-baseline="middle">
|
|
115
118
|
<text transform="rotate(30) translate(0,-72)">030</text>
|
|
@@ -122,30 +125,46 @@
|
|
|
122
125
|
<text transform="rotate(330) translate(0,-72)">330</text>
|
|
123
126
|
</g>
|
|
124
127
|
</g>
|
|
125
|
-
<!--
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
128
|
+
<!-- Rev65 / 2.0.2: HDG in a small chip that straddles the top
|
|
129
|
+
rim. The chip's top edge sits above the ring by ~4 units
|
|
130
|
+
(visually "asomando" per Carlos), the bottom edge sits
|
|
131
|
+
just below the ring so the chip clearly reads as an
|
|
132
|
+
overlay on top of the compass, not as a floating badge.
|
|
133
|
+
The chip does NOT push the SVG viewBox up - only the
|
|
134
|
+
painted rect crosses the ring line. Number CENTERED on
|
|
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>
|
|
132
143
|
<!-- Rev50: boat overhead SVG (mareas-ihm), scaled 0.22 so
|
|
133
144
|
the hull spans ~100 units = 50 % of the 200-unit rose
|
|
134
145
|
diameter. Rotated -90 so bow points up. -->
|
|
135
146
|
<g id="rose-boat" transform="rotate(-90) scale(0.22)">
|
|
136
147
|
<image href="boat-cenital.svg" x="-228" y="-76" width="456" height="152"/>
|
|
137
148
|
</g>
|
|
138
|
-
<!--
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
149
|
+
<!-- Rev65 / 2.0.2: two independent wind arrows, both solid
|
|
150
|
+
(no more dashed shaft), each labelled AWA / TWA in text
|
|
151
|
+
that rotates with the arrow so it always reads from the
|
|
152
|
+
head. AWA = amber (the wind the crew feels), big head.
|
|
153
|
+
TWA = sea-green (informational), smaller head. Both
|
|
154
|
+
toggled independently from the corner popup. -->
|
|
142
155
|
<g id="rose-wind-arrow" style="display:none">
|
|
143
|
-
<line x1="0" y1="-86" x2="0" y2="-
|
|
144
|
-
<polygon points="0,-
|
|
156
|
+
<line x1="0" y1="-86" x2="0" y2="-32" stroke="#ffd54a" stroke-width="3" stroke-linecap="round"/>
|
|
157
|
+
<polygon points="0,-24 -9,-40 9,-40" fill="#ffd54a" stroke="#8a6a00" stroke-width="0.6"/>
|
|
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>
|
|
145
161
|
</g>
|
|
146
162
|
<g id="rose-wind-arrow-twa" style="display:none">
|
|
147
|
-
<line x1="0" y1="-86" x2="0" y2="-
|
|
148
|
-
<polygon points="0,-
|
|
163
|
+
<line x1="0" y1="-86" x2="0" y2="-38" stroke="#5ce8c0" stroke-width="2.5" stroke-linecap="round"/>
|
|
164
|
+
<polygon points="0,-32 -6,-42 6,-42" fill="#5ce8c0" stroke="#1a4438" stroke-width="0.6"/>
|
|
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>
|
|
149
168
|
</g>
|
|
150
169
|
<!-- Rev57: target indicator - CYAN diamond just outside the
|
|
151
170
|
compass ring, only shown when AP is engaged. Rotated to
|
|
@@ -154,10 +173,17 @@
|
|
|
154
173
|
<g id="rose-target-arrow" style="display:none">
|
|
155
174
|
<polygon points="0,-104 -7,-96 0,-88 7,-96" fill="#4dd4ff" stroke="#003b52" stroke-width="1.2"/>
|
|
156
175
|
</g>
|
|
157
|
-
<!--
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
176
|
+
<!-- Rev65 / 2.0.2: wind speed split into two stacked lines
|
|
177
|
+
- a small dim label (AWS/TWS/both) and a large white
|
|
178
|
+
number - separated by empty space so the S cardinal and
|
|
179
|
+
the surrounding rose stay visible in between. The whole
|
|
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>
|
|
161
187
|
</svg>
|
|
162
188
|
</div>
|
|
163
189
|
</div>
|
|
@@ -251,12 +277,18 @@
|
|
|
251
277
|
<div id="rudder-value" class="value">--</div>
|
|
252
278
|
</div>
|
|
253
279
|
|
|
254
|
-
<!-- Rev53: swipe affordance strip with central triangle toggle
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
280
|
+
<!-- Rev53/65: swipe affordance strip with central triangle toggle
|
|
281
|
+
AND (Rev65 / 2.0.2) a rudder-angle indicator painted on the
|
|
282
|
+
same strip. Track: horizontal gradient from red (port) at the
|
|
283
|
+
left to green (stbd) at the right, on a black band. Marker:
|
|
284
|
+
a thin vertical bar that sits at the exact centre when the
|
|
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. -->
|
|
259
289
|
<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>
|
|
260
292
|
<span class="swipe-hint-arrow" data-swipe-dir="prev">‹</span>
|
|
261
293
|
<span class="swipe-hint-arrow" data-swipe-dir="prev">‹</span>
|
|
262
294
|
<button class="nav-toggle" id="nav-toggle" aria-label="Mostrar / ocultar barra de pestañas" title="Mostrar / ocultar barra de pestañas">
|
|
@@ -886,6 +918,6 @@
|
|
|
886
918
|
</div>
|
|
887
919
|
|
|
888
920
|
<!-- Cache-busting: bump the ?v= on every Rev to force Firefox to fetch fresh app.js -->
|
|
889
|
-
<script src="app.js?v=
|
|
921
|
+
<script src="app.js?v=Rev65"></script>
|
|
890
922
|
</body>
|
|
891
923
|
</html>
|
package/public/style.css
CHANGED
|
@@ -1543,13 +1543,15 @@ 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 */
|
|
1546
1547
|
display: flex;
|
|
1547
1548
|
align-items: center;
|
|
1548
1549
|
justify-content: space-between;
|
|
1549
1550
|
gap: 10px;
|
|
1550
1551
|
margin-top: 10px;
|
|
1551
1552
|
padding: 8px 12px;
|
|
1552
|
-
background:
|
|
1553
|
+
background: #08111c; /* Rev65: solid dark base; the rudder track
|
|
1554
|
+
below paints its own gradient on top. */
|
|
1553
1555
|
border: 1px dashed rgba(55,182,255,0.35);
|
|
1554
1556
|
border-radius: 10px;
|
|
1555
1557
|
color: rgba(255, 255, 255, 0.65);
|
|
@@ -1559,6 +1561,46 @@ main { touch-action: pan-y; overflow-x: hidden; }
|
|
|
1559
1561
|
user-select: none;
|
|
1560
1562
|
overflow: hidden;
|
|
1561
1563
|
}
|
|
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
|
+
}
|
|
1562
1604
|
.swipe-hint-arrow {
|
|
1563
1605
|
font-size: 28px;
|
|
1564
1606
|
line-height: 1;
|