From 0f600d04f52d05c032db1555a41a0695bdd4f729 Mon Sep 17 00:00:00 2001 From: Maurice Date: Tue, 20 Oct 2026 17:51:26 +0200 Subject: [PATCH] fix(tours): import the walk icon, keep one stop menu open and count only stage ends a change lost The activity section used Footprints without importing it. Opening one stop's menu now closes any other. lostStageEnds no longer counts a stop the change kept without its mark as lost twice over. The wanderer watch switch keeps the same tree once the connection is known, so it is not remounted and keeps its focus. --- .../components/Settings/WandererWatchRow.tsx | 29 +++++++-------- .../Tours/activity/TourActivitySection.tsx | 2 +- .../Tours/planner/TourWaypointList.tsx | 35 +++++++++++++++---- .../src/components/Tours/stages/tourStages.ts | 9 +++-- 4 files changed, 50 insertions(+), 25 deletions(-) diff --git a/client/src/components/Settings/WandererWatchRow.tsx b/client/src/components/Settings/WandererWatchRow.tsx index 3680288b8..59bef0b74 100644 --- a/client/src/components/Settings/WandererWatchRow.tsx +++ b/client/src/components/Settings/WandererWatchRow.tsx @@ -12,24 +12,25 @@ import { useWandererWatch, type WandererWatchController } from './useWandererWat /** The switch, dimmed with a word on why while there is no connection to watch with. */ function WatchSwitch({ W }: { W: WandererWatchController }) { const { t } = useTranslation(); - const toggle = ( - void W.toggle()} - label={t('wanderer.watch.label')} - disabled={!W.available} - /> - ); - if (W.available) return toggle; + const off = !W.available; // A disabled button takes no focus and, in some browsers, no hover either, so - // the reason sits on a focusable wrapper the tooltip listens to. + // the reason sits on a focusable wrapper the tooltip listens to. The wrapper + // stays once the switch can be used, so the button is never mounted anew and + // keeps its focus when the connection is known. return ( - + - {toggle} + + void W.toggle()} + label={t('wanderer.watch.label')} + disabled={off} + /> + ); diff --git a/client/src/components/Tours/activity/TourActivitySection.tsx b/client/src/components/Tours/activity/TourActivitySection.tsx index b3d15bb2e..c972cbe8b 100644 --- a/client/src/components/Tours/activity/TourActivitySection.tsx +++ b/client/src/components/Tours/activity/TourActivitySection.tsx @@ -1,5 +1,5 @@ import type { TourActivity, TourListItem } from '@trek/shared'; -import { CalendarDays, CheckCheck, Clock, CloudOff, FileDown, Plus, RefreshCw, Unlink } from 'lucide-react'; +import { CalendarDays, CheckCheck, Clock, CloudOff, FileDown, Footprints, Plus, RefreshCw, Unlink } from 'lucide-react'; import { useMemo } from 'react'; import { useTranslation } from '../../../i18n'; import { useSettingsStore } from '../../../store/settingsStore'; diff --git a/client/src/components/Tours/planner/TourWaypointList.tsx b/client/src/components/Tours/planner/TourWaypointList.tsx index 030653e56..ba6e07900 100644 --- a/client/src/components/Tours/planner/TourWaypointList.tsx +++ b/client/src/components/Tours/planner/TourWaypointList.tsx @@ -39,7 +39,8 @@ import { PaddleRolePill } from '../paddle/PaddleWaypointParts'; import { usePaddleRegionLoad } from '../paddle/usePaddleRegionLoad'; import { usePaddleWaypointActions } from '../paddle/usePaddleWaypointActions'; import { usePortageRouting } from '../paddle/usePortageRouting'; -import { StageDivider, StageEndCorner, stageMenuItems } from '../stages/StageParts'; +import { stageMenuItems } from '../stages/stageActions'; +import { StageDivider, StageEndCorner } from '../stages/StageParts'; import { RailCard } from '../tourParts'; import { StationScheduleRow } from '../walk/StationSchedule'; import { usePlannerWalkSchedule } from '../walk/useWalkSchedule'; @@ -465,6 +466,8 @@ function WaypointRowActions({ isSaving, onUnlink, stationName, + menuOwner, + onMenuOpen, }: { planner: TourPlannerController; point: WaypointPoint; @@ -476,9 +479,13 @@ function WaypointRowActions({ onUnlink?: () => void; /** CW-01: the name the row shows, for a station without one of its own. */ stationName?: string; + /** The row whose menu is open; opening one row's menu closes the menu of any other. */ + menuOwner: string | null; + onMenuOpen: (id: string) => void; }) { const { t } = useTranslation(); const menu = useContextMenu(); + const menuOpen = !!menu.menu && menuOwner === point.id; const paddle = usePaddleWaypointActions(planner); const [stationOpen, setStationOpen] = useState(false); const station = planner.tourType === 'walk' && !point.shaping && !(planner.isLoop && index === lastIndex); @@ -511,14 +518,21 @@ function WaypointRowActions({ ]; return ( undefined} - onPress={(event) => (menu.menu ? menu.close() : menu.open(event, items, true))} + onPress={(event) => { + if (menuOpen) { + menu.close(); + return; + } + onMenuOpen(point.id); + menu.open(event, items, true); + }} > @@ -530,7 +544,7 @@ function WaypointRowActions({ > - + {stationOpen && ( (null); const addRows = canEdit && ( <> setPlacesOpen((open) => !open)} /> @@ -719,6 +734,10 @@ export default function TourWaypointList({ const stage = stageStarts.get(index); return stage ? : null; }; + // A row's name: the trip place it stands for, else the name looked up for its spot. + const placeNameOf = (point: WaypointPoint, linked: ReturnType) => + linked?.name ?? placeNames[point.id]; + const ferrataOf = (point: WaypointPoint) => ferrataEnds[point.id] ?? null; const stageMarkOf = (stage: number | undefined) => stage ? { stageNote: t('tours.stages.endOf', { number: stage }), corner: } : {}; const endDrag = () => { @@ -768,7 +787,7 @@ export default function TourWaypointList({ const isDropTarget = dropTarget?.id === point.id && draggedWaypointId !== point.id; const loopEnd = planner.isLoop && index === lastIndex; const linkedPlace = tripPlaces.linkedPlaceOf(point); - const placeName = linkedPlace?.name ?? placeNames[point.id]; + const placeName = placeNameOf(point, linkedPlace); const stop = stopNumbers.get(point.id) ?? index + 1; return ( @@ -829,7 +848,7 @@ export default function TourWaypointList({ tripPlaces.unlink(point.id) : undefined} stationName={placeName} + menuOwner={menuOwner} + onMenuOpen={setMenuOwner} /> )} diff --git a/client/src/components/Tours/stages/tourStages.ts b/client/src/components/Tours/stages/tourStages.ts index 4845dcd8e..a251f5eec 100644 --- a/client/src/components/Tours/stages/tourStages.ts +++ b/client/src/components/Tours/stages/tourStages.ts @@ -94,9 +94,12 @@ export function lostStageEnds( ): number { const kept = new Set(next.filter((point) => point.stageEnd).map((point) => point.id)); const asChanged = new Map(changed.map((point) => [point.id, point])); - return current.filter( - (point) => point.stageEnd && !kept.has(point.id) && (asChanged.get(point.id)?.stageEnd ?? true) - ).length; + return current.filter((point) => { + if (!point.stageEnd || kept.has(point.id)) return false; + const asChange = asChanged.get(point.id); + // A point the change removed lost its mark; one the change kept lost it only if the change still had it. + return !asChange || !!asChange.stageEnd; + }).length; } /** What a stage measures; null where the route does not tell (no heights on the water, a route not planned yet). */