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.
This commit is contained in:
Maurice
2026-10-20 17:51:26 +02:00
parent b53257deaf
commit 0f600d04f5
4 changed files with 50 additions and 25 deletions
@@ -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 = (
<ToggleSwitch
on={W.enabled}
onToggle={() => 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 (
<Tooltip label={t('wanderer.watch.notConnected')}>
<Tooltip label={t('wanderer.watch.notConnected')} disabled={!off}>
<span
tabIndex={0}
className="inline-flex cursor-not-allowed rounded-full opacity-50 outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--text-primary)] focus-visible:ring-offset-2 focus-visible:ring-offset-[color:var(--bg-card)]"
tabIndex={off ? 0 : undefined}
className={`inline-flex rounded-full outline-none ${off ? 'cursor-not-allowed opacity-50 focus-visible:ring-2 focus-visible:ring-[color:var(--text-primary)] focus-visible:ring-offset-2 focus-visible:ring-offset-[color:var(--bg-card)]' : ''}`}
>
<span className="pointer-events-none inline-flex">{toggle}</span>
<span className={`inline-flex ${off ? 'pointer-events-none' : ''}`}>
<ToggleSwitch
on={W.enabled}
onToggle={() => void W.toggle()}
label={t('wanderer.watch.label')}
disabled={off}
/>
</span>
</span>
</Tooltip>
);
@@ -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';
@@ -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 (
<span
className={`flex-none items-center gap-0.5 group-focus-within:flex group-hover:flex ${selected || menu.menu ? 'flex' : 'hidden'}`}
className={`flex-none items-center gap-0.5 group-focus-within:flex group-hover:flex ${selected || menuOpen ? 'flex' : 'hidden'}`}
>
<WaypointAction
label={t('tours.planner.moreActions')}
disabled={isSaving}
active={!!menu.menu}
active={menuOpen}
onAct={() => 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);
}}
>
<MoreHorizontal size={14} strokeWidth={2.2} />
</WaypointAction>
@@ -530,7 +544,7 @@ function WaypointRowActions({
>
<Trash2 size={13} strokeWidth={2} />
</WaypointAction>
<ContextMenu menu={menu.menu} onClose={menu.close} />
<ContextMenu menu={menuOpen ? menu.menu : null} onClose={menu.close} />
{stationOpen && (
<StationDialog
point={point}
@@ -683,6 +697,7 @@ export default function TourWaypointList({
// CW-02, CW-04: the arrivals and opening hours of a city walk on one of its days; hints only, nothing saved.
const walk = usePlannerWalkSchedule(planner, tripPlaces.linkedPlaceOf);
const [placesOpen, setPlacesOpen] = useState(false);
const [menuOwner, setMenuOwner] = useState<string | null>(null);
const addRows = canEdit && (
<>
<AddPointRow disabled={isSaving} placesOpen={placesOpen} onTogglePlaces={() => setPlacesOpen((open) => !open)} />
@@ -719,6 +734,10 @@ export default function TourWaypointList({
const stage = stageStarts.get(index);
return stage ? <StageDivider stage={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<typeof tripPlaces.linkedPlaceOf>) =>
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: <StageEndCorner /> } : {};
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 (
<Fragment key={point.id}>
@@ -829,7 +848,7 @@ export default function TourWaypointList({
<WaypointNumber
stop={stop}
selected={selected}
ferrata={ferrataEnds[point.id] ?? null}
ferrata={ferrataOf(point)}
poiCategory={point.poiCategory ?? null}
station={point}
loopEnd={loopEnd}
@@ -861,6 +880,8 @@ export default function TourWaypointList({
isSaving={isSaving}
onUnlink={point.linkedPlaceId ? () => tripPlaces.unlink(point.id) : undefined}
stationName={placeName}
menuOwner={menuOwner}
onMenuOpen={setMenuOwner}
/>
)}
</div>
@@ -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). */