Compare commits

...

3 Commits

Author SHA1 Message Date
AmanTahiliani
21b29bec6a fix(command-center): hide the season strip's native scrollbar
The strip sits inside a left/right edge mask, so the always-on macOS
scrollbar rendered as a half-faded light slab across the dark panel and
claimed an 11px gutter. Hide it: the strip already opens centred on the
current GP and the fades mark the overflow.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 23:50:54 -04:00
AmanTahiliani
fbae59c431 feat(command-center): level the dashboard columns
Stretch the two dashboard columns to a shared row height so whichever side
is taller sets the baseline and the other fills down to it, instead of the
main panel stopping short of the sidebar.

Raise the paddock briefing preview to ten headlines and let the strip take
the column's leftover height, so the extra space is filled with news rather
than empty panel; anything past the fold scrolls in place.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 12:56:01 -04:00
AmanTahiliani
3397352835 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>
2026-08-08 12:48:15 -04:00
7 changed files with 106 additions and 8 deletions

View File

@@ -29,6 +29,8 @@ const SOURCE_DISPLAY: Record<string, string> = {
'f1-youtube': 'F1 YouTube',
}
const PREVIEW_LIMIT = 10
export function PaddockBriefing() {
const now = useMemo(() => new Date(), [])
@@ -76,7 +78,9 @@ export function PaddockBriefing() {
const sinceLabel = sinceLastLabel(meetings, now)
const unreadCount = news?.filter((i) => !i.read_at).length ?? 0
const preview = sinceItems.length > 0 ? sinceItems.slice(0, 5) : (tagged.slice(0, 5))
// The strip flexes to the bottom of the dashboard column, so offer more than
// fits and let the overflow scroll rather than leaving the column short.
const preview = sinceItems.length > 0 ? sinceItems.slice(0, PREVIEW_LIMIT) : tagged.slice(0, PREVIEW_LIMIT)
return (
<section className="cc-briefing" data-testid="paddock-briefing">

View File

@@ -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

View File

@@ -1865,13 +1865,22 @@ a { color: inherit; text-decoration: none; }
display: grid;
grid-template-columns: minmax(0, 65fr) minmax(0, 35fr);
gap: var(--s6);
align-items: start;
/* Stretch, not start: whichever column is taller sets the row height and the
other fills down to it, so the two never end on different baselines. */
align-items: stretch;
}
.cc-dashboard-panel {
flex: 1;
min-height: 0;
}
}
.cc-dashboard-main {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
}
.cc-dashboard-panel {
@@ -3357,7 +3366,15 @@ 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);
flex: 1;
min-height: 0;
}
.cc-briefing .sec-header {
margin-bottom: 0;
}
.briefing-state {
@@ -3396,6 +3413,14 @@ a { color: inherit; text-decoration: none; }
display: flex;
flex-direction: column;
gap: 1px;
/* Takes the column's leftover height; extra headlines scroll instead of
pushing the panel past the sidebar. */
flex: 1;
min-height: 0;
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.14) transparent;
}
.cc-brief-item {
@@ -4835,7 +4860,23 @@ a { color: inherit; text-decoration: none; }
gap: 0;
padding: var(--s3) 0 var(--s4);
overflow-x: auto;
scrollbar-width: thin;
overscroll-behavior-x: contain;
/* No native scrollbar: it sits inside the edge mask below, so it renders as a
half-faded light slab on the dark panel. The strip opens centred on the
current GP and the fades mark the overflow, so nothing is lost by hiding it. */
scrollbar-width: none;
-ms-overflow-style: none;
}
.cc-calendar-grid::-webkit-scrollbar {
display: none;
}
/* 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 {

View File

@@ -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