@@ -62,7 +62,7 @@ export function renderOverlayLeaderboard(rows, { t, escapeHtml, formatTeamActive
}
const sortAttrs = sortable
- ? ` data-sort-id="overlay-leaderboard" data-sort-labels="${sortValuesAttribute([t("table.pos"), t("table.driver"), t("table.laps"), t("table.result"), t("table.gap"), t("table.ahead_gap"), t("table.own_delta"), t("table.best_lap")])}"`
+ ? ` data-sort-id="overlay-leaderboard" data-sort-text-columns="1" data-sort-labels="${sortValuesAttribute([t("table.pos"), t("table.driver"), t("table.laps"), t("table.result"), t("table.gap"), t("table.ahead_gap"), t("table.own_delta"), t("table.best_lap")])}"`
: "";
return `
diff --git a/src/styles.css b/src/styles.css
index c372a20..4f3d287 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -3666,15 +3666,17 @@ select:focus {
.overlay-obs-tower {
display: flex;
+ flex-wrap: wrap;
align-items: flex-start;
justify-content: space-between;
gap: 10px;
}
.overlay-obs-tower-left {
+ flex: 0 1 320px;
display: grid;
gap: 6px;
- max-width: 320px;
+ max-width: 100%;
min-width: 0;
}
@@ -3688,7 +3690,8 @@ select:focus {
}
.overlay-obs-tower-standings {
- max-width: 340px;
+ flex: 0 1 340px;
+ max-width: 100%;
min-width: 0;
}
@@ -3740,7 +3743,7 @@ select:focus {
.overlay-obs-tower-row {
display: grid;
- grid-template-columns: 24px minmax(0, 1fr) auto;
+ grid-template-columns: 24px minmax(72px, 1fr) minmax(0, auto);
gap: 6px;
align-items: center;
padding: 5px 6px;
@@ -3774,6 +3777,8 @@ select:focus {
.overlay-obs-tower-trail {
display: flex;
+ flex-wrap: wrap;
+ min-width: 0;
align-items: center;
gap: 6px;
justify-content: flex-end;
diff --git a/src/timing_views.js b/src/timing_views.js
index 63466d8..f382afa 100644
--- a/src/timing_views.js
+++ b/src/timing_views.js
@@ -311,7 +311,7 @@ function renderLeaderboard(rows, deps) {
`;
})
- ), "timing-leaderboard");
+ ), "timing-leaderboard", [1, 2, 3]);
}
function renderRecentPassings(session, deps) {
@@ -340,7 +340,7 @@ function renderRecentPassings(session, deps) {
`;
})
- ), "timing-passings");
+ ), "timing-passings", [1, 2, 3, 5]);
}
export function renderTimingView(deps) {
diff --git a/src/training_views.js b/src/training_views.js
index 87c4c63..cb7f6c2 100644
--- a/src/training_views.js
+++ b/src/training_views.js
@@ -581,7 +581,7 @@ export function renderTrainingView(deps) {
${channelStats.recentPitStops.length ? `
${escapeHtml(t("training.recent_pit_stops"))}
-
+
| ${escapeHtml(t("training.date"))} | ${escapeHtml(t("table.event"))} | ${escapeHtml(t("table.session"))} | ${escapeHtml(t("table.car"))} | ${escapeHtml(t("table.duration"))} |
${channelStats.recentPitStops.map((stop) => `
@@ -614,7 +614,7 @@ export function renderTrainingView(deps) {
+
-
+
| ${escapeHtml(t("training.driver"))} | ${escapeHtml(t("training.laps"))} | ${escapeHtml(t("training.training_days"))} | ${escapeHtml(t("training.best_lap"))} | ${escapeHtml(t("training.next_target"))} |
${clubRows}
@@ -942,7 +942,7 @@ function renderTrainingOverview({ state, dom, t, escapeHtml, formatLap, uid, sav
${escapeHtml(t("training.overview_class_hint"))}
-
+
| ${escapeHtml(t("training.driver"))} | ${escapeHtml(t("training.class"))} | ${escapeHtml(t("training.laps"))} | ${escapeHtml(t("training.training_days"))} | ${escapeHtml(t("training.best_lap"))} | ${escapeHtml(t("training.last_activity"))} | ${escapeHtml(t("training.next_target"))} |
${tableRows}
diff --git a/tests/list_sorting.test.js b/tests/list_sorting.test.js
new file mode 100644
index 0000000..7a87e60
--- /dev/null
+++ b/tests/list_sorting.test.js
@@ -0,0 +1,33 @@
+import test from "node:test";
+import assert from "node:assert/strict";
+import { parseSortValue, compareSortValues, sortableTable } from "../src/list_sorting.js";
+
+test("zero-padded and ordinary lap times sort chronologically", () => {
+ const times = ["00:59.000", "0:10.000", "01:00.000", "00:09,500", "1:02:03.000"];
+ assert.deepEqual(times.map(parseSortValue), [59, 10, 60, 9.5, 3723]);
+ assert.deepEqual([...times].sort((a, b) => compareSortValues(parseSortValue(a), parseSortValue(b))), ["00:09,500", "0:10.000", "00:59.000", "01:00.000", "1:02:03.000"]);
+});
+
+test("distinct numeric-looking names have deterministic ascending and descending order", () => {
+ for (const [left, right] of [["09", "9"], ["Car 09", "Car 9"], ["009", "09"]]) {
+ const comparison = compareSortValues(left, right);
+ assert.notEqual(comparison, 0);
+ assert.equal(Math.sign(comparison), -Math.sign(compareSortValues(right, left)));
+ assert.equal(Math.sign(comparison), -Math.sign(compareSortValues(left, right, "desc")));
+ }
+ assert.ok(compareSortValues("Car 2", "Car 10") < 0);
+});
+
+test("missing times stay last in both sort directions", () => {
+ for (const direction of ["asc", "desc"]) {
+ assert.ok(compareSortValues(null, 20, direction) > 0);
+ assert.ok(compareSortValues(20, NaN, direction) < 0);
+ }
+ assert.equal(parseSortValue("-"), null);
+ assert.equal(parseSortValue("-00:01.500"), -1.5);
+ assert.equal(parseSortValue("+0.250s"), 0.25);
+});
+
+test("tables can declare names and identifiers as text columns", () => {
+ assert.match(sortableTable('', 'timing-passings', [1, 2, 3]), /data-sort-text-columns="1,2,3"/);
+});
diff --git a/tests/run.js b/tests/run.js
index 36a2f74..92b7bcc 100644
--- a/tests/run.js
+++ b/tests/run.js
@@ -15,3 +15,4 @@ import "./training_views.test.js";
import "./session_replay.test.js";
import "./public_training_refresh.test.js";
+import "./list_sorting.test.js";