mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-22 06:56:35 +00:00
ba70ad28b8
Arabic runs the app right to left, but margins, paddings, insets, text alignment, borders and corner radii were written for the left side, so icons sat against the wrong edge of their labels, indents and badges pointed the wrong way and amount columns aligned left. They now use the logical forms (ms-, ps-, start-, text-start, marginInlineStart, insetInlineEnd and their CSS equivalents), which are the very same sides in a left to right language. Geometry stays physical on purpose: maps, positions measured from the pointer or a bounding box, time axes, sliders, transforms and print output. Amounts drawn as several spans keep their order in a right to left row, the time picker keeps hours before minutes, and two switches whose thumb slid out of its track in Arabic now move the right way. lint:rtl counts what is still physical per file against a baseline that may only shrink (1512 uses in 370 files before, 319 in 113 now), and CI runs it, so new code reaches for the logical forms.
138 lines
6.4 KiB
TypeScript
138 lines
6.4 KiB
TypeScript
import { AlertTriangle, Route } from 'lucide-react'
|
|
import { useTranslation } from '../../i18n'
|
|
import { MapTogglePill } from './MapTogglePill'
|
|
import { routeModeIcon } from '../Planner/routeModes'
|
|
import { formatDistance } from '../../utils/units'
|
|
import type { TripRouteOverview as Overview } from './useTripRouteOverview'
|
|
import type { DistanceUnit } from '../../types'
|
|
|
|
/**
|
|
* Toggle for the whole-trip route overview (#1736). Same frosted shell as the other
|
|
* map controls, so it lines up with the compass and the layer switcher.
|
|
*/
|
|
export function TripRouteOverviewPill({ active, onToggle }: { active: boolean; onToggle: () => void }) {
|
|
const { t } = useTranslation()
|
|
return (
|
|
<MapTogglePill
|
|
active={active}
|
|
onToggle={onToggle}
|
|
label={active ? t('map.overview.hide') : t('map.overview.show')}
|
|
testId="trip-overview-pill"
|
|
icon={<Route size={17} strokeWidth={2} />}
|
|
/>
|
|
)
|
|
}
|
|
|
|
/**
|
|
* The trip's stages and its total distance, read off the overview the map is drawing.
|
|
*
|
|
* One component for both shells: the phone and the desktop place it differently but the
|
|
* rows are the same rows, and a second copy of them is a second thing to keep in step.
|
|
*/
|
|
export function TripRouteOverviewPanel({ overview, unit, selectedDayId, onSelectDay, maxWidth = 320 }: {
|
|
overview: Overview
|
|
unit: DistanceUnit
|
|
selectedDayId?: number | null
|
|
onSelectDay?: (dayId: number) => void
|
|
/** How wide the card may grow before day names start to ellipsize. */
|
|
maxWidth?: number
|
|
}) {
|
|
const { t } = useTranslation()
|
|
if (!overview.days.length) return null
|
|
// Only once the round is over: while it runs every leg still waiting is unrouted too,
|
|
// and the ellipsis on the total already says the number is growing.
|
|
const unrouted = overview.loading ? 0 : (overview.unroutedLegs ?? 0)
|
|
|
|
return (
|
|
<div
|
|
data-testid="trip-overview-panel"
|
|
style={{
|
|
pointerEvents: 'auto', borderRadius: 14, overflow: 'hidden',
|
|
background: 'var(--sidebar-bg)',
|
|
backdropFilter: 'blur(20px) saturate(180%)',
|
|
WebkitBackdropFilter: 'blur(20px) saturate(180%)',
|
|
boxShadow: 'var(--sidebar-shadow, 0 4px 16px rgba(0,0,0,0.14))',
|
|
// Sized to its rows rather than to the space available: a trip of short day
|
|
// names left a band of empty card over the map, which on a phone is most of
|
|
// what there is to look at.
|
|
width: 'fit-content', maxWidth,
|
|
}}
|
|
>
|
|
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 12, padding: '10px 14px 8px' }}>
|
|
<span className="text-content-muted" style={{ fontSize: 'calc(11px * var(--fs-scale-caption, 1))', textTransform: 'uppercase', letterSpacing: 0.4 }}>
|
|
{t('map.overview.total')}
|
|
</span>
|
|
{/* Same tier as the label beside it: the two read as one line rather than as a
|
|
heading with a number stuck under it. */}
|
|
<span className="text-content" style={{ fontSize: 'calc(11px * var(--fs-scale-caption, 1))', fontWeight: 600 }}>
|
|
{formatDistance(overview.totalDistance / 1000, unit)}
|
|
{/* Still routing: the number is a partial sum, and saying so beats a total
|
|
that silently grows while you read it. */}
|
|
{overview.loading && <span className="text-content-muted" style={{ fontWeight: 400 }}>{' '}…</span>}
|
|
</span>
|
|
</div>
|
|
{/* A leg the router refused stays a straight line and adds nothing to the sum, so
|
|
the total is short by however much road those legs are. Said in words rather than
|
|
left to the reader to notice, because the number is what a fuel estimate starts from. */}
|
|
{unrouted > 0 && (
|
|
<div
|
|
data-testid="trip-overview-unrouted"
|
|
className="text-warning"
|
|
style={{ display: 'flex', alignItems: 'flex-start', gap: 6, padding: '0 14px 8px', fontSize: 'calc(11px * var(--fs-scale-caption, 1))' }}
|
|
>
|
|
<AlertTriangle size={12} style={{ flexShrink: 0, marginTop: 1 }} aria-hidden />
|
|
<span>{t('map.overview.unrouted', { count: unrouted })}</span>
|
|
</div>
|
|
)}
|
|
<div style={{ maxHeight: 220, overflowY: 'auto', borderTop: '1px solid var(--border-primary)' }}>
|
|
{overview.days.map(day => {
|
|
const label = day.title || t('dayplan.dayN', { n: day.dayNumber })
|
|
const dayUnrouted = overview.loading ? 0 : (day.unroutedLegs ?? 0)
|
|
const row = (
|
|
<>
|
|
<span style={{ width: 8, height: 8, borderRadius: 999, background: day.color.line, flexShrink: 0 }} aria-hidden />
|
|
<span className="text-content" style={{ flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
{label}
|
|
</span>
|
|
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 3, flexShrink: 0 }} className="text-content-muted">
|
|
{day.modes.map(mode => {
|
|
const Icon = routeModeIcon(mode)
|
|
return <Icon key={mode} size={12} strokeWidth={2} aria-hidden />
|
|
})}
|
|
</span>
|
|
{/* The day's own share of the shortfall, so the reader knows which figure
|
|
to distrust rather than only that one of them is off. */}
|
|
{dayUnrouted > 0 && (
|
|
<AlertTriangle
|
|
size={12}
|
|
className="text-warning"
|
|
style={{ flexShrink: 0 }}
|
|
role="img"
|
|
aria-label={t('map.overview.dayUnrouted', { count: dayUnrouted })}
|
|
/>
|
|
)}
|
|
<span className="text-content-muted" style={{ flexShrink: 0, fontVariantNumeric: 'tabular-nums' }}>
|
|
{formatDistance(day.distance / 1000, unit)}
|
|
</span>
|
|
</>
|
|
)
|
|
const style = {
|
|
display: 'flex', alignItems: 'center', gap: 8, width: '100%',
|
|
padding: '7px 14px', textAlign: 'start' as const,
|
|
fontSize: 'calc(12px * var(--fs-scale-body, 1))',
|
|
background: day.dayId === selectedDayId ? 'var(--bg-hover)' : 'transparent',
|
|
border: 'none',
|
|
}
|
|
return onSelectDay ? (
|
|
<button key={day.dayId} type="button" onClick={() => onSelectDay(day.dayId)} style={{ ...style, cursor: 'pointer' }}>
|
|
{row}
|
|
</button>
|
|
) : (
|
|
<div key={day.dayId} style={style}>{row}</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|