Improve race setup navigation and manage flow

This commit is contained in:
larssand
2026-03-20 15:21:36 +01:00
parent 920763601b
commit 1de654a220
4 changed files with 142 additions and 46 deletions

View File

@@ -44,6 +44,9 @@ JMK RB RaceController is an RC timing and race-control system with support for s
- a preset-aware summary card that changes between endurance and normal race focus - a preset-aware summary card that changes between endurance and normal race focus
- status badges for `Setup`, `Format`, `Generation`, and `Live / results` - status badges for `Setup`, `Format`, `Generation`, and `Live / results`
- summary warnings when participants, sessions, teams, or lap-window settings are missing - summary warnings when participants, sessions, teams, or lap-window settings are missing
- clickable summary warnings that jump to the relevant Manage section
- clickable step cards that act as quick navigation inside `Manage`
- compact step cards with live counters such as drivers, teams, sessions, and active/results state
- separate `Race actions` for generation, reseeding and bump-up - separate `Race actions` for generation, reseeding and bump-up
- reorganized in-app `Guide` with overview cards and two-column sections - reorganized in-app `Guide` with overview cards and two-column sections
- explicit race participant selection - explicit race participant selection
@@ -145,7 +148,7 @@ JMK RB RaceController is an RC timing and race-control system with support for s
- `Format` - `Format`
- `Generation` - `Generation`
- `Live / results` - `Live / results`
- `Setup` handles race participants and teams. - `Setup` handles race participants and teams. The top step cards also act as quick navigation between the four blocks.
- `Format` handles practice/qualifying, finals, validation, presets, and advanced settings. In Basic mode, endurance hides most qualifying/finals fields until Advanced is opened. - `Format` handles practice/qualifying, finals, validation, presets, and advanced settings. In Basic mode, endurance hides most qualifying/finals fields until Advanced is opened.
- `Generation` is kept separate so actions like qualifying generation, reseeding, finals generation, and bump-up do not clutter the format form. - `Generation` is kept separate so actions like qualifying generation, reseeding, finals generation, and bump-up do not clutter the format form.
- `Live / results` is where grid, standings, print, PDF, and finals matrix are used once the structure is ready. - `Live / results` is where grid, standings, print, PDF, and finals matrix are used once the structure is ready.

View File

