diff --git a/README.md b/README.md
index f6f3fc7..1202d2d 100644
--- a/README.md
+++ b/README.md
@@ -90,7 +90,8 @@ JMK RB RaceController is an RC timing and race-control system with support for s
- Editing in UI:
- classes, event names/dates, drivers, and cars can be edited directly
- drivers and cars support optional `brand` fields for team, sponsor, manufacturer, or model data
- - `Drivers` and `Cars` can be filtered by brand directly in the UI
+ - `Drivers` and `Cars` can be searched by name, transponder, or brand directly in the UI
+ - race start lists and heat sheets include driver brand in print, CSV, and PDF exports
- Live race control:
- countdown during timed sessions
- auto-finish at end of session time with `Race is finished`
diff --git a/README.sv.md b/README.sv.md
index 2b94087..9004cc6 100644
--- a/README.sv.md
+++ b/README.sv.md
@@ -81,7 +81,8 @@ RC timing app med sponsor-eventflöde (delade bilar/transpondrar mellan olika he
- Redigering i UI:
- Klasser, eventnamn/datum, förare och bilar kan redigeras direkt
- förare och bilar har valfria brandfält för team, sponsor, märke eller modell
- - `Förare` och `Bilar` kan filtreras direkt på brand i UI
+ - `Förare` och `Bilar` kan sökas på namn, transponder eller brand direkt i UI
+ - startlistor och heatsheets tar med förarens brand i print, CSV och PDF-export
- Live race-kontroll:
- Nedräkning under pågående session
- Auto-finish vid tidslut med status `Race is finished`
diff --git a/src/app.js b/src/app.js
index 0bb3f64..3f53a02 100644
--- a/src/app.js
+++ b/src/app.js
@@ -87,14 +87,14 @@ const TRANSLATIONS = {
"drivers.create": "Skapa förare",
"drivers.name_placeholder": "Förarnamn",
"drivers.brand_placeholder": "Team / märke (valfritt)",
- "drivers.brand_filter_placeholder": "Filtrera på team / märke",
+ "drivers.brand_filter_placeholder": "Sök namn / transponder / brand",
"drivers.transponder_placeholder": "Personlig transponder (valfritt)",
"drivers.add": "Lägg till förare",
"drivers.title": "Förare",
"cars.create": "Skapa bil",
"cars.name_placeholder": "Bilnamn eller nummer",
"cars.brand_placeholder": "Märke / modell (valfritt)",
- "cars.brand_filter_placeholder": "Filtrera på märke / modell",
+ "cars.brand_filter_placeholder": "Sök namn / transponder / brand",
"cars.transponder_placeholder": "Bilens transponder",
"cars.add": "Lägg till bil",
"cars.title": "Bilar",
@@ -729,14 +729,14 @@ const TRANSLATIONS = {
"drivers.create": "Create Driver",
"drivers.name_placeholder": "Driver name",
"drivers.brand_placeholder": "Team / brand (optional)",
- "drivers.brand_filter_placeholder": "Filter by team / brand",
+ "drivers.brand_filter_placeholder": "Search name / transponder / brand",
"drivers.transponder_placeholder": "Personal transponder (optional)",
"drivers.add": "Add Driver",
"drivers.title": "Drivers",
"cars.create": "Create Track Car",
"cars.name_placeholder": "Car name or number",
"cars.brand_placeholder": "Brand / model (optional)",
- "cars.brand_filter_placeholder": "Filter by brand / model",
+ "cars.brand_filter_placeholder": "Search name / transponder / brand",
"cars.transponder_placeholder": "Car transponder",
"cars.add": "Add Car",
"cars.title": "Cars",
@@ -2841,10 +2841,12 @@ function renderDrivers() {
const classOptions = state.classes
.map((c) => ``)
.join("");
+ const driverSearch = driverBrandFilter.trim().toLowerCase();
const filteredDrivers = state.drivers.filter((driver) =>
- String(driver.brand || "")
- .toLowerCase()
- .includes(driverBrandFilter.trim().toLowerCase())
+ !driverSearch ||
+ [driver.name, driver.transponder, driver.brand]
+ .map((value) => String(value || "").toLowerCase())
+ .some((value) => value.includes(driverSearch))
);
const editingDriver = state.drivers.find((driver) => driver.id === selectedDriverEditId) || null;
@@ -3013,10 +3015,12 @@ function renderDrivers() {
}
function renderCars() {
+ const carSearch = carBrandFilter.trim().toLowerCase();
const filteredCars = state.cars.filter((car) =>
- String(car.brand || "")
- .toLowerCase()
- .includes(carBrandFilter.trim().toLowerCase())
+ !carSearch ||
+ [car.name, car.transponder, car.brand]
+ .map((value) => String(value || "").toLowerCase())
+ .some((value) => value.includes(carSearch))
);
const editingCar = state.cars.find((car) => car.id === selectedCarEditId) || null;
dom.view.innerHTML = `
@@ -5910,10 +5914,18 @@ function renderOverlayLeaderboard(rows) {
+
${escapeHtml(row.gapAhead || "-")}
@@ -8463,16 +8475,18 @@ function buildRaceStartListsHtml(event) {
${
entries.length
? renderTable(
- [t("events.slot"), t("table.driver"), t("table.transponder")],
- entries.map(
- (entry) => `
+ [t("events.slot"), t("table.driver"), t("table.brand"), t("table.transponder")],
+ entries.map((entry) => {
+ const driver = state.drivers.find((item) => item.id === entry.id);
+ return `
| ${entry.slot} |
${escapeHtml(entry.name)} |
+ ${escapeHtml(driver?.brand || "-")} |
${escapeHtml(entry.meta || "-")} |
- `
- )
+ `;
+ })
)
: `
${t("common.no_entries")}
`
}
@@ -8684,8 +8698,11 @@ async function exportRaceStartListsPdf(event) {
const sections = sessions.map((session) =>
buildPdfSection(
`${session.name} • ${getSessionTypeLabel(session.type)}`,
- [t("events.slot"), t("table.driver"), t("table.transponder")],
- getSessionGridEntries(session).map((entry) => [String(entry.slot), entry.name, entry.meta || "-"])
+ [t("events.slot"), t("table.driver"), t("table.brand"), t("table.transponder")],
+ getSessionGridEntries(session).map((entry) => {
+ const driver = state.drivers.find((item) => item.id === entry.id);
+ return [String(entry.slot), entry.name, driver?.brand || "-", entry.meta || "-"];
+ })
)
);
@@ -8815,8 +8832,11 @@ async function exportSessionHeatSheetPdf(session) {
sections: [
buildPdfSection(
`${session.name} • ${getSessionTypeLabel(session.type)}`,
- [t("events.slot"), t("table.driver"), t("table.transponder")],
- getSessionGridEntries(session).map((entry) => [String(entry.slot), entry.name, entry.meta || "-"])
+ [t("events.slot"), t("table.driver"), t("table.brand"), t("table.transponder")],
+ getSessionGridEntries(session).map((entry) => {
+ const driver = state.drivers.find((item) => item.id === entry.id);
+ return [String(entry.slot), entry.name, driver?.brand || "-", entry.meta || "-"];
+ })
),
],
});
@@ -8848,16 +8868,18 @@ function buildSessionHeatSheetHtml(session) {
${
entries.length
? renderTable(
- [t("events.slot"), t("table.driver"), t("table.transponder")],
- entries.map(
- (entry) => `
+ [t("events.slot"), t("table.driver"), t("table.brand"), t("table.transponder")],
+ entries.map((entry) => {
+ const driver = state.drivers.find((item) => item.id === entry.id);
+ return `
| ${entry.slot} |
${escapeHtml(entry.name)} |
+ ${escapeHtml(driver?.brand || "-")} |
${escapeHtml(entry.meta || "-")} |
- `
- )
+ `;
+ })
)
: `
${t("common.no_entries")}
`
}
@@ -8868,7 +8890,7 @@ function exportSessionHeatSheet(session) {
const event = state.events.find((item) => item.id === session.eventId);
const entries = getSessionGridEntries(session);
const rows = [
- ["event", "class", "session", "type", "start_mode", "duration_min", "slot", "driver", "transponder"],
+ ["event", "class", "session", "type", "start_mode", "duration_min", "slot", "driver", "brand", "transponder"],
...entries.map((entry) => [
event?.name || "",
getClassName(event?.classId || ""),
@@ -8878,6 +8900,7 @@ function exportSessionHeatSheet(session) {
String(session.durationMin || ""),
String(entry.slot),
entry.name,
+ state.drivers.find((item) => item.id === entry.id)?.brand || "",
entry.meta || "",
]),
];
diff --git a/src/styles.css b/src/styles.css
index 9e2c61f..fd27589 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -879,8 +879,8 @@ select:focus {
.overlay-board {
display: grid;
- grid-template-columns: minmax(0, 1.92fr) minmax(180px, 0.31fr);
- gap: 8px;
+ grid-template-columns: minmax(0, 1.98fr) minmax(156px, 0.27fr);
+ gap: 6px;
}
.overlay-board-tv {
@@ -982,7 +982,7 @@ select:focus {
}
.overlay-table-wrap {
- padding: 4px 6px 6px;
+ padding: 3px 5px 5px;
}
.overlay-display-wrap {
@@ -1002,8 +1002,8 @@ select:focus {
display: flex;
justify-content: space-between;
align-items: end;
- gap: 8px;
- padding: 7px 10px;
+ gap: 6px;
+ padding: 6px 8px;
background:
linear-gradient(135deg, rgba(225, 6, 0, 0.18), rgba(225, 6, 0, 0.04)),
rgba(7, 12, 20, 0.92);
@@ -1018,13 +1018,13 @@ select:focus {
.overlay-fastest-banner strong {
display: block;
- margin-top: 2px;
+ margin-top: 1px;
font-family: Orbitron, sans-serif;
- font-size: clamp(1.02rem, 1.7vw, 1.45rem);
+ font-size: clamp(0.94rem, 1.45vw, 1.26rem);
}
.overlay-leaderboard-card {
- padding: 5px;
+ padding: 4px;
}
.overlay-leaderboard-card-tv {
@@ -1045,13 +1045,13 @@ select:focus {
}
.overlay-fastest-driver {
- font-size: 0.76rem;
+ font-size: 0.68rem;
text-align: right;
}
.overlay-fastest-meta {
color: var(--muted);
- font-size: 0.58rem;
+ font-size: 0.52rem;
text-transform: uppercase;
letter-spacing: 0.06em;
white-space: nowrap;
@@ -1063,14 +1063,14 @@ select:focus {
.overlay-race-metric strong,
.overlay-race-best strong {
- font-size: clamp(0.78rem, 0.98vw, 0.92rem);
- line-height: 1.05;
+ font-size: clamp(0.72rem, 0.88vw, 0.84rem);
+ line-height: 1.02;
}
.overlay-race-pos .pos-pill {
- min-width: 26px;
- height: 26px;
- font-size: 0.76rem;
+ min-width: 22px;
+ height: 22px;
+ font-size: 0.68rem;
}
.overlay-shell-dense .pill {
@@ -1123,12 +1123,12 @@ select:focus {
.overlay-side {
display: grid;
- gap: 6px;
- font-size: 0.78rem;
+ gap: 4px;
+ font-size: 0.72rem;
}
.overlay-side-card {
- padding: 6px;
+ padding: 5px;
}
.overlay-rotating-card {
@@ -1136,15 +1136,15 @@ select:focus {
}
.overlay-side-card h3 {
- margin: 0 0 3px;
- font-size: 0.78rem;
+ margin: 0 0 2px;
+ font-size: 0.7rem;
}
.overlay-passing {
display: flex;
justify-content: space-between;
- gap: 5px;
- padding: 3px 0;
+ gap: 4px;
+ padding: 2px 0;
border-bottom: 1px solid var(--line);
}
@@ -1153,13 +1153,13 @@ select:focus {
}
.overlay-side-card .overlay-passing strong {
- font-size: 0.72rem;
+ font-size: 0.66rem;
line-height: 1.15;
}
.overlay-side-card .overlay-passing span {
font-family: Orbitron, sans-serif;
- font-size: 0.64rem;
+ font-size: 0.58rem;
color: var(--text);
white-space: nowrap;
}
@@ -1171,15 +1171,15 @@ select:focus {
.overlay-race-list {
display: grid;
- gap: 4px;
+ gap: 3px;
}
.overlay-race-row {
display: grid;
- grid-template-columns: 38px minmax(170px, 1.72fr) repeat(3, minmax(90px, 0.62fr)) minmax(95px, 0.66fr);
- gap: 6px;
+ grid-template-columns: 34px minmax(160px, 1.7fr) repeat(4, minmax(78px, 0.54fr)) minmax(86px, 0.58fr);
+ gap: 5px;
align-items: center;
- padding: 5px 7px;
+ padding: 4px 6px;
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 8px;
background: rgba(255, 255, 255, 0.03);
@@ -1202,15 +1202,15 @@ select:focus {
}
.overlay-race-driver strong {
- font-size: clamp(0.82rem, 1.04vw, 0.96rem);
- line-height: 1.05;
+ font-size: clamp(0.75rem, 0.92vw, 0.88rem);
+ line-height: 1.02;
}
.overlay-race-driver span,
.overlay-race-metric label,
.overlay-race-best label {
color: var(--muted);
- font-size: 0.54rem;
+ font-size: 0.5rem;
text-transform: uppercase;
letter-spacing: 0.06em;
}
@@ -1225,14 +1225,14 @@ select:focus {
}
.overlay-prediction {
- margin-top: 2px;
+ margin-top: 1px;
}
.overlay-prediction-meta {
display: flex;
justify-content: space-between;
- gap: 8px;
- margin-bottom: 2px;
+ gap: 6px;
+ margin-bottom: 1px;
}
.overlay-prediction-meta label,