mirror of
https://github.com/AmanTahiliani/box-box.git
synced 2026-10-06 10:41:57 -04:00
feat(command-center): balance columns and open the calendar on the current GP
Move the paddock briefing under the season calendar in the main column so the sidebar (championship + data note) stops running longer than the left side of the dashboard. Open the season strip scrolled to the round in focus rather than at round one, so the relevant GP is centred on load. Re-centring repeats while the strip reflows (font load, resize) and stops as soon as the reader scrolls it themselves; the edge fade only applies when the strip actually overflows. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useQueries, useQuery } from '@tanstack/react-query'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import {
|
||||
@@ -56,6 +56,9 @@ function pickAnalysisSession(weekend: Weekend | undefined): WeekendSession | und
|
||||
|
||||
export function CommandCenterPage() {
|
||||
const [now, setNow] = useState(() => Date.now())
|
||||
const calendarScrollRef = useRef<HTMLDivElement | null>(null)
|
||||
const focusCardRef = useRef<HTMLAnchorElement | null>(null)
|
||||
const railTakenOver = useRef(false)
|
||||
|
||||
const seasonsQuery = useQuery({
|
||||
queryKey: ['seasons'],
|
||||
@@ -198,6 +201,56 @@ export function CommandCenterPage() {
|
||||
? `${nextSession.session_name} is next on the timetable. The weekend rail keeps every session and its local analysis in reach.`
|
||||
: 'The weekend timetable is ready to explore.'
|
||||
|
||||
// Park the calendar rail on the GP in focus so it lands centred, instead of
|
||||
// making the reader scroll past every round that has already happened.
|
||||
const focusMeetingKey = focusMeeting?.meeting_key
|
||||
useEffect(() => {
|
||||
railTakenOver.current = false
|
||||
}, [focusMeetingKey])
|
||||
|
||||
useEffect(() => {
|
||||
const rail = calendarScrollRef.current
|
||||
const card = focusCardRef.current
|
||||
if (!rail || !card) return
|
||||
|
||||
const sync = () => {
|
||||
const overflows = rail.scrollWidth > rail.clientWidth + 1
|
||||
rail.classList.toggle('is-scrollable', overflows)
|
||||
if (!overflows || railTakenOver.current) return
|
||||
const railBox = rail.getBoundingClientRect()
|
||||
const cardBox = card.getBoundingClientRect()
|
||||
const offset = rail.scrollLeft + (cardBox.left - railBox.left)
|
||||
const target = offset - (rail.clientWidth - cardBox.width) / 2
|
||||
rail.scrollLeft = Math.max(0, Math.min(target, rail.scrollWidth - rail.clientWidth))
|
||||
}
|
||||
|
||||
sync()
|
||||
|
||||
const takeOver = () => {
|
||||
railTakenOver.current = true
|
||||
}
|
||||
rail.addEventListener('wheel', takeOver, { passive: true })
|
||||
rail.addEventListener('touchstart', takeOver, { passive: true })
|
||||
rail.addEventListener('pointerdown', takeOver)
|
||||
rail.addEventListener('keydown', takeOver)
|
||||
|
||||
// The strip reflows as fonts load and as the column width changes, so keep
|
||||
// re-centring until the reader takes the rail over themselves.
|
||||
let observer: ResizeObserver | undefined
|
||||
if (typeof ResizeObserver !== 'undefined') {
|
||||
observer = new ResizeObserver(sync)
|
||||
observer.observe(rail)
|
||||
}
|
||||
|
||||
return () => {
|
||||
observer?.disconnect()
|
||||
rail.removeEventListener('wheel', takeOver)
|
||||
rail.removeEventListener('touchstart', takeOver)
|
||||
rail.removeEventListener('pointerdown', takeOver)
|
||||
rail.removeEventListener('keydown', takeOver)
|
||||
}
|
||||
}, [focusMeetingKey, seasonMeetings.length])
|
||||
|
||||
if (seasonsQuery.isLoading) {
|
||||
return <div className="page loading-state">loading command center…</div>
|
||||
}
|
||||
@@ -317,7 +370,7 @@ export function CommandCenterPage() {
|
||||
{seasonMeetings.length} rounds · {meetingStats.full}/{meetingStats.total || 0} local
|
||||
</span>
|
||||
</div>
|
||||
<div className="cc-calendar-grid" role="list">
|
||||
<div className="cc-calendar-grid" role="list" ref={calendarScrollRef} data-testid="cc-calendar-rail">
|
||||
{seasonMeetings.map((meeting, index) => {
|
||||
const weekend = weekendsByKey.get(meeting.meeting_key)
|
||||
const target = pickAnalysisSession(weekend)
|
||||
@@ -327,6 +380,7 @@ export function CommandCenterPage() {
|
||||
return (
|
||||
<Link
|
||||
key={meeting.meeting_key}
|
||||
ref={status === 'focus' ? focusCardRef : undefined}
|
||||
to="/race-hub"
|
||||
search={targetKey ? { session_key: targetKey } : {}}
|
||||
className={`cc-calendar-card cc-calendar-${status}`}
|
||||
@@ -359,6 +413,7 @@ export function CommandCenterPage() {
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
<PaddockBriefing />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -456,7 +511,6 @@ export function CommandCenterPage() {
|
||||
</section>
|
||||
)}
|
||||
|
||||
<PaddockBriefing />
|
||||
<div className="cc-data-note mono" data-testid="cc-data-note">
|
||||
<span className="cc-cov-dot cc-cov-local" aria-hidden="true" />
|
||||
{meetingStats.full}/{meetingStats.total || 0} locally complete · availability is shown per session
|
||||
|
||||
@@ -3357,7 +3357,13 @@ a { color: inherit; text-decoration: none; }
|
||||
|
||||
/* ── Command Center — Paddock Briefing preview strip ── */
|
||||
.cc-briefing {
|
||||
margin-top: var(--s7);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--s3);
|
||||
}
|
||||
|
||||
.cc-briefing .sec-header {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.briefing-state {
|
||||
@@ -4835,9 +4841,17 @@ a { color: inherit; text-decoration: none; }
|
||||
gap: 0;
|
||||
padding: var(--s3) 0 var(--s4);
|
||||
overflow-x: auto;
|
||||
overscroll-behavior-x: contain;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
/* The strip opens centred on the GP in focus, so rounds run off both edges —
|
||||
fade them out rather than slicing a card in half. */
|
||||
.cc-calendar-grid.is-scrollable {
|
||||
-webkit-mask-image: linear-gradient(to right, transparent 0, #000 28px, #000 calc(100% - 28px), transparent 100%);
|
||||
mask-image: linear-gradient(to right, transparent 0, #000 28px, #000 calc(100% - 28px), transparent 100%);
|
||||
}
|
||||
|
||||
.cc-calendar-card {
|
||||
min-width: 112px;
|
||||
min-height: 138px;
|
||||
|
||||
@@ -156,7 +156,6 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--s2);
|
||||
margin-bottom: var(--s3);
|
||||
font-size: 11px;
|
||||
color: var(--text-3);
|
||||
font-family: var(--f-mono);
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 180 KiB After Width: | Height: | Size: 188 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 123 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 160 KiB |
Reference in New Issue
Block a user