feat(mobile): today marked on the day rail, and a button back to it

While a trip is running, today's chip on the phone carries a ring and a dot, and a button beside the rail jumps back to today from wherever it was scrolled, in the plan and on the map. It stays in place, dimmed, while today is open, so the rail never shifts under the thumb. (#2392)
This commit is contained in:
Maurice
2026-10-01 10:11:08 +02:00
parent 48f28b40c4
commit ba0cc331c1
29 changed files with 127 additions and 4 deletions
+32 -4
View File
@@ -1,8 +1,7 @@
import { useEffect, useRef, useState, type ComponentType, type ReactNode } from 'react'
import { findFocusDayId } from '../../../components/Planner/today'
import { findFocusDayId, findTodayDayId } from '../../../components/Planner/today'
import {
CalendarDays, ChevronDown, ChevronLeft, Download, FileDown, List, Map as MapIcon, MoreHorizontal,
FolderSync, Plane, Plus, Rows3, Route, SlidersHorizontal, Trash2, Upload,
CalendarCheck, CalendarDays, ChevronDown, ChevronLeft, Download, FileDown, FolderSync, List, Map as MapIcon, MoreHorizontal, Plane, Plus, Route, Rows3, SlidersHorizontal, Trash2, Upload,
} from 'lucide-react'
import { useTripPlanner } from '../../../pages/tripPlanner/useTripPlanner'
import { pickDockTabs } from './dockTabs'
@@ -405,6 +404,15 @@ export default function MTripShell({
const dockTabs = pickDockTabs(enabledTabIds)
const tabLabel = (id: string) => planner.TRIP_TABS.find(tab => tab.id === id)?.label ?? id
// While the trip runs, today's chip carries a ring and a button jumps back to it
// from wherever the rail was scrolled (#2392). Null outside the trip's dates.
const todayDayId = findTodayDayId(days)
const jumpToToday = () => {
if (todayDayId == null || todayDayId === planner.selectedDayId) return
if (view === 'map') selectDayOnMap(todayDayId)
else planner.handleSelectDay(todayDayId, true)
}
const onDayChipTap = (dayId: number) => {
if (dayId === planner.selectedDayId) openSheet('day', { dayId })
// In map mode a day tap fits to that day's places, draws its route and drops
@@ -483,6 +491,7 @@ export default function MTripShell({
<div className="flex flex-1 items-center gap-[2px] overflow-x-auto rounded-full border border-[color:var(--m-gbr)] bg-[color:var(--m-glass)] p-[3px] backdrop-blur-[24px] backdrop-saturate-[1.7]">
{days.map((day, idx) => {
const active = day.id === planner.selectedDayId
const isToday = day.id === todayDayId
const tint = dayTints[day.id]
return (
<button
@@ -496,11 +505,16 @@ export default function MTripShell({
// Inactive chips only — an inline background would otherwise beat
// the active chip's bg-m-act class.
style={active ? undefined : { background: dayTintBackground(tint, 'badge', '--day-tint-chip') }}
data-today={isToday || undefined}
className={`flex flex-1 items-center justify-center gap-[3px] whitespace-nowrap rounded-full px-3 py-[5px] text-center text-[0.75rem] font-semibold ${
active ? 'bg-m-act text-m-actfg shadow-[0_6px_16px_-6px_rgba(0,0,0,.4)]' : 'text-m-ink'
}`}
} ${isToday && !active ? 'ring-[1.5px] ring-inset ring-[color:var(--m-st-info)]' : ''}`}
>
{isToday && (
<span aria-hidden="true" className={`h-[5px] w-[5px] flex-none rounded-full ${active ? 'bg-m-actfg' : 'bg-[color:var(--m-st-info)]'}`} />
)}
{dayChipLabel(day, language, t('planner.dayN', { n: day.day_number ?? idx + 1 }))}
{isToday && <span className="sr-only">{t('mobileTrip.today')}</span>}
{/* Marks the second tap as "opens the day", the only day-sheet
route that also exists in map view. */}
{active && <ChevronDown size={11} strokeWidth={2.6} aria-hidden="true" className="flex-none opacity-70" />}
@@ -508,6 +522,20 @@ export default function MTripShell({
)
})}
</div>
{/* Back to today from anywhere on the rail (#2392). Mounted for the whole
running trip and dimmed while today is already open, so it never
appears under the thumb and shoves the rail aside. */}
{todayDayId != null && (
<button
type="button"
onClick={jumpToToday}
disabled={todayDayId === planner.selectedDayId}
aria-label={t('mobileTrip.jumpToToday')}
className="flex w-9 flex-none items-center justify-center rounded-full border border-[color:var(--m-gbr)] bg-[color:var(--m-glass)] text-[color:var(--m-st-info)] backdrop-blur-[24px] backdrop-saturate-[1.7] transition-opacity disabled:opacity-40"
>
<CalendarCheck size={15} strokeWidth={2.4} aria-hidden="true" />
</button>
)}
{/* Drop the day filter and show the whole trip (#2257). Map only: the
plan timeline and the road trip chain are both single-day, so there is
nothing to widen there. Stays mounted while a day is active rather than
@@ -599,6 +599,47 @@ describe('MTripShell', () => {
})
})
// #2392 — on the road the phone plan is used one day at a time, and finding today
// again meant scrolling the rail; today is marked and one tap away.
describe('today on the day rail (#2392)', () => {
const shift = (days: number) => {
const d = new Date()
d.setDate(d.getDate() + days)
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
}
const running = () => [
{ id: 21, day_number: 1, date: shift(-1) },
{ id: 22, day_number: 2, date: todayIso() },
{ id: 23, day_number: 3, date: shift(1) },
] as unknown as Day[]
const jump = () => screen.getByRole('button', { name: 'mobileTrip.jumpToToday' })
it('FE-MOB-SHELL-090: marks today and jumps back to it from another day', () => {
const { planner, container } = renderShell({ days: running(), selectedDayId: 21 } as Partial<TripPlanner>)
const marked = container.querySelectorAll('[data-today]')
expect(marked).toHaveLength(1)
expect(marked[0].className).toContain('ring-inset')
expect(marked[0]).toHaveTextContent('mobileTrip.today')
expect(jump()).toBeEnabled()
fireEvent.click(jump())
expect(planner.handleSelectDay).toHaveBeenLastCalledWith(22, true)
})
it('FE-MOB-SHELL-091: with today open the button stays, dimmed, so the rail does not move', () => {
const { container } = renderShell({ days: running(), selectedDayId: 22 } as Partial<TripPlanner>)
expect(jump()).toBeDisabled()
// The active chip keeps its own look; the dot still says which day is today.
expect(container.querySelector('[data-today]')?.className).not.toContain('ring-inset')
})
it('FE-MOB-SHELL-092: outside the trip dates there is no today and no button', () => {
const { container } = renderShell()
expect(container.querySelector('[data-today]')).toBeNull()
expect(screen.queryByRole('button', { name: 'mobileTrip.jumpToToday' })).toBeNull()
})
})
// #2257 — the map filters down to one day, and until now nothing took that
// filter back off: the chip rail can only swap one day for another, and the
// second tap on the active chip is the day sheet. Its own control, map only.
+2
View File
@@ -7,6 +7,8 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.addToDayQuestion': 'إضافة إلى يوم؟',
'mobileTrip.addTransportShort': 'النقل',
'mobileTrip.allDays': 'كل الأيام',
'mobileTrip.today': 'اليوم',
'mobileTrip.jumpToToday': 'الانتقال إلى اليوم',
'mobileTrip.assignedDays': 'الأيام المخصصة',
'mobileTrip.assignmentNotes': 'ملاحظات خاصة باليوم',
'mobileTrip.bookingsEmpty': 'لا توجد حجوزات بعد',
+2
View File
@@ -7,6 +7,8 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.addToDayQuestion': 'Günə əlavə edilsin?',
'mobileTrip.addTransportShort': 'Nəqliyyat',
'mobileTrip.allDays': 'Bütün günlər',
'mobileTrip.today': 'Bu gün',
'mobileTrip.jumpToToday': 'Bu günə keç',
'mobileTrip.assignedDays': 'Təyin edilmiş günlər',
'mobileTrip.assignmentNotes': 'Günə aid qeydlər',
'mobileTrip.bookingsEmpty': 'Hələ rezervasiya yoxdur',
+2
View File
@@ -7,6 +7,8 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.addToDayQuestion': 'Adicionar a um dia?',
'mobileTrip.addTransportShort': 'Transporte',
'mobileTrip.allDays': 'Todos os dias',
'mobileTrip.today': 'Hoje',
'mobileTrip.jumpToToday': 'Ir para hoje',
'mobileTrip.assignedDays': 'Dias atribuídos',
'mobileTrip.assignmentNotes': 'Notas específicas do dia',
'mobileTrip.bookingsEmpty': 'Nenhuma reserva ainda',
+2
View File
@@ -7,6 +7,8 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.addToDayQuestion': 'Afegeix a un dia?',
'mobileTrip.addTransportShort': 'Transport',
'mobileTrip.allDays': 'Tots els dies',
'mobileTrip.today': 'Avui',
'mobileTrip.jumpToToday': 'Ves a avui',
'mobileTrip.assignedDays': 'Dies assignats',
'mobileTrip.assignmentNotes': 'Notes específiques del dia',
'mobileTrip.bookingsEmpty': 'Encara no hi ha reserves',
+2
View File
@@ -7,6 +7,8 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.addToDayQuestion': 'Přidat do dne?',
'mobileTrip.addTransportShort': 'Doprava',
'mobileTrip.allDays': 'Všechny dny',
'mobileTrip.today': 'Dnes',
'mobileTrip.jumpToToday': 'Přejít na dnešek',
'mobileTrip.assignedDays': 'Přiřazené dny',
'mobileTrip.assignmentNotes': 'Poznámky ke dni',
'mobileTrip.bookingsEmpty': 'Zatím žádné rezervace',
+2
View File
@@ -7,6 +7,8 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.addToDayQuestion': 'Zu einem Tag hinzufügen?',
'mobileTrip.addTransportShort': 'Transport',
'mobileTrip.allDays': 'Alle Tage',
'mobileTrip.today': 'Heute',
'mobileTrip.jumpToToday': 'Zu heute springen',
'mobileTrip.assignedDays': 'Zugewiesene Tage',
'mobileTrip.assignmentNotes': 'Tagesspezifische Notizen',
'mobileTrip.bookingsEmpty': 'Noch keine Buchungen',
+2
View File
@@ -7,6 +7,8 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.addToDayQuestion': 'Add to a day?',
'mobileTrip.addTransportShort': 'Transport',
'mobileTrip.allDays': 'All Days',
'mobileTrip.today': 'Today',
'mobileTrip.jumpToToday': 'Jump to today',
'mobileTrip.assignedDays': 'Assigned Days',
'mobileTrip.assignmentNotes': 'Day-specific notes',
'mobileTrip.bookingsEmpty': 'No bookings yet',
+2
View File
@@ -7,6 +7,8 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.addToDayQuestion': '¿Añadir a un día?',
'mobileTrip.addTransportShort': 'Transporte',
'mobileTrip.allDays': 'Todos los días',
'mobileTrip.today': 'Hoy',
'mobileTrip.jumpToToday': 'Ir a hoy',
'mobileTrip.assignedDays': 'Días asignados',
'mobileTrip.assignmentNotes': 'Notas específicas del día',
'mobileTrip.bookingsEmpty': 'Aún no hay reservas',
+2
View File
@@ -7,6 +7,8 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.addToDayQuestion': 'Kas lisada päevale?',
'mobileTrip.addTransportShort': 'Transport',
'mobileTrip.allDays': 'Kõik päevad',
'mobileTrip.today': 'Täna',
'mobileTrip.jumpToToday': 'Mine tänasele päevale',
'mobileTrip.assignedDays': 'Määratud päevad',
'mobileTrip.assignmentNotes': 'Päevapõhised märkmed',
'mobileTrip.bookingsEmpty': 'Broneeringuid veel pole',
+2
View File
@@ -7,6 +7,8 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.addToDayQuestion': 'Ajouter à un jour ?',
'mobileTrip.addTransportShort': 'Transport',
'mobileTrip.allDays': 'Tous les jours',
'mobileTrip.today': "Aujourd'hui",
'mobileTrip.jumpToToday': "Aller à aujourd'hui",
'mobileTrip.assignedDays': 'Jours assignés',
'mobileTrip.assignmentNotes': 'Notes propres au jour',
'mobileTrip.bookingsEmpty': 'Aucune réservation',
+2
View File
@@ -7,6 +7,8 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.addToDayQuestion': 'Προσθήκη σε ημέρα;',
'mobileTrip.addTransportShort': 'Μεταφορά',
'mobileTrip.allDays': 'Όλες οι Ημέρες',
'mobileTrip.today': 'Σήμερα',
'mobileTrip.jumpToToday': 'Μετάβαση στο σήμερα',
'mobileTrip.assignedDays': 'Ανατεθειμένες ημέρες',
'mobileTrip.assignmentNotes': 'Σημειώσεις ημέρας',
'mobileTrip.bookingsEmpty': 'Δεν υπάρχουν κρατήσεις ακόμη',
+2
View File
@@ -7,6 +7,8 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.addToDayQuestion': 'Hozzáadás egy naphoz?',
'mobileTrip.addTransportShort': 'Közlekedés',
'mobileTrip.allDays': 'Minden nap',
'mobileTrip.today': 'Ma',
'mobileTrip.jumpToToday': 'Ugrás a mai napra',
'mobileTrip.assignedDays': 'Hozzárendelt napok',
'mobileTrip.assignmentNotes': 'Napra szóló jegyzetek',
'mobileTrip.bookingsEmpty': 'Még nincsenek foglalások',
+2
View File
@@ -7,6 +7,8 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.addToDayQuestion': 'Tambahkan ke hari?',
'mobileTrip.addTransportShort': 'Transportasi',
'mobileTrip.allDays': 'Semua Hari',
'mobileTrip.today': 'Hari ini',
'mobileTrip.jumpToToday': 'Lompat ke hari ini',
'mobileTrip.assignedDays': 'Hari yang ditetapkan',
'mobileTrip.assignmentNotes': 'Catatan khusus hari',
'mobileTrip.bookingsEmpty': 'Belum ada reservasi',
+2
View File
@@ -7,6 +7,8 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.addToDayQuestion': 'Aggiungere a un giorno?',
'mobileTrip.addTransportShort': 'Trasporto',
'mobileTrip.allDays': 'Tutti i giorni',
'mobileTrip.today': 'Oggi',
'mobileTrip.jumpToToday': 'Vai a oggi',
'mobileTrip.assignedDays': 'Giorni assegnati',
'mobileTrip.assignmentNotes': 'Note specifiche del giorno',
'mobileTrip.bookingsEmpty': 'Ancora nessuna prenotazione',
+2
View File
@@ -7,6 +7,8 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.addToDayQuestion': '日に追加しますか?',
'mobileTrip.addTransportShort': '移動',
'mobileTrip.allDays': 'すべての日',
'mobileTrip.today': '今日',
'mobileTrip.jumpToToday': '今日へ移動',
'mobileTrip.assignedDays': '割り当てられた日',
'mobileTrip.assignmentNotes': '日別メモ',
'mobileTrip.bookingsEmpty': '予約はまだありません',
+2
View File
@@ -7,6 +7,8 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.addToDayQuestion': '하루에 추가할까요?',
'mobileTrip.addTransportShort': '교통',
'mobileTrip.allDays': '모든 날',
'mobileTrip.today': '오늘',
'mobileTrip.jumpToToday': '오늘로 이동',
'mobileTrip.assignedDays': '배정된 날짜',
'mobileTrip.assignmentNotes': '날짜별 메모',
'mobileTrip.bookingsEmpty': '아직 예약이 없습니다',
+2
View File
@@ -7,6 +7,8 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.addToDayQuestion': 'Toevoegen aan een dag?',
'mobileTrip.addTransportShort': 'Transport',
'mobileTrip.allDays': 'Alle dagen',
'mobileTrip.today': 'Vandaag',
'mobileTrip.jumpToToday': 'Naar vandaag',
'mobileTrip.assignedDays': 'Toegewezen dagen',
'mobileTrip.assignmentNotes': 'Dagspecifieke notities',
'mobileTrip.bookingsEmpty': 'Nog geen boekingen',
+2
View File
@@ -7,6 +7,8 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.addToDayQuestion': 'Dodać do dnia?',
'mobileTrip.addTransportShort': 'Transport',
'mobileTrip.allDays': 'Wszystkie dni',
'mobileTrip.today': 'Dziś',
'mobileTrip.jumpToToday': 'Przejdź do dziś',
'mobileTrip.assignedDays': 'Przypisane dni',
'mobileTrip.assignmentNotes': 'Notatki dla dnia',
'mobileTrip.bookingsEmpty': 'Brak rezerwacji',
+2
View File
@@ -7,6 +7,8 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.addToDayQuestion': 'Добавить в день?',
'mobileTrip.addTransportShort': 'Транспорт',
'mobileTrip.allDays': 'Все дни',
'mobileTrip.today': 'Сегодня',
'mobileTrip.jumpToToday': 'Перейти к сегодня',
'mobileTrip.assignedDays': 'Назначенные дни',
'mobileTrip.assignmentNotes': 'Заметки на день',
'mobileTrip.bookingsEmpty': 'Пока нет бронирований',
+2
View File
@@ -7,6 +7,8 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.addToDayQuestion': 'Pridať do dňa?',
'mobileTrip.addTransportShort': 'Doprava',
'mobileTrip.allDays': 'Všetky dni',
'mobileTrip.today': 'Dnes',
'mobileTrip.jumpToToday': 'Prejsť na dnešok',
'mobileTrip.assignedDays': 'Priradené dni',
'mobileTrip.assignmentNotes': 'Poznámky ku konkrétnemu dňu',
'mobileTrip.bookingsEmpty': 'Zatiaľ žiadne rezervácie',
+2
View File
@@ -7,6 +7,8 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.addToDayQuestion': 'Lägg till på en dag?',
'mobileTrip.addTransportShort': 'Transport',
'mobileTrip.allDays': 'Alla dagar',
'mobileTrip.today': 'Idag',
'mobileTrip.jumpToToday': 'Gå till idag',
'mobileTrip.assignedDays': 'Tilldelade dagar',
'mobileTrip.assignmentNotes': 'Dagsspecifika anteckningar',
'mobileTrip.bookingsEmpty': 'Inga bokningar än',
+2
View File
@@ -7,6 +7,8 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.addToDayQuestion': "เพิ่มในวันใด?",
'mobileTrip.addTransportShort': "ขนส่ง",
'mobileTrip.allDays': "ทุกวัน",
'mobileTrip.today': 'วันนี้',
'mobileTrip.jumpToToday': 'ไปที่วันนี้',
'mobileTrip.assignedDays': "วันที่ได้รับมอบหมาย",
'mobileTrip.assignmentNotes': "บันทึกเฉพาะวัน",
'mobileTrip.bookingsEmpty': "ยังไม่มีการจอง",
+2
View File
@@ -7,6 +7,8 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.addToDayQuestion': 'Bir güne eklensin mi?',
'mobileTrip.addTransportShort': 'Ulaşım',
'mobileTrip.allDays': 'Tüm Günler',
'mobileTrip.today': 'Bugün',
'mobileTrip.jumpToToday': 'Bugüne git',
'mobileTrip.assignedDays': 'Atanan Günler',
'mobileTrip.assignmentNotes': 'Güne özel notlar',
'mobileTrip.bookingsEmpty': 'Henüz rezervasyon yok',
+2
View File
@@ -7,6 +7,8 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.addToDayQuestion': 'Додати до дня?',
'mobileTrip.addTransportShort': 'Транспорт',
'mobileTrip.allDays': 'Усі дні',
'mobileTrip.today': 'Сьогодні',
'mobileTrip.jumpToToday': 'Перейти до сьогодні',
'mobileTrip.assignedDays': 'Призначені дні',
'mobileTrip.assignmentNotes': 'Нотатки на день',
'mobileTrip.bookingsEmpty': 'Поки немає бронювань',
+2
View File
@@ -7,6 +7,8 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.addToDayQuestion': 'Thêm vào một ngày?',
'mobileTrip.addTransportShort': 'Di chuyển',
'mobileTrip.allDays': 'Tất cả các ngày',
'mobileTrip.today': 'Hôm nay',
'mobileTrip.jumpToToday': 'Đến hôm nay',
'mobileTrip.assignedDays': 'Ngày được gán',
'mobileTrip.assignmentNotes': 'Ghi chú theo ngày',
'mobileTrip.bookingsEmpty': 'Chưa có đặt chỗ nào',
+2
View File
@@ -7,6 +7,8 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.addToDayQuestion': '新增到某一天?',
'mobileTrip.addTransportShort': '交通',
'mobileTrip.allDays': '所有天',
'mobileTrip.today': '今天',
'mobileTrip.jumpToToday': '跳到今天',
'mobileTrip.assignedDays': '已分配的天數',
'mobileTrip.assignmentNotes': '按天備註',
'mobileTrip.bookingsEmpty': '暫無預訂',
+2
View File
@@ -7,6 +7,8 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.addToDayQuestion': '添加到某一天?',
'mobileTrip.addTransportShort': '交通',
'mobileTrip.allDays': '所有天',
'mobileTrip.today': '今天',
'mobileTrip.jumpToToday': '跳到今天',
'mobileTrip.assignedDays': '已分配的天数',
'mobileTrip.assignmentNotes': '按天备注',
'mobileTrip.bookingsEmpty': '暂无预订',