signalk-race-control 0.5.0 → 0.5.1
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/README.md +7 -1
- package/index.js +13 -2
- package/package.json +1 -1
- package/public/app.js +35 -13
- package/public/index.html +1 -1
- package/public/style.css +15 -1
package/README.md
CHANGED
|
@@ -25,7 +25,13 @@ and a live projected finishing order — during and after the race.
|
|
|
25
25
|
- **Add/remove boats explicitly** — type a name (autocompletes against the VET
|
|
26
26
|
register, the cross-race boat registry, and any live AIS/self vessel — matching
|
|
27
27
|
anywhere in the name, not just the start) and click **Add Boat**. Boats aren't
|
|
28
|
-
auto-populated from AIS; you control exactly who's racing.
|
|
28
|
+
auto-populated from AIS; you control exactly who's racing. Rows stay in the order
|
|
29
|
+
boats were added — adding or removing one doesn't reshuffle the rest — until the
|
|
30
|
+
race actually starts, when the table switches to rank order; whichever boat is
|
|
31
|
+
marked **self** always stays on top regardless of phase. The **Boat** column stays
|
|
32
|
+
pinned in place while scrolling sideways through the rest of the (wide) table, and
|
|
33
|
+
the page itself isn't width-capped, so a larger screen shows more columns at once
|
|
34
|
+
without scrolling.
|
|
29
35
|
- **Elapsed / corrected time** — Time-on-Time correction: `corrected = elapsed × TCF`,
|
|
30
36
|
where TCF is edited per boat directly in the webapp and persisted server-side,
|
|
31
37
|
shared by everyone viewing the page.
|
package/index.js
CHANGED
|
@@ -426,7 +426,7 @@ button.confirming { background: var(--bad); color: #2a0a0a; border-color: var(--
|
|
|
426
426
|
button:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
427
427
|
.status { min-height: 1.2em; margin-top: 0.5rem; font-size: 0.85rem; color: var(--muted); }
|
|
428
428
|
.status.error { color: var(--bad); }
|
|
429
|
-
main { padding: 1rem 1.5rem max(2rem, env(safe-area-inset-bottom));
|
|
429
|
+
main { padding: 1rem 1.5rem max(2rem, env(safe-area-inset-bottom)); margin: 0 auto; }
|
|
430
430
|
.add-boat-row { display: flex; gap: 0.5rem; margin-bottom: 1rem; flex-wrap: wrap; }
|
|
431
431
|
.add-boat-row input[type='text'] { flex: 1; min-width: 12rem; padding: 0.45rem 0.6rem; background: var(--panel); color: var(--text); border: 1px solid var(--border); border-radius: 6px; font-size: 0.9rem; }
|
|
432
432
|
.add-boat-row input[type='number'] { width: 6rem; padding: 0.45rem 0.6rem; background: var(--panel); color: var(--text); border: 1px solid var(--border); border-radius: 6px; font-size: 0.9rem; }
|
|
@@ -434,6 +434,8 @@ main { padding: 1rem 1.5rem max(2rem, env(safe-area-inset-bottom)); max-width: 9
|
|
|
434
434
|
table { width: 100%; min-width: 44rem; border-collapse: collapse; font-variant-numeric: tabular-nums; }
|
|
435
435
|
thead th { text-align: left; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); padding: 0.5rem 0.6rem; border-bottom: 1px solid var(--border); }
|
|
436
436
|
tbody td { padding: 0.55rem 0.6rem; border-bottom: 1px solid var(--border); }
|
|
437
|
+
.boat-name-col { position: sticky; left: 0; z-index: 1; background: var(--bg); }
|
|
438
|
+
thead .boat-name-col { z-index: 2; }
|
|
437
439
|
tbody tr.finished td { color: var(--good); }
|
|
438
440
|
tbody tr.dnf td { color: var(--muted); }
|
|
439
441
|
.dnf-tag { color: var(--bad); font-weight: 700; font-size: 0.85rem; letter-spacing: 0.03em; }
|
|
@@ -498,7 +500,7 @@ tbody tr.dnf td { color: var(--muted); }
|
|
|
498
500
|
<table id="boatsTable">
|
|
499
501
|
<thead>
|
|
500
502
|
<tr>
|
|
501
|
-
<th>Boat</th>
|
|
503
|
+
<th class="boat-name-col">Boat</th>
|
|
502
504
|
<th>Sail #</th>
|
|
503
505
|
<th>TCF</th>
|
|
504
506
|
<th id="vetAlternativesTh" hidden>VET alternatives</th>
|
|
@@ -817,6 +819,14 @@ tbody tr.dnf td { color: var(--muted); }
|
|
|
817
819
|
return { boat: boat, elapsedMs: elapsedMs, correctedMs: correctedMs, rankMs: rankMs };
|
|
818
820
|
})
|
|
819
821
|
.sort(function (a, b) {
|
|
822
|
+
// Self always leads, whatever else is true.
|
|
823
|
+
var aSelf = a.boat.id === race.selfBoatId;
|
|
824
|
+
var bSelf = b.boat.id === race.selfBoatId;
|
|
825
|
+
if (aSelf !== bSelf) return aSelf ? -1 : 1;
|
|
826
|
+
// Before the race actually starts, nobody has a corrected time to
|
|
827
|
+
// rank by anyway — leave order exactly as race.boats gave it
|
|
828
|
+
// (registration order) rather than reshuffling on every add/remove.
|
|
829
|
+
if (!race.startTime) return 0;
|
|
820
830
|
if (a.rankMs == null && b.rankMs == null) return a.boat.name.localeCompare(b.boat.name);
|
|
821
831
|
if (a.rankMs == null) return 1;
|
|
822
832
|
if (b.rankMs == null) return -1;
|
|
@@ -938,6 +948,7 @@ tbody tr.dnf td { color: var(--muted); }
|
|
|
938
948
|
});
|
|
939
949
|
var nameSpan = document.createElement('span');
|
|
940
950
|
var tdName = document.createElement('td');
|
|
951
|
+
tdName.className = 'boat-name-col';
|
|
941
952
|
tdName.append(selfBtn, nameSpan);
|
|
942
953
|
|
|
943
954
|
var sailNumberInput = document.createElement('input');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "signalk-race-control",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "SignalK plugin and webapp for tracking elapsed and handicap-corrected (Time-on-Time) race time, with a per-boat editable TCF and boat names pulled from AIS when available.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"author": "Joachim Bakke <github@heiamoss.com>",
|
package/public/app.js
CHANGED
|
@@ -1607,6 +1607,19 @@
|
|
|
1607
1607
|
};
|
|
1608
1608
|
})
|
|
1609
1609
|
.sort((a, b) => {
|
|
1610
|
+
// Self always leads, whatever else is true — it's the boat whoever
|
|
1611
|
+
// is looking at this screen cares about finding without hunting
|
|
1612
|
+
// for it.
|
|
1613
|
+
const aSelf = a.boatId === raceState.selfBoatId;
|
|
1614
|
+
const bSelf = b.boatId === raceState.selfBoatId;
|
|
1615
|
+
if (aSelf !== bSelf) return aSelf ? -1 : 1;
|
|
1616
|
+
// Before the race actually starts, nobody has a corrected time to
|
|
1617
|
+
// rank by anyway — sorting by name (or anything else derived) just
|
|
1618
|
+
// reshuffled the rows confusingly every time a boat was added or
|
|
1619
|
+
// removed. Rely on sort's stability and leave order exactly as
|
|
1620
|
+
// Object.values(raceState.boats) gave it (i.e. registration order)
|
|
1621
|
+
// until there's an actual race to rank.
|
|
1622
|
+
if (!raceState.startTime) return 0;
|
|
1610
1623
|
if (a.rankMs == null && b.rankMs == null) return a.name.localeCompare(b.name);
|
|
1611
1624
|
if (a.rankMs == null) return 1;
|
|
1612
1625
|
if (b.rankMs == null) return -1;
|
|
@@ -1692,6 +1705,7 @@
|
|
|
1692
1705
|
nameSpan.className = 'boat-name-text';
|
|
1693
1706
|
|
|
1694
1707
|
const tdName = document.createElement('td');
|
|
1708
|
+
tdName.className = 'boat-name-col';
|
|
1695
1709
|
tdName.append(selfBtn, nameSpan);
|
|
1696
1710
|
|
|
1697
1711
|
const sailNumberInput = document.createElement('input');
|
|
@@ -2041,20 +2055,28 @@
|
|
|
2041
2055
|
const vsSelfMap = computeVsSelf(boats, raceState.selfBoatId);
|
|
2042
2056
|
|
|
2043
2057
|
boats.forEach((b) => {
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
row = buildRow(b.boatId);
|
|
2047
|
-
rows.set(b.boatId, row);
|
|
2048
|
-
}
|
|
2049
|
-
// Moving an already-attached node to its sorted position keeps it
|
|
2050
|
-
// intact (focus, in-progress edits) rather than recreating it — but
|
|
2051
|
-
// the move itself still resets a text cursor and closes an open
|
|
2052
|
-
// <select>, so skip it entirely while the user has something in this
|
|
2053
|
-
// row focused. It'll snap to its correct position as soon as they're
|
|
2054
|
-
// done (next render after focus moves away).
|
|
2055
|
-
if (!row.tr.contains(document.activeElement)) {
|
|
2056
|
-
boatsBody.appendChild(row.tr);
|
|
2058
|
+
if (!rows.get(b.boatId)) {
|
|
2059
|
+
rows.set(b.boatId, buildRow(b.boatId));
|
|
2057
2060
|
}
|
|
2061
|
+
});
|
|
2062
|
+
// Moving already-attached nodes to their sorted position keeps them
|
|
2063
|
+
// intact (focus, in-progress edits) rather than recreating them — but
|
|
2064
|
+
// the move itself still resets a text cursor and closes an open
|
|
2065
|
+
// <select>, so the whole reorder pass is skipped while the user has
|
|
2066
|
+
// anything in ANY row focused, not just the row that would move.
|
|
2067
|
+
// Reordering around a skipped row by moving everyone else in sequence
|
|
2068
|
+
// can still shuffle it to a different spot as a side effect of those
|
|
2069
|
+
// other moves, which is exactly the "keep self on top" guarantee this
|
|
2070
|
+
// was supposed to preserve — so it's all-or-nothing instead. It'll
|
|
2071
|
+
// snap to the fully correct order on the next render once focus moves
|
|
2072
|
+
// away.
|
|
2073
|
+
const anyRowFocused = boats.some((b) => rows.get(b.boatId).tr.contains(document.activeElement));
|
|
2074
|
+
if (!anyRowFocused) {
|
|
2075
|
+
boats.forEach((b) => boatsBody.appendChild(rows.get(b.boatId).tr));
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
boats.forEach((b) => {
|
|
2079
|
+
const row = rows.get(b.boatId);
|
|
2058
2080
|
|
|
2059
2081
|
row.tr.classList.toggle('finished', !!b.finishTime);
|
|
2060
2082
|
row.tr.classList.toggle('dnf', !!b.dnf);
|
package/public/index.html
CHANGED
package/public/style.css
CHANGED
|
@@ -216,7 +216,6 @@ button:disabled {
|
|
|
216
216
|
|
|
217
217
|
main {
|
|
218
218
|
padding: 1rem 1.5rem 2rem;
|
|
219
|
-
max-width: 1000px;
|
|
220
219
|
margin: 0 auto;
|
|
221
220
|
}
|
|
222
221
|
|
|
@@ -591,6 +590,21 @@ tbody td {
|
|
|
591
590
|
border-bottom: 1px solid var(--border);
|
|
592
591
|
}
|
|
593
592
|
|
|
593
|
+
/* Frozen first column, so the boat's name stays visible while scrolling
|
|
594
|
+
the (much wider than most screens, especially phones in landscape)
|
|
595
|
+
table sideways to reach the other columns. Needs its own opaque
|
|
596
|
+
background so scrolled-under cells don't show through. */
|
|
597
|
+
.boat-name-col {
|
|
598
|
+
position: sticky;
|
|
599
|
+
left: 0;
|
|
600
|
+
z-index: 1;
|
|
601
|
+
background: var(--bg);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
thead .boat-name-col {
|
|
605
|
+
z-index: 2;
|
|
606
|
+
}
|
|
607
|
+
|
|
594
608
|
tbody tr.finished td {
|
|
595
609
|
color: var(--good);
|
|
596
610
|
}
|