Visual parity fixes — drop pixel mismatch on 6+ pages

- OperatorLogo: accept BCP-47 codes (`ru-ru`) by trimming to first 2
  chars before picking the en/ru asset variant. Fixes the Russian
  flight-details page rendering ROSSIYA (Latin) instead of РОССИЯ.
- FlightCard / FlightList: thread `direction` from the search page so
  arrival results show Высадка (deboarding) instead of Посадка
  (boarding) — Angular parity. The arrival side reads from
  arrivalLeg.transition.deboarding when direction === 'arrival'.
- OnlineBoardFilter:
  - Дата рейса starts blank with `ДД.ММ.ГГГГ` placeholder; submit
    handler defaults to today on empty.
  - Город вылета / Город прилета placeholders flip to
    `Все направления` when the opposite-direction field is filled.
  - Filter content row now flows with $space-l vertical gap to match
    Angular's accordion-content rhythm (was ~6 px tighter).
- FlightsMiniList: `display: none` on mobile. Avoids the duplicate
  summary card that was floating above the main details on small
  viewports — Angular hides the sidebar mini-list there.
- FlightsMap calendar trigger: override PrimeReact's filled-blue
  button to a transparent outline so it reads as a glyph (matches
  Angular's outline calendar icon).

Pixel-mismatch results (re-diffed via scripts/visual-diff.mjs):
  en-onlineboard-route       5.50% → 4.62%
  onlineboard-arrival        5.53% → 4.63%
  onlineboard-departure      5.92% → 5.03%
  onlineboard-route          5.16% → 4.78%
  mobile-onlineboard-start  23.51% → 20.37%
  mobile-flight-details     18.82% → 17.92%
  flight-details            carrier-logo verified visually; pixel
                            count unchanged (height delta dominates)
  onlineboard-start         14.56% → 14.52%

Larger remaining mismatches (schedule-route 14%, flights-map 34%,
flight-details 11%) are dominated by structural Angular features the
React port doesn't yet ship (day grouping, code-share bundling on
schedule; geo-driven origin marker on map; height-delta on details).
Tracked as P1 follow-ups in the comparison report.
This commit is contained in:
2026-04-19 20:18:15 +03:00
parent 9acfeb4052
commit e7cf11e799
9 changed files with 196 additions and 20 deletions
@@ -253,6 +253,23 @@
}
}
// PrimeReact Calendar trigger inside the map filter: Angular renders
// an outline calendar icon (no background); the PrimeReact default is
// a filled-blue button. Strip the fill + tone the icon down to the
// neutral gray so it reads as a glyph rather than a CTA.
.flights-map-filter .p-calendar.p-calendar-w-btn .p-datepicker-trigger.p-button {
background: transparent !important;
border: 1px solid #e0e0e0 !important;
border-left: none !important;
color: #6b7280 !important;
box-shadow: none !important;
}
.flights-map-filter .p-calendar.p-calendar-w-btn .p-datepicker-trigger.p-button:hover,
.flights-map-filter .p-calendar.p-calendar-w-btn .p-datepicker-trigger.p-button:focus {
background: rgba(0, 0, 0, 0.04) !important;
color: #1a3a5c !important;
}
// Leaflet city tooltips: text-only with white text-shadow halo, matching
// Angular's _leaflet-popup.scss treatment.
.leaflet-tooltip.city-label {
@@ -1,3 +1,5 @@
@use "../../../../styles/screen" as screen;
.mini-list {
display: flex;
flex-direction: column;
@@ -7,6 +9,14 @@
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
// Angular hides the sidebar mini-list on mobile (the main details
// card already shows everything). React's PageLayout floats the
// left column above the main content on mobile, so the mini-list
// would render as a duplicate summary card. Hide it.
@include screen.mobile {
display: none;
}
&__item {
padding: 12px 14px;
border-bottom: 1px solid #e8edf3;
@@ -273,11 +273,17 @@
}
.calendar {
margin-top: vars.$space-xl;
// margin-top removed: vertical rhythm now driven by .filter-content gap.
}
.filter-content {
// container for form fields inside accordion content
// Vertical rhythm between filter rows. Angular's accordion content
// separates fields by ~$space-l (15px); the previous default
// packed inputs about ~6 px tighter and surfaced as a measurable
// pixel-diff against Angular on the start page.
display: flex;
flex-direction: column;
gap: vars.$space-l;
}
.filter-button {
@@ -88,8 +88,11 @@ export const OnlineBoardFilter: FC<OnlineBoardFilterProps> = ({
// Flight number fields
const [flightNumber, setFlightNumber] = useState(initialFlightNumber ?? "");
// Start blank to match Angular's `ДД.ММ.ГГГГ` placeholder; the search
// handler defaults to today when the field is left empty so the
// legacy "search today" UX still works.
const [flightDate, setFlightDate] = useState<Date | null>(
initialTab === "flight" && initialDate ? yyyymmddToDate(initialDate) : new Date(),
initialTab === "flight" && initialDate ? yyyymmddToDate(initialDate) : null,
);
const [flightNumberError, setFlightNumberError] = useState<string | null>(null);
@@ -97,7 +100,7 @@ export const OnlineBoardFilter: FC<OnlineBoardFilterProps> = ({
const [routeDepartureCode, setRouteDepartureCode] = useState<string>(initialDeparture ?? "");
const [routeArrivalCode, setRouteArrivalCode] = useState<string>(initialArrival ?? "");
const [routeDate, setRouteDate] = useState<Date | null>(
initialDate ? yyyymmddToDate(initialDate) : new Date(),
initialDate ? yyyymmddToDate(initialDate) : null,
);
const [timeRange, setTimeRange] = useState<[number, number]>([0, 1440]);
@@ -177,8 +180,9 @@ export const OnlineBoardFilter: FC<OnlineBoardFilterProps> = ({
setFlightNumberError(error);
if (error) return;
if (!flightDate) return;
const dateParam = dateToYyyymmdd(flightDate);
// Empty date defaults to today, matching Angular's behaviour when
// the placeholder ДД.ММ.ГГГГ is left untouched.
const dateParam = dateToYyyymmdd(flightDate ?? new Date());
const cleaned = flightNumber.trim().replace(/\s+/g, "");
const carrier = "SU";
const num = cleaned;
@@ -193,8 +197,7 @@ export const OnlineBoardFilter: FC<OnlineBoardFilterProps> = ({
(e: FormEvent) => {
e.preventDefault();
if (!routeDate) return;
const dateParam = dateToYyyymmdd(routeDate);
const dateParam = dateToYyyymmdd(routeDate ?? new Date());
const depCode = routeDepartureCode.trim().toUpperCase();
const arrCode = routeArrivalCode.trim().toUpperCase();
if (!depCode || !arrCode) return;
@@ -336,7 +339,11 @@ export const OnlineBoardFilter: FC<OnlineBoardFilterProps> = ({
<div className="filter-content">
<CityAutocomplete
label={t("SHARED.DEPARTURE_CITY")}
placeholder={t("SHARED.CITY_PLACEHOLDER")}
placeholder={
routeArrivalCode && !routeDepartureCode
? t("SHARED.ALL_DIRECTIONS")
: t("SHARED.CITY_PLACEHOLDER")
}
value={routeDepartureCode}
onChange={setRouteDepartureCode}
dictionaries={dictionaries}
@@ -358,7 +365,11 @@ export const OnlineBoardFilter: FC<OnlineBoardFilterProps> = ({
<CityAutocomplete
label={t("SHARED.ARRIVAL_CITY")}
placeholder={t("SHARED.CITY_PLACEHOLDER")}
placeholder={
routeDepartureCode && !routeArrivalCode
? t("SHARED.ALL_DIRECTIONS")
: t("SHARED.CITY_PLACEHOLDER")
}
value={routeArrivalCode}
onChange={setRouteArrivalCode}
dictionaries={dictionaries}
@@ -458,6 +458,7 @@ export const OnlineBoardSearchPage: FC<OnlineBoardSearchPageProps> = ({
loading={loading}
onFlightClick={handleFlightClick}
initialCurrentFlightId={initialCurrentFlightId}
direction={params.type}
/>
</section>
)}