diff --git a/client/src/components/Roadtrip/GoogleRouteImport.tsx b/client/src/components/Roadtrip/GoogleRouteImport.tsx index 19082888a..2841a454e 100644 --- a/client/src/components/Roadtrip/GoogleRouteImport.tsx +++ b/client/src/components/Roadtrip/GoogleRouteImport.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from 'react' +import { useEffect, useId, useRef, useState } from 'react' import { MoreHorizontal, Link2 } from 'lucide-react' import { useTranslation } from '../../i18n/TranslationContext' import { useTripStore } from '../../store/tripStore' @@ -6,7 +6,7 @@ import { googleRouteRepo } from '../../repo/googleRouteRepo' import { generateUUID } from '../../sync/mutationQueue' import { getApiErrorMessage } from '../../types' import type { GoogleRoutePreview } from '@trek/shared' -import Modal from '../shared/Modal' +import { DialogButton, DialogFooter, DialogHeader, DialogSection, DialogShell, DialogTile, FooterSpacer, NEUTRAL_TINT, fs } from '../shared/DialogShell' import CustomSelect from '../shared/CustomSelect' import { ContextMenu, useContextMenu } from '../shared/ContextMenu' import { Tooltip } from '../shared/Tooltip' @@ -24,6 +24,7 @@ export default function GoogleRouteImport({ tripId, dayId }: { tripId: number; d const controller = useRef(null) const requestId = useRef('') const menu = useContextMenu() + const labelId = useId() useEffect(() => () => controller.current?.abort(), []) const inspect = async () => { controller.current?.abort() @@ -51,6 +52,8 @@ export default function GoogleRouteImport({ tripId, dayId }: { tripId: number; d setTarget(String(dayId ?? days[0]?.id ?? '')) requestId.current = generateUUID(); setOpen(true) } + // Every way out is held while a preview or an import is in flight, as before. + const close = () => { if (!busy) setOpen(false) } const resolved = preview?.stops.filter(s => s.lat != null && s.lng != null) ?? [] return <> @@ -60,29 +63,48 @@ export default function GoogleRouteImport({ tripId, dayId }: { tripId: number; d - { if (!busy) setOpen(false) }} title={t('roadtrip.import.title')} size="lg"> -
-

{t('roadtrip.import.note')}

- {!preview ? setUrl(e.target.value)} aria-label="Google Maps URL" placeholder="https://maps.app.goo.gl/…" - className="w-full rounded-xl border border-edge bg-surface-input p-3 text-body text-content" /> : <> + } + tint={NEUTRAL_TINT} + labelId={labelId} + onClose={close} + title={t('roadtrip.import.title')} + sub={t('roadtrip.import.note')} + subWraps + /> + )} + footer={( + + + {t('common.cancel')} + void (preview ? save() : inspect())}> + {busy ? t('common.loading') : t(preview ? 'common.import' : 'common.preview')} + + + )} + > + {!preview ? setUrl(e.target.value)} aria-label="Google Maps URL" placeholder="https://maps.app.goo.gl/…" + className="w-full rounded-[10px] border border-edge bg-surface-input px-3 py-2.5 text-content outline-none placeholder:text-content-faint focus:ring-2 focus:ring-[color:var(--text-primary)] disabled:opacity-50" + style={fs(13, 'body')} /> : <> + { setTarget(String(value)); requestId.current = generateUUID() }} options={days.map(day => ({ value: String(day.id), label: day.title || `${t('roadtrip.import.day')} ${day.day_number}` }))} disabled={busy || saved} /> -
    - {preview.stops.map((stop, index) =>
  1. - {index + 1} - {stop.name}{stop.name !== `${stop.lat}, ${stop.lng}` && {stop.lat == null || stop.lng == null ? t('roadtrip.import.unresolved') : `${stop.lat.toFixed(5)}, ${stop.lng.toFixed(5)}`}} -
  2. )} -
- } - {failure &&

{saved ? t('common.saved') + ': ' : ''}{failure}

} -
- - -
-
-
+ +
    + {preview.stops.map((stop, index) =>
  1. + {index + 1} + {stop.name}{stop.name !== `${stop.lat}, ${stop.lng}` && {stop.lat == null || stop.lng == null ? t('roadtrip.import.unresolved') : `${stop.lat.toFixed(5)}, ${stop.lng.toFixed(5)}`}} +
  2. )} +
