feat: enhance departure status display and loading state handling
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,3 +6,4 @@ dist/
|
|||||||
test-results/
|
test-results/
|
||||||
playwright-report/
|
playwright-report/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.impeccable/
|
||||||
|
|||||||
17
src/App.tsx
17
src/App.tsx
@@ -52,7 +52,11 @@ function ScheduledTime({ departure, now, className = '', showDay = true }: { dep
|
|||||||
function DepartureStatus({ departure, now, large = false }: { departure: DisplayDeparture; now: number; large?: boolean }) {
|
function DepartureStatus({ departure, now, large = false }: { departure: DisplayDeparture; now: number; large?: boolean }) {
|
||||||
return <div className={`departure-status ${large ? 'departure-status-large' : ''}`}>
|
return <div className={`departure-status ${large ? 'departure-status-large' : ''}`}>
|
||||||
{departure.status === 'cancelled' ? <span className="cancelled-label"><Close size={22} />Отменён</span> : <Countdown departure={departure} now={now} large={large} />}
|
{departure.status === 'cancelled' ? <span className="cancelled-label"><Close size={22} />Отменён</span> : <Countdown departure={departure} now={now} large={large} />}
|
||||||
{departure.status === 'notice' && <span className="departure-change">Есть изменения</span>}
|
{/* The large countdown must not read as a live arrival estimate. One line
|
||||||
|
with the change flag keeps the hero's height unchanged. */}
|
||||||
|
{large && departure.status !== 'cancelled' ? <span className="countdown-meta"><span className="countdown-source" aria-hidden="true">по расписанию</span>
|
||||||
|
{departure.status === 'notice' && <span className="departure-change">Есть изменения</span>}</span>
|
||||||
|
: departure.status === 'notice' && <span className="departure-change">Есть изменения</span>}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,6 +68,7 @@ export function DeparturesScreen({ data, departures, now, alerts, loading, rowCo
|
|||||||
const next = settings.showNextSameRoute ? nextSameRoute(departures, first) : undefined;
|
const next = settings.showNextSameRoute ? nextSameRoute(departures, first) : undefined;
|
||||||
const waiting = longWait(settings, first, now);
|
const waiting = longWait(settings, first, now);
|
||||||
const reading = readability(settings);
|
const reading = readability(settings);
|
||||||
|
const lastTrip = Boolean(first && first.id === lastTripId);
|
||||||
const table = useRef<HTMLDivElement>(null);
|
const table = useRef<HTMLDivElement>(null);
|
||||||
const [tableHeight, setTableHeight] = useState(360);
|
const [tableHeight, setTableHeight] = useState(360);
|
||||||
const [page, setPage] = useState(0);
|
const [page, setPage] = useState(0);
|
||||||
@@ -83,7 +88,7 @@ export function DeparturesScreen({ data, departures, now, alerts, loading, rowCo
|
|||||||
return () => clearInterval(timer);
|
return () => clearInterval(timer);
|
||||||
}, [paused, visible.pages, reading.calm]);
|
}, [paused, visible.pages, reading.calm]);
|
||||||
return <div className="departure-layout">
|
return <div className="departure-layout">
|
||||||
<section className="departure-content" data-long-wait={waiting} data-next={Boolean(next)} aria-label="Ближайшие рейсы">
|
<section className="departure-content" data-long-wait={waiting} data-next={Boolean(next)} data-last-trip={lastTrip} aria-label="Ближайшие рейсы">
|
||||||
<div className="section-heading"><span className="eyebrow">БЛИЖАЙШИЙ РЕЙС</span><span className="source-label"><Clock /> По расписанию{data?.mode === 'demo' ? ' · демо' : ''}</span></div>
|
<div className="section-heading"><span className="eyebrow">БЛИЖАЙШИЙ РЕЙС</span><span className="source-label"><Clock /> По расписанию{data?.mode === 'demo' ? ' · демо' : ''}</span></div>
|
||||||
{first ? <div className="hero-departure" data-long-wait={waiting}>
|
{first ? <div className="hero-departure" data-long-wait={waiting}>
|
||||||
<RouteBadge number={first.routeNumber} large />
|
<RouteBadge number={first.routeNumber} large />
|
||||||
@@ -94,7 +99,7 @@ export function DeparturesScreen({ data, departures, now, alerts, loading, rowCo
|
|||||||
{waiting ? <div className="long-wait-time"><span>{localDate(Date.parse(first.scheduledAt)) > localDate(now) ? 'Завтра в' : 'Сегодня в'}</span><ScheduledTime departure={first} now={now} showDay={false} />{first.status === 'notice' && <span className="departure-change">Есть изменения</span>}</div> : <DepartureStatus departure={first} now={now} large />}
|
{waiting ? <div className="long-wait-time"><span>{localDate(Date.parse(first.scheduledAt)) > localDate(now) ? 'Завтра в' : 'Сегодня в'}</span><ScheduledTime departure={first} now={now} showDay={false} />{first.status === 'notice' && <span className="departure-change">Есть изменения</span>}</div> : <DepartureStatus departure={first} now={now} large />}
|
||||||
</div> : <div className="empty-departures" data-cancelled={departures.length > 0}><BusFront size={46} /><h2>{departures.length ? 'Ближайшие рейсы отменены' : loading ? 'Получаем расписание' : !data ? 'Расписание недоступно' : fresh ? 'На сегодня рейсов больше нет' : 'Сохранённые рейсы закончились'}</h2>
|
</div> : <div className="empty-departures" data-cancelled={departures.length > 0}><BusFront size={46} /><h2>{departures.length ? 'Ближайшие рейсы отменены' : loading ? 'Получаем расписание' : !data ? 'Расписание недоступно' : fresh ? 'На сегодня рейсов больше нет' : 'Сохранённые рейсы закончились'}</h2>
|
||||||
<p>{departures.length ? 'Подробности — в информации о движении' : loading ? 'Ближайший транспорт появится здесь' : error === 'api_not_configured' ? 'Подключите RideBus API или включите деморежим в настройках' : !data || !fresh ? 'Ожидаем подключения к RideBus' : 'Новые рейсы появятся с началом движения'}</p></div>}
|
<p>{departures.length ? 'Подробности — в информации о движении' : loading ? 'Ближайший транспорт появится здесь' : error === 'api_not_configured' ? 'Подключите RideBus API или включите деморежим в настройках' : !data || !fresh ? 'Ожидаем подключения к RideBus' : 'Новые рейсы появятся с началом движения'}</p></div>}
|
||||||
{first && first.id === lastTripId && <LastTripNotice number={first.routeNumber} />}
|
{first && lastTrip && <LastTripNotice number={first.routeNumber} />}
|
||||||
<div className="next-heading"><span className="eyebrow">{!first && departures.length ? 'ОТМЕНЁННЫЕ РЕЙСЫ' : remaining.some(item => item.status === 'cancelled') ? 'ДРУГИЕ РЕЙСЫ' : 'СЛЕДОМ ПРИБУДУТ'}</span>{visible.pages > 1 ? <span className="page-indicator">{visible.start + 1}{visible.items.length > 1 ? `–${visible.start + visible.items.length}` : ''} из {visible.count}</span> : <ArrowDown size={22} />}</div>
|
<div className="next-heading"><span className="eyebrow">{!first && departures.length ? 'ОТМЕНЁННЫЕ РЕЙСЫ' : remaining.some(item => item.status === 'cancelled') ? 'ДРУГИЕ РЕЙСЫ' : 'СЛЕДОМ ПРИБУДУТ'}</span>{visible.pages > 1 ? <span className="page-indicator">{visible.start + 1}{visible.items.length > 1 ? `–${visible.start + visible.items.length}` : ''} из {visible.count}</span> : <ArrowDown size={22} />}</div>
|
||||||
<div ref={table} className="departure-table" data-rows={rowCount} data-paged={visible.pages > 1} style={{ '--rows': rows, '--row-height': `${tableHeight / rows}px` } as CSSProperties}>
|
<div ref={table} className="departure-table" data-rows={rowCount} data-paged={visible.pages > 1} style={{ '--rows': rows, '--row-height': `${tableHeight / rows}px` } as CSSProperties}>
|
||||||
{visible.items.map((departure, index) => <article className="departure-row" data-status={departure.status} key={`position-${index}`}>
|
{visible.items.map((departure, index) => <article className="departure-row" data-status={departure.status} key={`position-${index}`}>
|
||||||
@@ -309,7 +314,9 @@ function BoardApplication({ management }: { management?: { state: ManagedState;
|
|||||||
<div className="stop-meta">{direction && <div className="stop-direction"><ArrowRight /><span>{direction}</span></div>}<WeatherSummary data={weather.data} now={now} failed={weather.failed} /></div></div>
|
<div className="stop-meta">{direction && <div className="stop-direction"><ArrowRight /><span>{direction}</span></div>}<WeatherSummary data={weather.data} now={now} failed={weather.failed} /></div></div>
|
||||||
<div className="header-right"><div className="brand" aria-label="RideBus"><span className="official-mark" aria-hidden="true" dangerouslySetInnerHTML={{ __html: logo }} /><span className="brand-word">ridebus</span></div><div className="clock" aria-label={`Текущее время ${night.active ? `${timeParts[0]}:${timeParts[1]}` : clockFormat.format(now)}`}><NumericText value={`${timeParts[0]}:${timeParts[1]}`} />{!night.active && <small><NumericText value={timeParts[2]} /></small>}</div><div className="today">{dateFormat.format(now)}</div></div>
|
<div className="header-right"><div className="brand" aria-label="RideBus"><span className="official-mark" aria-hidden="true" dangerouslySetInnerHTML={{ __html: logo }} /><span className="brand-word">ridebus</span></div><div className="clock" aria-label={`Текущее время ${night.active ? `${timeParts[0]}:${timeParts[1]}` : clockFormat.format(now)}`}><NumericText value={`${timeParts[0]}:${timeParts[1]}`} />{!night.active && <small><NumericText value={timeParts[2]} /></small>}</div><div className="today">{dateFormat.format(now)}</div></div>
|
||||||
</header>
|
</header>
|
||||||
{!fresh && !settings.demo && <div className="offline-banner" role="status"><WifiOff /><span>{data ? `Нет свежих данных. Сохранённое расписание · обновлено в ${updateLabel}` : loading ? 'Подключаемся к RideBus…' : 'Не удалось загрузить расписание. Проверьте подключение.'}</span><button onClick={refresh} aria-label="Повторить подключение"><Refresh /></button></div>}
|
{!fresh && !settings.demo && (loading
|
||||||
|
? <div className="offline-banner" data-state="loading" role="status"><Refresh /><span>{data ? 'Обновляем расписание…' : 'Подключаемся к RideBus…'}</span></div>
|
||||||
|
: <div className="offline-banner" role="status"><WifiOff /><span>{data ? `Нет свежих данных. Сохранённое расписание · обновлено в ${updateLabel}` : 'Не удалось загрузить расписание. Проверьте подключение.'}</span><button onClick={refresh} aria-label="Повторить подключение"><Refresh /></button></div>)}
|
||||||
{management?.failed && <div className="managed-offline" role="status">Нет связи с управлением · сохранённые настройки</div>}
|
{management?.failed && <div className="managed-offline" role="status">Нет связи с управлением · сохранённые настройки</div>}
|
||||||
<main className="scene-container"><div className="scene" key={scene} data-scene={scene}>
|
<main className="scene-container"><div className="scene" key={scene} data-scene={scene}>
|
||||||
{night.active && night.first && <NightScreen first={night.first} completedToday={night.completedToday} now={now} routeFiltered={Boolean(settings.routeFilter)} alerts={alerts} paused={paused || settingsOpen} />}
|
{night.active && night.first && <NightScreen first={night.first} completedToday={night.completedToday} now={now} routeFiltered={Boolean(settings.routeFilter)} alerts={alerts} paused={paused || settingsOpen} />}
|
||||||
@@ -321,7 +328,7 @@ function BoardApplication({ management }: { management?: { state: ManagedState;
|
|||||||
{!night.active && scene !== 'departures' && <div className="persistent-next"><span className="eyebrow">БЛИЖАЙШИЙ РЕЙС</span>{first ? <><RouteBadge number={first.routeNumber} filled /><div className="persistent-destination"><strong>{first.destination}</strong><ScheduledTime departure={first} now={now} />{guide.lastTripId === first.id && <LastTripNotice number={first.routeNumber} compact />}</div><Countdown departure={first} now={now} /></> : <span>Нет доступных рейсов</span>}</div>}
|
{!night.active && scene !== 'departures' && <div className="persistent-next"><span className="eyebrow">БЛИЖАЙШИЙ РЕЙС</span>{first ? <><RouteBadge number={first.routeNumber} filled /><div className="persistent-destination"><strong>{first.destination}</strong><ScheduledTime departure={first} now={now} />{guide.lastTripId === first.id && <LastTripNotice number={first.routeNumber} compact />}</div><Countdown departure={first} now={now} /></> : <span>Нет доступных рейсов</span>}</div>}
|
||||||
<footer className="board-footer"><div className="cycle-progress"><span style={{ transform: `scaleX(${held || paused || settingsOpen || scenes.length < 2 ? 0 : progress})` }} /></div>
|
<footer className="board-footer"><div className="cycle-progress"><span style={{ transform: `scaleX(${held || paused || settingsOpen || scenes.length < 2 ? 0 : progress})` }} /></div>
|
||||||
<div className="footer-main"><nav className="scene-navigation" aria-label="Экраны табло">{scenes.map((item, index) => <button key={item} onClick={() => goTo(item)} className={scene === item ? 'active' : ''} aria-label={`0${index + 1} ${night.active ? 'Ночной режим' : sceneNames[item]}`} title={night.active ? 'Ночной режим' : sceneNames[item]} aria-current={scene === item ? 'page' : undefined}><span className="scene-index">0{index + 1}</span><span>{night.active ? 'Ночной режим' : sceneNames[item]}</span></button>)}</nav>
|
<div className="footer-main"><nav className="scene-navigation" aria-label="Экраны табло">{scenes.map((item, index) => <button key={item} onClick={() => goTo(item)} className={scene === item ? 'active' : ''} aria-label={`0${index + 1} ${night.active ? 'Ночной режим' : sceneNames[item]}`} title={night.active ? 'Ночной режим' : sceneNames[item]} aria-current={scene === item ? 'page' : undefined}><span className="scene-index">0{index + 1}</span><span>{night.active ? 'Ночной режим' : sceneNames[item]}</span></button>)}</nav>
|
||||||
<div className="footer-status">{!fresh && !settings.demo && <WifiOff />}<span>{settings.demo ? 'Демонстрация' : held ? 'Приоритет транспорта' : `Обновлено в ${updateLabel}`}{data?.partial ? ' · часть рейсов недоступна' : ''}{offlineSaved === false ? ' · сохранение недоступно' : ''}</span></div>
|
<div className="footer-status">{!fresh && !settings.demo && !loading && <WifiOff />}<span>{settings.demo ? 'Демонстрация' : held ? 'Приоритет транспорта' : `Обновлено в ${updateLabel}`}{data?.partial ? ' · часть рейсов недоступна' : ''}{offlineSaved === false ? ' · сохранение недоступно' : ''}</span></div>
|
||||||
<div className="board-controls"><button className="icon-button" onClick={() => { setPaused(value => !value); setStartedAt(Date.now()); }} aria-label={paused ? 'Продолжить смену экранов' : 'Приостановить смену экранов'} title="Пауза · пробел">{paused ? <Play /> : <Pause />}</button><button className="icon-button" onClick={nextScene} aria-label="Следующий экран" title="Следующий экран · →"><ArrowRight /></button><button className="icon-button" onClick={() => void toggleFullscreen()} aria-label={fullscreen ? 'Выйти из полного экрана' : 'На весь экран'} title="Полный экран · F"><Fullscreen /></button><button className="icon-button" onClick={() => setSettingsOpen(true)} aria-label="Настройки табло" title="Настройки · S"><SettingsIcon /></button></div></div>
|
<div className="board-controls"><button className="icon-button" onClick={() => { setPaused(value => !value); setStartedAt(Date.now()); }} aria-label={paused ? 'Продолжить смену экранов' : 'Приостановить смену экранов'} title="Пауза · пробел">{paused ? <Play /> : <Pause />}</button><button className="icon-button" onClick={nextScene} aria-label="Следующий экран" title="Следующий экран · →"><ArrowRight /></button><button className="icon-button" onClick={() => void toggleFullscreen()} aria-label={fullscreen ? 'Выйти из полного экрана' : 'На весь экран'} title="Полный экран · F"><Fullscreen /></button><button className="icon-button" onClick={() => setSettingsOpen(true)} aria-label="Настройки табло" title="Настройки · S"><SettingsIcon /></button></div></div>
|
||||||
</footer>
|
</footer>
|
||||||
{notice && <div className="toast" role="status">{notice}</div>}
|
{notice && <div className="toast" role="status">{notice}</div>}
|
||||||
|
|||||||
@@ -62,8 +62,10 @@ export function useBoard(settings: Settings, config: Config) {
|
|||||||
const demoTimer = setInterval(() => setData(createDemo()), 60000);
|
const demoTimer = setInterval(() => setData(createDemo()), 60000);
|
||||||
return () => clearInterval(demoTimer);
|
return () => clearInterval(demoTimer);
|
||||||
}
|
}
|
||||||
|
// A saved snapshot fills the screen while the first request is still in
|
||||||
|
// flight; that is not a connection failure until the request fails.
|
||||||
readSnapshot(config.cityId, settings.stopId).then(cached => {
|
readSnapshot(config.cityId, settings.stopId).then(cached => {
|
||||||
if (!stopped && !received && cached) { setData(cached); setFailed(true); setLoading(false); setOfflineSaved(true); }
|
if (!stopped && !received && cached) { setData(cached); setOfflineSaved(true); }
|
||||||
}).catch(() => { /* Offline storage may be unavailable in private mode. */ });
|
}).catch(() => { /* Offline storage may be unavailable in private mode. */ });
|
||||||
async function load() {
|
async function load() {
|
||||||
if (running || stopped) return;
|
if (running || stopped) return;
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ h1 { font-size: clamp(30px, 3.8vw, 76px); letter-spacing: -.05em; line-height: 1
|
|||||||
.scene { width: 100%; height: 100%; min-height: 0; animation: scene-in 400ms ease-out both; }
|
.scene { width: 100%; height: 100%; min-height: 0; animation: scene-in 400ms ease-out both; }
|
||||||
.departure-layout { display: flex; flex-direction: column; height: 100%; min-height: 0; padding: clamp(12px, 1.8vh, 24px) 0; }
|
.departure-layout { display: flex; flex-direction: column; height: 100%; min-height: 0; padding: clamp(12px, 1.8vh, 24px) 0; }
|
||||||
.departure-content { flex: 1 1 0; min-height: 0; min-width: 0; display: grid; grid-template-rows: auto calc(var(--hero-height) + var(--hero-gap)) auto minmax(0, 1fr); }
|
.departure-content { flex: 1 1 0; min-height: 0; min-width: 0; display: grid; grid-template-rows: auto calc(var(--hero-height) + var(--hero-gap)) auto minmax(0, 1fr); }
|
||||||
|
/* The last-trip notice is an extra child; give it its own row so the table keeps the flexible one. */
|
||||||
|
.departure-content[data-last-trip='true'] { grid-template-rows: auto calc(var(--hero-height) + var(--hero-gap)) auto auto minmax(0, 1fr); }
|
||||||
.section-heading, .next-heading { display: flex; align-items: center; justify-content: space-between; gap: 15px; min-width: 0; }
|
.section-heading, .next-heading { display: flex; align-items: center; justify-content: space-between; gap: 15px; min-width: 0; }
|
||||||
.eyebrow { font-size: clamp(12px, 1.05vw, 21px); font-weight: 650; letter-spacing: .12em; color: var(--muted); }
|
.eyebrow { font-size: clamp(12px, 1.05vw, 21px); font-weight: 650; letter-spacing: .12em; color: var(--muted); }
|
||||||
.source-label { display: flex; align-items: center; gap: 8px; color: var(--secondary); font-size: clamp(13px, 1.15vw, 23px); white-space: nowrap; }
|
.source-label { display: flex; align-items: center; gap: 8px; color: var(--secondary); font-size: clamp(13px, 1.15vw, 23px); white-space: nowrap; }
|
||||||
@@ -78,6 +80,7 @@ h1 { font-size: clamp(30px, 3.8vw, 76px); letter-spacing: -.05em; line-height: 1
|
|||||||
.quiet-placeholder { display: none; }
|
.quiet-placeholder { display: none; }
|
||||||
.offline-banner { display: flex; align-items: center; gap: 9px; color: var(--warning); font-size: clamp(13px, 1.2vw, 22px); padding-top: 9px; flex: 0 0 auto; }
|
.offline-banner { display: flex; align-items: center; gap: 9px; color: var(--warning); font-size: clamp(13px, 1.2vw, 22px); padding-top: 9px; flex: 0 0 auto; }
|
||||||
.offline-banner button { color: inherit; border: 0; background: none; display: flex; padding: 3px; margin-left: auto; }
|
.offline-banner button { color: inherit; border: 0; background: none; display: flex; padding: 3px; margin-left: auto; }
|
||||||
|
.offline-banner[data-state='loading'] { color: var(--secondary); }
|
||||||
|
|
||||||
/* Cards switch to one panel when the actual available area is too small. */
|
/* Cards switch to one panel when the actual available area is too small. */
|
||||||
.info-screen { height: 100%; min-height: 0; display: flex; flex-direction: column; padding: 16px 0; overflow-y: auto; scrollbar-width: thin; scrollbar-color: var(--outline) var(--background); }
|
.info-screen { height: 100%; min-height: 0; display: flex; flex-direction: column; padding: 16px 0; overflow-y: auto; scrollbar-width: thin; scrollbar-color: var(--outline) var(--background); }
|
||||||
@@ -564,6 +567,9 @@ h1 { font-size: clamp(30px, 3.8vw, 76px); letter-spacing: -.05em; line-height: 1
|
|||||||
.hero-next time { color: var(--accent); display: inline-flex; align-items: baseline; gap: 6px; font-weight: 650; }
|
.hero-next time { color: var(--accent); display: inline-flex; align-items: baseline; gap: 6px; font-weight: 650; }
|
||||||
.departure-status { min-width: 0; display: flex; flex-direction: column; align-items: flex-end; justify-content: center; gap: 6px; }
|
.departure-status { min-width: 0; display: flex; flex-direction: column; align-items: flex-end; justify-content: center; gap: 6px; }
|
||||||
.departure-change { color: var(--warning); font-size: clamp(12px, calc(1.1vw * var(--reading-scale)), 20px); line-height: 1.2; }
|
.departure-change { color: var(--warning); font-size: clamp(12px, calc(1.1vw * var(--reading-scale)), 20px); line-height: 1.2; }
|
||||||
|
.countdown-meta { display: flex; flex-wrap: wrap; justify-content: flex-end; align-items: baseline; gap: 2px .7em; text-align: right; font-size: clamp(14px, calc(var(--hero-height) * .12), calc(24px * var(--reading-scale))); line-height: 1.2; }
|
||||||
|
.countdown-meta .departure-change { font-size: 1em; }
|
||||||
|
.countdown-source { color: var(--secondary); font-weight: 500; white-space: nowrap; }
|
||||||
.cancelled-label { display: inline-flex; align-items: center; gap: 8px; color: var(--warning); font-size: min(calc(26px * var(--reading-scale)), calc(var(--row-height, 96px) * .31), 5.5vw); font-weight: 650; white-space: nowrap; }
|
.cancelled-label { display: inline-flex; align-items: center; gap: 8px; color: var(--warning); font-size: min(calc(26px * var(--reading-scale)), calc(var(--row-height, 96px) * .31), 5.5vw); font-weight: 650; white-space: nowrap; }
|
||||||
.cancelled-label svg { width: .9em; height: .9em; }
|
.cancelled-label svg { width: .9em; height: .9em; }
|
||||||
.departure-row[data-status='cancelled'] .route-badge { color: var(--warning); border-color: var(--warning); }
|
.departure-row[data-status='cancelled'] .route-badge { color: var(--warning); border-color: var(--warning); }
|
||||||
@@ -597,6 +603,9 @@ h1 { font-size: clamp(30px, 3.8vw, 76px); letter-spacing: -.05em; line-height: 1
|
|||||||
.hero-next { font-size: 15px; gap: 4px 7px; margin-top: 6px; }
|
.hero-next { font-size: 15px; gap: 4px 7px; margin-top: 6px; }
|
||||||
.departure-status { gap: 4px; }
|
.departure-status { gap: 4px; }
|
||||||
.departure-change { font-size: 12px; }
|
.departure-change { font-size: 12px; }
|
||||||
|
/* Wrap within the countdown's width rather than squeezing the destination. */
|
||||||
|
.countdown-meta { font-size: 13px; width: min-content; min-width: 100%; }
|
||||||
|
.countdown-source { white-space: normal; }
|
||||||
.hero-departure[data-long-wait='true'] { grid-template-columns: auto minmax(0, 1fr); grid-template-rows: minmax(0, 1fr) auto; gap: 8px 12px; }
|
.hero-departure[data-long-wait='true'] { grid-template-columns: auto minmax(0, 1fr); grid-template-rows: minmax(0, 1fr) auto; gap: 8px 12px; }
|
||||||
.hero-departure[data-long-wait='true'] > .route-badge { grid-row: 1 / 3; width: min(calc(var(--hero-height) * .7), 110px); height: min(calc(var(--hero-height) - 10px), 160px); }
|
.hero-departure[data-long-wait='true'] > .route-badge { grid-row: 1 / 3; width: min(calc(var(--hero-height) * .7), 110px); height: min(calc(var(--hero-height) - 10px), 160px); }
|
||||||
.hero-departure[data-long-wait='true'] .transport-kind { display: none; }
|
.hero-departure[data-long-wait='true'] .transport-kind { display: none; }
|
||||||
@@ -608,6 +617,7 @@ h1 { font-size: clamp(30px, 3.8vw, 76px); letter-spacing: -.05em; line-height: 1
|
|||||||
.board[data-distance='far'] .hero-departure:not([data-long-wait='true']) > .route-badge { grid-row: 1 / 3; width: 110px; height: min(calc(var(--hero-height) - 10px), 160px); }
|
.board[data-distance='far'] .hero-departure:not([data-long-wait='true']) > .route-badge { grid-row: 1 / 3; width: 110px; height: min(calc(var(--hero-height) - 10px), 160px); }
|
||||||
.board[data-distance='far'] .hero-departure:not([data-long-wait='true']) .transport-kind { display: none; }
|
.board[data-distance='far'] .hero-departure:not([data-long-wait='true']) .transport-kind { display: none; }
|
||||||
.board[data-distance='far'] .hero-departure > .departure-status { grid-column: 2; align-items: flex-start; }
|
.board[data-distance='far'] .hero-departure > .departure-status { grid-column: 2; align-items: flex-start; }
|
||||||
|
.board[data-distance='far'] .hero-departure .countdown-meta { justify-content: flex-start; text-align: left; }
|
||||||
.board[data-distance='far'] .hero-departure .countdown-large .countdown-value { font-size: 64px; }
|
.board[data-distance='far'] .hero-departure .countdown-large .countdown-value { font-size: 64px; }
|
||||||
}
|
}
|
||||||
::view-transition { pointer-events: none; }
|
::view-transition { pointer-events: none; }
|
||||||
|
|||||||
Reference in New Issue
Block a user