@@ -42,6 +42,9 @@ RC timing app med sponsor-eventflöde (delade bilar/transpondrar mellan olika he
- ett presetstyrt sammanfattningskort som växlar mellan endurance- och vanligt race-fokus - ett presetstyrt sammanfattningskort som växlar mellan endurance- och vanligt race-fokus
- statusbrickor för `Setup`, `Format`, `Generering` och `Live / resultat` - statusbrickor för `Setup`, `Format`, `Generering` och `Live / resultat`
- sammanfattningsvarningar när deltagare, sessioner, lag eller varvfönster saknas - sammanfattningsvarningar när deltagare, sessioner, lag eller varvfönster saknas
- klickbara sammanfattningsvarningar som hoppar till rätt del av Hantera
- klickbara stegkort som fungerar som snabbnavigering i `Hantera`
- kompaktare stegkort med levande räknare för t.ex. förare, lag, sessioner och aktiv/resultat-status
- separata `Race actions` för generering, reseeding och bump-up - separata `Race actions` för generering, reseeding och bump-up
- omgjord inbyggd `Guide` med översiktskort och tvåkolumnslayout - omgjord inbyggd `Guide` med översiktskort och tvåkolumnslayout
- välj exakt vilka förare som är med i racet - välj exakt vilka förare som är med i racet
@@ -134,7 +137,7 @@ RC timing app med sponsor-eventflöde (delade bilar/transpondrar mellan olika he
- `Format` - `Format`
- `Generering` - `Generering`
- `Live / resultat` - `Live / resultat`
- `Setup` används för racedeltagare och lag. - `Setup` används för racedeltagare och lag. Stegkorten högst upp fungerar också som snabbnavigering mellan de fyra blocken.
- `Format` används för practice/kval, finaler, validering, presets och avancerade val. I Grundläge döljer endurance de flesta kval-/finalfält tills du öppnar Avancerat. - `Format` används för practice/kval, finaler, validering, presets och avancerade val. I Grundläge döljer endurance de flesta kval-/finalfält tills du öppnar Avancerat.
- `Generering` ligger separat så knappar för kval, reseeding, finaler och bump-up inte blandas ihop med raceformatet. - `Generering` ligger separat så knappar för kval, reseeding, finaler och bump-up inte blandas ihop med raceformatet.
- `Live / resultat` är där grid, standings, print, PDF och finalmatris används när upplägget väl är klart. - `Live / resultat` är där grid, standings, print, PDF och finalmatris används när upplägget väl är klart.

View File

@@ -213,7 +213,14 @@ const TRANSLATIONS = {
"events.summary_warning_no_finals": "Inga finalsessioner finns ännu.", "events.summary_warning_no_finals": "Inga finalsessioner finns ännu.",
"events.summary_warning_no_teams": "Endurance/Team Race saknar lag. Lägg upp lag i Setup innan körning.", "events.summary_warning_no_teams": "Endurance/Team Race saknar lag. Lägg upp lag i Setup innan körning.",
"events.summary_warning_invalid_lap_window": "Min/max varvtid ser fel ut. Max måste vara större än min.", "events.summary_warning_invalid_lap_window": "Min/max varvtid ser fel ut. Max måste vara större än min.",
"events.detail_drivers": "förare",
"events.detail_teams": "lag",
"events.detail_sessions": "sessioner",
"events.detail_results": "resultat",
"events.detail_active": "aktiva",
"guide.manage_steps_6": "6. Statusbrickorna visar om varje steg är klart, väntar eller behöver åtgärdas. Sammanfattningen till höger visar också varningar om deltagare, lag eller sessioner saknas.", "guide.manage_steps_6": "6. Statusbrickorna visar om varje steg är klart, väntar eller behöver åtgärdas. Sammanfattningen till höger visar också varningar om deltagare, lag eller sessioner saknas.",
"guide.manage_steps_7": "7. Klicka på en varning i sammanfattningen för att hoppa direkt till rätt sektion i Hantera i stället för att leta manuellt.",
"guide.manage_steps_8": "8. Stegkorten högst upp fungerar också som snabbhopp. Klicka på Setup, Format, Generering eller Live / resultat för att gå direkt till rätt block.",
"guide.race_wizard_title": "Create Race Wizard", "guide.race_wizard_title": "Create Race Wizard",
"guide.race_wizard_1": "1. Börja i Race Setup och använd wizarden när du skapar ett nytt race, i stället för att bygga allt direkt i Hantera.", "guide.race_wizard_1": "1. Börja i Race Setup och använd wizarden när du skapar ett nytt race, i stället för att bygga allt direkt i Hantera.",
"guide.race_wizard_2": "2. Steg 1 sätter namn, datum, klass och preset. Presetet fyller rimliga standardvärden innan du finjusterar något.", "guide.race_wizard_2": "2. Steg 1 sätter namn, datum, klass och preset. Presetet fyller rimliga standardvärden innan du finjusterar något.",
@@ -940,7 +947,14 @@ const TRANSLATIONS = {
"events.summary_warning_no_finals": "No finals exist yet.", "events.summary_warning_no_finals": "No finals exist yet.",
"events.summary_warning_no_teams": "Endurance/Team Race has no teams yet. Add teams in Setup before running.", "events.summary_warning_no_teams": "Endurance/Team Race has no teams yet. Add teams in Setup before running.",
"events.summary_warning_invalid_lap_window": "The min/max lap window looks invalid. Max must be greater than min.", "events.summary_warning_invalid_lap_window": "The min/max lap window looks invalid. Max must be greater than min.",
"events.detail_drivers": "drivers",
"events.detail_teams": "teams",
"events.detail_sessions": "sessions",
"events.detail_results": "results",
"events.detail_active": "active",
"guide.manage_steps_6": "6. The status badges show whether each step is complete, pending, or needs action. The summary on the right also warns when participants, teams, or sessions are missing.", "guide.manage_steps_6": "6. The status badges show whether each step is complete, pending, or needs action. The summary on the right also warns when participants, teams, or sessions are missing.",
"guide.manage_steps_7": "7. Click a warning in the summary to jump straight to the matching Manage section instead of hunting for it manually.",
"guide.manage_steps_8": "8. The step cards at the top also work as quick jumps. Click Setup, Format, Generation or Live / results to move straight to the right block.",
"guide.race_wizard_title": "Create Race Wizard", "guide.race_wizard_title": "Create Race Wizard",
"guide.race_wizard_1": "1. Start in Race Setup and use the wizard when creating a new race instead of building everything directly in Manage.", "guide.race_wizard_1": "1. Start in Race Setup and use the wizard when creating a new race instead of building everything directly in Manage.",
"guide.race_wizard_2": "2. Step 1 sets name, date, class and preset. The preset fills sensible defaults before you fine-tune anything.", "guide.race_wizard_2": "2. Step 1 sets name, date, class and preset. The preset fills sensible defaults before you fine-tune anything.",
@@ -3779,7 +3793,7 @@ function renderEventManager(eventId) {
} }
eventManageArea.innerHTML = ` eventManageArea.innerHTML = `
<section class="panel"> <section class="panel" id="manage-session-plan">
<div class="panel-header"> <div class="panel-header">
<h3>${t("events.manage_title")}: ${escapeHtml(event.name)}</h3> <h3>${t("events.manage_title")}: ${escapeHtml(event.name)}</h3>
</div> </div>
@@ -3926,27 +3940,31 @@ function renderEventManager(eventId) {
? ` ? `
<section class="panel mt-16"> <section class="panel mt-16">
<div class="panel-body manage-step-grid"> <div class="panel-body manage-step-grid">
<article class="manage-step-card manage-step-card-${manageStatuses?.setup || "pending"}"> <article class="manage-step-card manage-step-card-${manageStatuses?.setup?.status || "pending"} manage-step-card-link" data-target="manage-session-plan" role="button" tabindex="0">
<div class="manage-step-head"><strong>${t("events.manage_step_setup")}</strong>${renderManageStatusBadge(manageStatuses?.setup || "pending")}</div> <div class="manage-step-head"><strong>${t("events.manage_step_setup")}</strong>${renderManageStatusBadge(manageStatuses?.setup?.status || "pending")}</div>
<div class="manage-step-meta">${escapeHtml(manageStatuses?.setup?.detail || "")}</div>
<p>${t("events.manage_step_setup_hint")}</p> <p>${t("events.manage_step_setup_hint")}</p>
</article> </article>
<article class="manage-step-card manage-step-card-${manageStatuses?.format || "pending"}"> <article class="manage-step-card manage-step-card-${manageStatuses?.format?.status || "pending"} manage-step-card-link" data-target="manage-format" role="button" tabindex="0">
<div class="manage-step-head"><strong>${t("events.manage_step_format")}</strong>${renderManageStatusBadge(manageStatuses?.format || "pending")}</div> <div class="manage-step-head"><strong>${t("events.manage_step_format")}</strong>${renderManageStatusBadge(manageStatuses?.format?.status || "pending")}</div>
<div class="manage-step-meta">${escapeHtml(manageStatuses?.format?.detail || "")}</div>
<p>${t("events.manage_step_format_hint")}</p> <p>${t("events.manage_step_format_hint")}</p>
</article> </article>
<article class="manage-step-card manage-step-card-${manageStatuses?.generation || "pending"}"> <article class="manage-step-card manage-step-card-${manageStatuses?.generation?.status || "pending"} manage-step-card-link" data-target="manage-generation" role="button" tabindex="0">
<div class="manage-step-head"><strong>${t("events.manage_step_generate")}</strong>${renderManageStatusBadge(manageStatuses?.generation || "pending")}</div> <div class="manage-step-head"><strong>${t("events.manage_step_generate")}</strong>${renderManageStatusBadge(manageStatuses?.generation?.status || "pending")}</div>
<div class="manage-step-meta">${escapeHtml(manageStatuses?.generation?.detail || "")}</div>
<p>${t("events.manage_step_generate_hint")}</p> <p>${t("events.manage_step_generate_hint")}</p>
</article> </article>
<article class="manage-step-card manage-step-card-${manageStatuses?.live || "pending"}"> <article class="manage-step-card manage-step-card-${manageStatuses?.live?.status || "pending"} manage-step-card-link" data-target="manage-live-results" role="button" tabindex="0">
<div class="manage-step-head"><strong>${t("events.manage_step_live")}</strong>${renderManageStatusBadge(manageStatuses?.live || "pending")}</div> <div class="manage-step-head"><strong>${t("events.manage_step_live")}</strong>${renderManageStatusBadge(manageStatuses?.live?.status || "pending")}</div>
<div class="manage-step-meta">${escapeHtml(manageStatuses?.live?.detail || "")}</div>
<p>${t("events.manage_step_live_hint")}</p> <p>${t("events.manage_step_live_hint")}</p>
</article> </article>
</div> </div>
</section> </section>
<div class="race-stage-grid mt-16"> <div class="race-stage-grid mt-16">
<section class="panel"> <section class="panel" id="manage-setup-participants">
<div class="panel-header panel-header-with-pill"> <div class="panel-header panel-header-with-pill">
<h3>${t("events.select_participants")}</h3> <h3>${t("events.select_participants")}</h3>
<span class="pill">${selectedParticipantCount}</span> <span class="pill">${selectedParticipantCount}</span>
@@ -3974,7 +3992,7 @@ function renderEventManager(eventId) {
</div> </div>
</section> </section>
<section class="panel"> <section class="panel" id="manage-setup-teams">
<div class="panel-header panel-header-with-pill"> <div class="panel-header panel-header-with-pill">
<h3>${t("events.teams")}</h3> <h3>${t("events.teams")}</h3>
<span class="pill">${raceTeams.length}</span> <span class="pill">${raceTeams.length}</span>
@@ -4059,7 +4077,7 @@ function renderEventManager(eventId) {
</section> </section>
</div> </div>
<section class="panel mt-16"> <section class="panel mt-16" id="manage-format">
<div class="panel-header"><h3>${t("events.race_format")}</h3></div> <div class="panel-header"><h3>${t("events.race_format")}</h3></div>
<div class="panel-body"> <div class="panel-body">
<p>${t("events.race_format_intro")}</p> <p>${t("events.race_format_intro")}</p>
@@ -4295,7 +4313,7 @@ function renderEventManager(eventId) {
<div class="race-summary-warnings"> <div class="race-summary-warnings">
<strong>${t("events.summary_warnings_title")}</strong> <strong>${t("events.summary_warnings_title")}</strong>
<ul> <ul>
${raceSummaryWarnings.map((warning) => `<li>${escapeHtml(warning)}</li>`).join("")} ${raceSummaryWarnings.map((warning) => `<li><button class="summary-warning-link" type="button" data-target="${escapeHtml(warning.target)}">${escapeHtml(warning.message)}</button></li>`).join("")}
</ul> </ul>
</div> </div>
` : ""} ` : ""}
@@ -4314,7 +4332,7 @@ function renderEventManager(eventId) {
</div> </div>
</aside> </aside>
</div> </div>
<div class="panel-body race-actions-panel"> <div class="panel-body race-actions-panel" id="manage-generation">
<div class="panel-header panel-header-inline"><h3>${t("events.race_actions_title")}</h3></div> <div class="panel-header panel-header-inline"><h3>${t("events.race_actions_title")}</h3></div>
<p class="hint">${t("events.race_actions_hint")}</p> <p class="hint">${t("events.race_actions_hint")}</p>
<div class="actions mt-16"> <div class="actions mt-16">
@@ -4328,14 +4346,14 @@ function renderEventManager(eventId) {
</div> </div>
</section> </section>
<section class="panel mt-16"> <section class="panel mt-16" id="manage-live-grid">
<div class="panel-header"><h3>${t("events.grid_editor")}</h3></div> <div class="panel-header"><h3>${t("events.grid_editor")}</h3></div>
<div class="panel-body"> <div class="panel-body">
${renderGridEditor(selectedGridSession)} ${renderGridEditor(selectedGridSession)}
</div> </div>
</section> </section>
<section class="panel mt-16"> <section class="panel mt-16" id="manage-live-results">
<div class="panel-header"><h3>${t("events.practice_standings")}</h3></div> <div class="panel-header"><h3>${t("events.practice_standings")}</h3></div>
<div class="panel-body"> <div class="panel-body">
${renderRaceStandingsTable(buildPracticeStandings(event), t("events.no_practice_results"))} ${renderRaceStandingsTable(buildPracticeStandings(event), t("events.no_practice_results"))}
@@ -4483,6 +4501,27 @@ function renderEventManager(eventId) {
} }
`; `;
const bindManageJump = (node) => {
const triggerJump = () => {
const targetId = node.getAttribute("data-target") || "";
const target = targetId ? document.getElementById(targetId) : null;
if (target) {
target.scrollIntoView({ behavior: "smooth", block: "start" });
}
};
node.addEventListener("click", triggerJump);
node.addEventListener("keydown", (event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
triggerJump();
}
});
};
eventManageArea.querySelectorAll(".summary-warning-link, .manage-step-card-link").forEach((node) => {
bindManageJump(node);
});
document.getElementById("eventBrandingForm")?.addEventListener("submit", (e) => { document.getElementById("eventBrandingForm")?.addEventListener("submit", (e) => {
e.preventDefault(); e.preventDefault();
const form = new FormData(e.currentTarget); const form = new FormData(e.currentTarget);
@@ -5820,7 +5859,7 @@ function renderGuide() {
<div class="guide-section-grid mt-16"> <div class="guide-section-grid mt-16">
${renderGuidePanel("guide.race_title", ["guide.race_1", "guide.race_2", "guide.race_3", "guide.race_4", "guide.race_4a", "guide.race_5", "guide.race_6", "guide.race_7", "guide.race_8", "guide.race_9", "guide.race_10"])} ${renderGuidePanel("guide.race_title", ["guide.race_1", "guide.race_2", "guide.race_3", "guide.race_4", "guide.race_4a", "guide.race_5", "guide.race_6", "guide.race_7", "guide.race_8", "guide.race_9", "guide.race_10"])}
${renderGuidePanel("guide.manage_steps_title", ["guide.manage_steps_1", "guide.manage_steps_2", "guide.manage_steps_3", "guide.manage_steps_4", "guide.manage_steps_5", "guide.manage_steps_6"])} ${renderGuidePanel("guide.manage_steps_title", ["guide.manage_steps_1", "guide.manage_steps_2", "guide.manage_steps_3", "guide.manage_steps_4", "guide.manage_steps_5", "guide.manage_steps_6", "guide.manage_steps_7", "guide.manage_steps_8"])}
</div> </div>
<div class="guide-section-grid mt-16"> <div class="guide-section-grid mt-16">
@@ -7781,24 +7820,24 @@ function getRaceSummaryWarnings(event, sessions, raceDrivers, raceTeams, selecte
const hasFinals = sessions.some((session) => session.type === "final"); const hasFinals = sessions.some((session) => session.type === "final");
const hasAnySession = sessions.length > 0; const hasAnySession = sessions.length > 0;
if (participantCount <= 0) { if (participantCount <= 0) {
warnings.push(t("events.summary_warning_no_participants")); warnings.push({ message: t("events.summary_warning_no_participants"), target: "manage-setup-participants" });
} }
if (!hasAnySession) { if (!hasAnySession) {
warnings.push(t("events.summary_warning_no_sessions")); warnings.push({ message: t("events.summary_warning_no_sessions"), target: "manage-session-plan" });
} }
if (!isEndurancePreset && !hasQualifying) { if (!isEndurancePreset && !hasQualifying) {
warnings.push(t("events.summary_warning_no_qualifying")); warnings.push({ message: t("events.summary_warning_no_qualifying"), target: "manage-generation" });
} }
if (!isEndurancePreset && !hasFinals) { if (!isEndurancePreset && !hasFinals) {
warnings.push(t("events.summary_warning_no_finals")); warnings.push({ message: t("events.summary_warning_no_finals"), target: "manage-generation" });
} }
if (isEndurancePreset && raceTeams.length <= 0) { if (isEndurancePreset && raceTeams.length <= 0) {
warnings.push(t("events.summary_warning_no_teams")); warnings.push({ message: t("events.summary_warning_no_teams"), target: "manage-setup-teams" });
} }
const minLapMs = Math.max(0, Number(cfg.minLapMs || 0) || 0); const minLapMs = Math.max(0, Number(cfg.minLapMs || 0) || 0);
const maxLapMs = Math.max(0, Number(cfg.maxLapMs || 0) || 0); const maxLapMs = Math.max(0, Number(cfg.maxLapMs || 0) || 0);
if (maxLapMs > 0 && maxLapMs <= minLapMs) { if (maxLapMs > 0 && maxLapMs <= minLapMs) {
warnings.push(t("events.summary_warning_invalid_lap_window")); warnings.push({ message: t("events.summary_warning_invalid_lap_window"), target: "manage-format" });
} }
return warnings; return warnings;
} }
@@ -7807,14 +7846,19 @@ function getRaceManageStatuses(event, sessions, raceDrivers, raceTeams, selected
const cfg = event.raceConfig || {}; const cfg = event.raceConfig || {};
const participantCount = cfg.participantsConfigured ? (cfg.driverIds || []).length : raceDrivers.length; const participantCount = cfg.participantsConfigured ? (cfg.driverIds || []).length : raceDrivers.length;
const isEndurancePreset = selectedPreset?.id === "endurance"; const isEndurancePreset = selectedPreset?.id === "endurance";
const hasQualifying = sessions.some((session) => session.type === "qualification"); const practiceCount = sessions.filter((session) => ["practice", "free_practice", "open_practice"].includes(session.type)).length;
const hasFinals = sessions.some((session) => session.type === "final"); const qualCount = sessions.filter((session) => session.type === "qualification").length;
const hasTeamRace = sessions.some((session) => session.type === "team_race"); const finalCount = sessions.filter((session) => session.type === "final").length;
const hasStartedSession = sessions.some((session) => ["running", "finished", "stopped"].includes(String(session.status || "").toLowerCase()) || session.startedAt); const teamCount = sessions.filter((session) => session.type === "team_race").length;
const hasQualifying = qualCount > 0;
const hasFinals = finalCount > 0;
const hasTeamRace = teamCount > 0;
const startedCount = sessions.filter((session) => ["running", "finished", "stopped"].includes(String(session.status || "").toLowerCase()) || session.startedAt).length;
const practiceRows = buildPracticeStandings(event); const practiceRows = buildPracticeStandings(event);
const qualifyingRows = buildQualifyingStandings(event); const qualifyingRows = buildQualifyingStandings(event);
const finalRows = buildFinalStandings(event); const finalRows = buildFinalStandings(event);
const lapWindowValid = Math.max(0, Number(cfg.maxLapMs || 0) || 0) > Math.max(0, Number(cfg.minLapMs || 0) || 0); const lapWindowValid = Math.max(0, Number(cfg.maxLapMs || 0) || 0) > Math.max(0, Number(cfg.minLapMs || 0) || 0);
const resultSourceCount = [practiceRows, qualifyingRows, finalRows].filter((rows) => rows.length > 0).length;
const setupStatus = participantCount > 0 && (!isEndurancePreset || raceTeams.length > 0) const setupStatus = participantCount > 0 && (!isEndurancePreset || raceTeams.length > 0)
? "complete" ? "complete"
@@ -7827,17 +7871,27 @@ function getRaceManageStatuses(event, sessions, raceDrivers, raceTeams, selected
const generationReady = isEndurancePreset ? hasTeamRace : hasQualifying || hasFinals; const generationReady = isEndurancePreset ? hasTeamRace : hasQualifying || hasFinals;
const generationStatus = generationReady ? "complete" : sessions.length > 0 ? "attention" : "pending"; const generationStatus = generationReady ? "complete" : sessions.length > 0 ? "attention" : "pending";
const liveStatus = hasStartedSession || practiceRows.length > 0 || qualifyingRows.length > 0 || finalRows.length > 0 const liveStatus = startedCount > 0 || resultSourceCount > 0 ? "complete" : generationReady ? "pending" : "pending";
? "complete"
: generationReady
? "pending"
: "pending";
return { return {
setup: setupStatus, setup: {
format: formatStatus, status: setupStatus,
generation: generationStatus, detail: `${participantCount} ${t("events.detail_drivers")} · ${raceTeams.length} ${t("events.detail_teams")}`,
live: liveStatus, },
format: {
status: formatStatus,
detail: `${selectedPreset?.label || t("events.preset_custom")} · ${((cfg.minLapMs || 0) / 1000).toFixed(1)} / ${((cfg.maxLapMs || 0) / 1000).toFixed(1)}s`,
},
generation: {
status: generationStatus,
detail: isEndurancePreset
? `P ${practiceCount} · T ${teamCount} · ${sessions.length} ${t("events.detail_sessions")}`
: `Q ${qualCount} · F ${finalCount} · ${sessions.length} ${t("events.detail_sessions")}`,
},
live: {
status: liveStatus,
detail: `${startedCount} ${t("events.detail_active")} · ${resultSourceCount} ${t("events.detail_results")}`,
},
}; };
} }

View File

@@ -178,7 +178,7 @@ body {
.topbar-right { .topbar-right {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 8px;
} }
.lang-wrap { .lang-wrap {
@@ -364,12 +364,12 @@ body {
.wizard-step, .wizard-step,
.manage-step-card { .manage-step-card {
padding: 12px; padding: 10px;
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 12px; border-radius: 12px;
background: rgba(255, 255, 255, 0.025); background: rgba(255, 255, 255, 0.025);
display: grid; display: grid;
gap: 6px; gap: 5px;
} }
.manage-step-head { .manage-step-head {
@@ -408,14 +408,35 @@ body {
.manage-step-card p { .manage-step-card p {
margin: 0; margin: 0;
color: var(--muted); color: var(--muted);
line-height: 1.45; line-height: 1.38;
}
.manage-step-meta {
font-size: 0.78rem;
color: var(--muted);
}
.manage-step-card-link {
cursor: pointer;
transition: border-color 0.2s ease, transform 0.2s ease, background 0.2s ease;
}
.manage-step-card-link:hover {
border-color: rgba(225, 6, 0, 0.32);
transform: translateY(-1px);
}
.manage-step-card-link:focus-visible {
outline: none;
border-color: rgba(225, 6, 0, 0.52);
box-shadow: 0 0 0 3px rgba(225, 6, 0, 0.12);
} }
.status-chip { .status-chip {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
min-width: 82px; min-width: 72px;
padding: 4px 8px; padding: 4px 8px;
border-radius: 999px; border-radius: 999px;
font-size: 0.72rem; font-size: 0.72rem;
@@ -471,13 +492,13 @@ body {
.race-stage-grid { .race-stage-grid {
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr); grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr);
gap: 14px; gap: 12px;
} }
.race-setup-shell { .race-setup-shell {
display: grid; display: grid;
grid-template-columns: minmax(0, 1.75fr) minmax(260px, 0.95fr); grid-template-columns: minmax(0, 1.75fr) minmax(250px, 0.9fr);
gap: 14px; gap: 12px;
align-items: start; align-items: start;
} }
@@ -591,6 +612,21 @@ body {
gap: 6px; gap: 6px;
} }
.summary-warning-link {
display: inline;
padding: 0;
border: 0;
background: transparent;
color: #ffd391;
text-align: left;
cursor: pointer;
font: inherit;
}
.summary-warning-link:hover {
text-decoration: underline;
}
.panel-header-inline { .panel-header-inline {
padding: 0; padding: 0;
border: 0; border: 0;