+ } + {failure &&

{saved ? t('common.saved') + ': ' : ''}{failure}

} + } diff --git a/client/src/components/Roadtrip/RoadtripCorridorPanel.tsx b/client/src/components/Roadtrip/RoadtripCorridorPanel.tsx index 07018c197..9611f3394 100644 --- a/client/src/components/Roadtrip/RoadtripCorridorPanel.tsx +++ b/client/src/components/Roadtrip/RoadtripCorridorPanel.tsx @@ -16,6 +16,7 @@ import { serviceColor } from './roadtripModel' import { alongLabel, offRouteLabel } from './corridorSearchModel' import { CORRIDOR_CATEGORY_BY_KEY, manualStopKindFor } from './stopKinds' import { FS } from './typeScale' +import { fs } from '../shared/DialogShell' import { CORRIDOR_CATEGORY_KEYS, CORRIDOR_SECTION_KM, CORRIDOR_WIDTHS_KM, type RoadtripCorridor } from './useRoadtripCorridor' import type { RoadtripStopType } from '@trek/shared' import type { CorridorPoi } from './useCorridorPois' @@ -457,11 +458,12 @@ export default function RoadtripCorridorPanel({ disabled={!canSearch} aria-label={t('roadtrip.poi.search')} title={narrow ? t('roadtrip.poi.search') : undefined} - className="flex h-[32px] min-w-0 flex-1 items-center justify-center gap-2 rounded-lg bg-accent text-body font-semibold text-accent-text transition-opacity disabled:opacity-50" + className="flex h-8 min-w-0 flex-1 items-center justify-center gap-1.5 rounded-[10px] bg-accent px-3 font-semibold text-accent-text transition-opacity enabled:hover:opacity-90 disabled:opacity-50" + style={fs(12, 'body')} > {search.loading - ? - : } + ? + : } {/* The word stays put while a run is on, and the spinner in front of it says the run is on. It used to become "Searching 3 of 12", which is both the longest label in the panel and the very sentence the progress row right @@ -481,9 +483,10 @@ export default function RoadtripCorridorPanel({ // the button carries the one word that fits beside "Search". aria-label={t('roadtrip.poi.addManual')} title={t('roadtrip.poi.addManual')} - className="flex h-[32px] min-w-0 flex-1 items-center justify-center gap-2 rounded-lg border border-edge bg-surface-card text-body font-semibold text-content transition-colors hover:bg-surface-hover" + className="flex h-8 min-w-0 flex-1 items-center justify-center gap-1.5 rounded-[10px] border border-edge bg-surface-card px-3 font-semibold text-content transition-colors hover:bg-surface-hover" + style={fs(12, 'body')} > - + {narrow ? null : {t('roadtrip.poi.addManualShort')}} ) : null} diff --git a/client/src/components/Roadtrip/RoadtripTrackModal.tsx b/client/src/components/Roadtrip/RoadtripTrackModal.tsx index 4ca4a9d6c..a4156a83c 100644 --- a/client/src/components/Roadtrip/RoadtripTrackModal.tsx +++ b/client/src/components/Roadtrip/RoadtripTrackModal.tsx @@ -1,11 +1,10 @@ -import React, { useEffect, useState } from 'react' +import React, { useEffect, useId, useState } from 'react' import { AlertTriangle, Check, RotateCw, Spline, Trash2 } from 'lucide-react' -import Modal from '../shared/Modal' +import { DialogButton, DialogFooter, DialogHeader, DialogShell, DialogTile, FooterSpacer, NEUTRAL_TINT, fs } from '../shared/DialogShell' import EmptyState from '../shared/EmptyState' import { useTranslation } from '../../i18n/TranslationContext' import { useSettingsStore } from '../../store/settingsStore' import { formatDistance } from '../../utils/units' -import { FS } from './typeScale' import type { FollowTrack, TrackChoice } from './useFollowTrack' /** @@ -24,8 +23,9 @@ import type { FollowTrack, TrackChoice } from './useFollowTrack' /** A track close enough that it is plainly about this day rather than another one. */ const ON_THIS_DAY_KM = 2 -function TrackRow({ track, disabled, onPick }: { +function TrackRow({ track, picked, disabled, onPick }: { track: TrackChoice + picked: boolean disabled: boolean onPick: () => void }): React.ReactElement { @@ -39,7 +39,10 @@ function TrackRow({ track, disabled, onPick }: { type="button" disabled={disabled} onClick={onPick} - className="flex w-full items-center gap-3 rounded-xl px-2 py-2 text-start transition-colors hover:bg-surface-hover disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent" + aria-pressed={picked} + className={`flex w-full items-center gap-3 rounded-[10px] px-3 py-2.5 text-start transition-colors disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent ${ + picked ? 'bg-surface-card shadow-sm ring-1 ring-edge-faint' : 'hover:bg-surface-card' + }`} > {/* The line's own colour, as a short stroke rather than a dot: it is a line on the map, and a dot beside a name reads as a place. */} @@ -50,10 +53,10 @@ function TrackRow({ track, disabled, onPick }: { aria-hidden /> - + {track.name || t('roadtrip.track.untitled')} - + {t('roadtrip.track.length', { distance: formatDistance(track.lengthKm, distanceUnit) })} {' · '} {onDay @@ -61,6 +64,7 @@ function TrackRow({ track, disabled, onPick }: { : t('roadtrip.track.gap', { distance: formatDistance(track.gapKm, distanceUnit) })} + {picked ? : null} ) @@ -74,6 +78,7 @@ export default function RoadtripTrackModal({ follow, dayNumber }: { const { t } = useTranslation() const distanceUnit = useSettingsStore(s => s.settings.distance_unit) const [picked, setPicked] = useState(null) + const labelId = useId() const { dayId, busy, round, error, outcome, tracks, viaCount, current } = follow @@ -85,109 +90,99 @@ export default function RoadtripTrackModal({ follow, dayNumber }: { const chosen = tracks.find(track => track.id === picked) ?? null return ( - - - {t('roadtrip.track.title', { number: dayNumber })} - - } - > -
- {/* What the day follows today, before anything is picked. Read back from the day - rather than remembered from the run that applied it, which is the whole reason - it is stored: it has to survive a reload. */} - {current ? ( -

- {t('roadtrip.track.current', { name: current.name })} -

- ) : null} - {tracks.length === 0 ? ( -
- -

{t('roadtrip.track.emptyHint')}

-
- ) : ( -
    - {tracks.map(track => ( - setPicked(track.id === picked ? null : track.id)} - /> - ))} -
- )} - - {/* What the pick will do, before it is done. The count is the honest part: a track - becomes a dozen waypoints on the day, and a traveller who later wonders where - they came from should have been told here. */} - {chosen ? ( -

- {t('roadtrip.track.explain')} -

- ) : null} - - {busy ? ( -

- - {t('roadtrip.track.working', { round })} -

- ) : null} - - {error ? ( -

- - {t(error === 'route' ? 'roadtrip.track.errorRoute' : 'roadtrip.track.errorSave')} -

- ) : null} - - {outcome && !busy && !error ? ( -

- - {outcome.vias === 0 - ? t('roadtrip.track.already') - : outcome.capped - ? t('roadtrip.track.capped', { - count: outcome.vias, - distance: formatDistance(outcome.strayKm, distanceUnit), - }) - : t('roadtrip.track.done', { - count: outcome.vias, - distance: formatDistance(outcome.strayKm, distanceUnit), - })} -

- ) : null} - -
+ labelledBy={labelId} + width="narrow" + header={( + } + tint={NEUTRAL_TINT} + labelId={labelId} + onClose={follow.close} + title={t('roadtrip.track.title', { number: dayNumber })} + // What the day follows today, before anything is picked. Read back from the day + // rather than remembered from the run that applied it, which is the whole reason + // it is stored: it has to survive a reload. + sub={current ? t('roadtrip.track.current', { name: current.name }) : undefined} + subWraps + /> + )} + footer={( + {/* Offered whenever the day carries vias, whether this dialog put them there or a dragged handle did: "put it back the way the router had it" is the same wish either way. */} {viaCount > 0 ? ( - + ) : null} - + + + )} + > + {tracks.length === 0 ? ( +
+ +

{t('roadtrip.track.emptyHint')}

-
- + ) : ( +
    + {tracks.map(track => ( + setPicked(track.id === picked ? null : track.id)} + /> + ))} +
+ )} + + {/* What the pick will do, before it is done. The count is the honest part: a track + becomes a dozen waypoints on the day, and a traveller who later wonders where + they came from should have been told here. */} + {chosen ? ( +

+ {t('roadtrip.track.explain')} +

+ ) : null} + + {busy ? ( +

+ + {t('roadtrip.track.working', { round })} +

+ ) : null} + + {error ? ( +

+ + {t(error === 'route' ? 'roadtrip.track.errorRoute' : 'roadtrip.track.errorSave')} +

+ ) : null} + + {outcome && !busy && !error ? ( +

+ + {outcome.vias === 0 + ? t('roadtrip.track.already') + : outcome.capped + ? t('roadtrip.track.capped', { + count: outcome.vias, + distance: formatDistance(outcome.strayKm, distanceUnit), + }) + : t('roadtrip.track.done', { + count: outcome.vias, + distance: formatDistance(outcome.strayKm, distanceUnit), + })} +

+ ) : null} + ) }