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). */