From 149b87fd479756da9f5ac452f1f0e2e7425c812d Mon Sep 17 00:00:00 2001 From: Maurice Date: Wed, 9 Sep 2026 14:44:10 +0200 Subject: [PATCH] feat(roadtrip): give the day boundary its own band, and flag a late pause Three things the rail was quietly getting wrong at a day boundary. A stop reached later than the time set on it showed a warning only when it carried a number. The schedule restarts its chain at any anchor, whatever kind of stop holds it, so the finding was computed for a fuel or charging halt too and then thrown away. Both branches now draw it, from one component, and it sits in the badge row beside the stay rather than on a line of its own. The midnight crossing was a small pill floating in the middle of the rail, which read as a badge belonging to the stop underneath. It is now a band across the full width that breaks the dashed line instead of letting it run through, with the mascot asleep in the gap. Both its animations stop under prefers-reduced-motion. And the stay dialog said 'leave 01:00' for a departure the chain places on the next day, because the clock wraps modulo 24 h. It now carries the same day marker the rail has always shown beside such a time. --- .../Roadtrip/RoadtripSidebar.test.tsx | 17 ++++ .../components/Roadtrip/RoadtripSidebar.tsx | 96 ++++++++++++++----- .../Roadtrip/RoadtripStayModal.test.tsx | 17 ++++ .../components/Roadtrip/RoadtripStayModal.tsx | 15 ++- client/src/index.css | 20 ++++ 5 files changed, 142 insertions(+), 23 deletions(-) diff --git a/client/src/components/Roadtrip/RoadtripSidebar.test.tsx b/client/src/components/Roadtrip/RoadtripSidebar.test.tsx index 715746b74..aef384da9 100644 --- a/client/src/components/Roadtrip/RoadtripSidebar.test.tsx +++ b/client/src/components/Roadtrip/RoadtripSidebar.test.tsx @@ -302,6 +302,23 @@ describe('RoadtripSidebar', () => { expect(onEditStay).toHaveBeenCalledWith({ placeId: 20, name: 'Berlin', minutes: null, arrival: null }) }) + it('FE-ROADTRIP-SIDEBAR-034: a pause reached late says so, the same as a numbered stop', () => { + // The schedule restarts its chain at any pinned time, whatever kind of stop carries + // it, so it computes the finding for a fuel halt too. The rail used to hand `late` + // only to the numbered branch and throw the pause's own away. + const stops = [ + stop({ assignmentId: 1, name: 'Hamburg' }), + stop({ assignmentId: 2, name: 'Aral Autohof', stopType: 'fuel', dwellMinutes: 15 }), + ] + const schedule = { + entries: stops.map(() => ({ arrival: '09:00', departure: '09:15', anchored: true, dayOffset: 0 })), + warnings: [{ index: 1, code: 'late' as const, minutes: 45 }], + } + wrap() + + expect(screen.getByLabelText(/45/)).toBeInTheDocument() + }) + it('FE-ROADTRIP-SIDEBAR-033: without the right to edit, a stop with no stay shows nothing', () => { const stops = [ stop({ assignmentId: 1, name: 'Hamburg', dwellMinutes: 90 }), diff --git a/client/src/components/Roadtrip/RoadtripSidebar.tsx b/client/src/components/Roadtrip/RoadtripSidebar.tsx index c68e90026..5b8906147 100644 --- a/client/src/components/Roadtrip/RoadtripSidebar.tsx +++ b/client/src/components/Roadtrip/RoadtripSidebar.tsx @@ -1,9 +1,10 @@ import React, { useState } from 'react' import { - CarFront, Footprints, Bike, Zap, AlertTriangle, Moon, + CarFront, Footprints, Bike, Zap, AlertTriangle, ParkingSquare, Shuffle, Fuel, Clock, Spline, type LucideIcon, } from 'lucide-react' +import MDancingTrek from '../../mobile/components/MDancingTrek' import { useTranslation } from '../../i18n/TranslationContext' import { Tooltip } from '../shared/Tooltip' import { useSettingsStore } from '../../store/settingsStore' @@ -377,9 +378,12 @@ function DriveBand({ leg, onAskAlternatives, alternativesOpen }: { * the trip is actually for. Its own icon on one flat disc: three kinds of pause that all * mean "we are still driving", and the icon is what tells them apart. */ -function ServiceStop({ stop, entry, driveFindings, selected, onSelect, onEditStay, onPickKind }: { +function ServiceStop({ stop, entry, late, driveFindings, selected, onSelect, onEditStay, onPickKind }: { stop: RoadtripStop entry: ScheduleEntry | undefined + /** How late the drive reaches a time pinned on this pause, the same finding a numbered + * stop shows. A fuel or charging halt can carry a pinned time like anything else. */ + late: ScheduleWarning | undefined /** Findings about the drive that ARRIVES here. A charging halt is a stop like any other * as far as the tank is concerned, so it carries them the same way a numbered one does. */ driveFindings?: ScheduleWarning[] @@ -459,6 +463,7 @@ function ServiceStop({ stop, entry, driveFindings, selected, onSelect, onEditSta {spurWorthLabelling(stop.offRoadMeters) ? : null} {(driveFindings ?? []).map(w => )} + {entry?.arrival ? : null} @@ -566,20 +571,42 @@ function DriveFindingBadge({ warning }: { warning: ScheduleWarning }): React.Rea * The schedule has carried this since it was written and nothing ever drew it: a day * running past midnight showed "02:30" as if it were tonight. */ +/** + * Where the chain crosses midnight. + * + * The one band in the rail that breaks the line instead of hanging off it, and the only + * thing here that moves: a night is not a place and not a drive, it is the day running + * out, and the rail should look like it stops. So it spans the marker column too, the + * dashes end above it and start again below, and the mascot dozes in the gap with three + * z's drifting off it. + * + * The ground is mixed rather than the soft info token: that token is translucent in the + * dark theme, and the mascot cuts its eyes out in the colour behind it, which would show + * the body through them. Mixing against the card keeps it opaque in both themes. + */ function OvernightBreak(): React.ReactElement { const { t } = useTranslation() + const night = 'color-mix(in srgb, var(--info) 13%, var(--bg-card))' return ( -
- - - +
+ - {t('roadtrip.warn.overnight')} + {/* Three z's on one baseline, each starting a third of the loop after the last, so + one is always on its way up. Purely decorative: the word beside it already says + what this band is. */} + + z + z + z +
) } @@ -651,8 +678,6 @@ function Stop({ stop, number, entry, late, driveFindings, selected, continues, s onEditStay?: () => void }): React.ReactElement { const { t } = useTranslation() - const distanceUnit = useSettingsStore(s => s.settings.distance_unit) - const lateText = late ? t('roadtrip.warn.late', { minutes: late.minutes ?? 0 }) : null return (
) : null} diff --git a/client/src/index.css b/client/src/index.css index a0ec5300e..9d09c5205 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -523,6 +523,20 @@ input[type="number"], input[type="time"], input[type="date"], input[type="dateti will-change: transform, opacity; } +/* Roadtrip: die Zzz am Tageswechsel im Streckenstrang. Drei Buchstaben auf einer + Grundlinie, jeder ein Drittel des Laufs nach dem vorigen, damit immer einer + unterwegs ist. Rein dekorativ — das Wort daneben sagt schon, was das Band ist. */ +@keyframes trek-doze { + 0% { opacity: 0; transform: translate(0, 3px) scale(0.7); } + 22% { opacity: 0.9; } + 100% { opacity: 0; transform: translate(6px, -10px) scale(1.15); } +} +.trek-doze > * { + animation: trek-doze 3s ease-out infinite; +} +.trek-doze > *:nth-child(2) { animation-delay: 1s; } +.trek-doze > *:nth-child(3) { animation-delay: 2s; } + /* Stagger-Helpers für Listen — Enter-Animation mit Offset */ @keyframes trek-fade-up { from { opacity: 0; transform: translateY(6px); } @@ -551,6 +565,12 @@ input[type="number"], input[type="time"], input[type="date"], input[type="dateti animation: none; background: var(--bg-tertiary); } + /* Die Zzz stehen still statt zu treiben — der Tageswechsel bleibt lesbar. */ + .trek-doze > * { + animation: none; + opacity: 0.6; + transform: none; + } button:not(:disabled):not([data-no-press]):active, [role="button"]:not([aria-disabled="true"]):not([data-no-press]):active, [data-press]:active {