mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-24 07:53:09 +00:00
feat(roadtrip): turn a place into a stop on the way from its own number
An ordinary place could not become a fuel stop without going through the full form, and the number in the rail is exactly what changes when it does: a service stop has none. So the number is the control. Click the 3, pick the pump, and the 3 becomes an orange disc while everything below renumbers itself. The disc of a service stop opens the same picker the other way, with a row to make it an ordinary destination again. Six coloured discs rather than six lines of text: at that size the colour and the shape are the label, and anyone who has seen one petrol stop on the map knows the orange pump before reading anything. Centred on the column rather than on the number, which sits hard against the left edge and would hang the panel half over the map. The click stops at the disc, so picking a kind never also selects the place underneath. Also in this change, from the same round: the corridor search offers hotels, a hit somebody could sleep at opens the popup in an overnight mode with a check-out day instead of a dwell, and booking one writes the place, its day and the row in day_accommodations together. A hotel keeps its number, because sleeping there ends the day rather than interrupting the drive, which is why the category list and the stop-kind list are now two lists instead of one.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import {
|
||||
MapPin, CarFront, Footprints, Bike, Zap, AlertTriangle, Moon,
|
||||
ParkingSquare, Shuffle, Fuel, Clock,
|
||||
@@ -12,6 +12,8 @@ import { formatDate, formatClockTime } from '../../utils/formatters'
|
||||
import { formatDurationShort, isServiceStopType, serviceColor, type ScheduleEntry, type ScheduleWarning } from './roadtripModel'
|
||||
import { STOP_KIND_BY_KEY } from './stopKinds'
|
||||
import { spurWorthLabelling } from './accessSpur'
|
||||
import StopKindPicker from './StopKindPicker'
|
||||
import type { RoadtripStopType } from '@trek/shared'
|
||||
import type { QuietDay, RoadtripDay, RoadtripRoutes, RoadtripStop } from './useRoadtripRoutes'
|
||||
import type { RouteVia } from '../../types'
|
||||
import { FS } from './typeScale'
|
||||
@@ -38,6 +40,12 @@ interface RoadtripSidebarProps {
|
||||
* which is also what a viewer sees.
|
||||
*/
|
||||
onEditStay?: (stop: { placeId: number; name: string; minutes: number | null; arrival: string | null }) => void
|
||||
/**
|
||||
* Turns a stop into a pause on the drive, or back into a destination.
|
||||
*
|
||||
* Absent leaves every disc read-only, which is also what a viewer sees.
|
||||
*/
|
||||
onSetStopKind?: (placeId: number, kind: RoadtripStopType | null) => Promise<void> | void
|
||||
}
|
||||
|
||||
const MODE_ICON: Record<string, LucideIcon> = {
|
||||
@@ -352,12 +360,14 @@ function DriveBand({ leg, onAskAlternatives, alternativesOpen }: {
|
||||
* the trip is actually for. Its own icon on one flat disc: three kinds of pause that all
|
||||
* mean "we are still driving", and the icon is what tells them apart.
|
||||
*/
|
||||
function ServiceStop({ stop, entry, driveFindings, selected, onSelect, onEditStay }: {
|
||||
function ServiceStop({ stop, entry, driveFindings, selected, onSelect, onEditStay, onPickKind }: {
|
||||
stop: RoadtripStop
|
||||
entry: ScheduleEntry | undefined
|
||||
/** Findings about the drive that ARRIVES here. A charging halt is a stop like any other
|
||||
* as far as the tank is concerned, so it carries them the same way a numbered one does. */
|
||||
driveFindings?: ScheduleWarning[]
|
||||
/** Opens the kind picker on the disc. Absent leaves the rail read-only. */
|
||||
onPickKind?: (anchor: HTMLElement) => void
|
||||
selected: boolean
|
||||
onSelect?: () => void
|
||||
/** Opens the dialog for how long this pause takes. Absent means the rail is read-only. */
|
||||
@@ -371,15 +381,39 @@ function ServiceStop({ stop, entry, driveFindings, selected, onSelect, onEditSta
|
||||
<div className="grid items-stretch" style={RAIL_GRID}>
|
||||
<span className="relative z-[1] flex flex-col items-center">
|
||||
<span className="flex-1" style={RAIL_DASH} aria-hidden />
|
||||
<span
|
||||
className={DISC}
|
||||
// theme-lint-disable — the road-signage palette in `roadtripModel`, shared with
|
||||
// the corridor list and the map pin so one kind of stop looks like itself
|
||||
// wherever it turns up.
|
||||
style={{ background: serviceColor(stop.stopType), color: '#fff' }}
|
||||
>
|
||||
<Icon size={12} strokeWidth={2.1} aria-label={label} />
|
||||
</span>
|
||||
{onPickKind ? (
|
||||
// The same control the other way round: the disc says what this is, and it is
|
||||
// also where it stops being that.
|
||||
<Tooltip label={t('roadtrip.stop.changeKind')}>
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={t('roadtrip.stop.changeKind')}
|
||||
onClick={e => { e.stopPropagation(); onPickKind(e.currentTarget as HTMLElement) }}
|
||||
onKeyDown={e => {
|
||||
if (e.key !== 'Enter' && e.key !== ' ') return
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
onPickKind(e.currentTarget as HTMLElement)
|
||||
}}
|
||||
className={`${DISC} cursor-pointer transition-transform hover:scale-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2`}
|
||||
// theme-lint-disable — the road-signage palette in `roadtripModel`, shared
|
||||
// with the corridor list and the map pin so one kind of stop looks like
|
||||
// itself wherever it turns up.
|
||||
style={{ background: serviceColor(stop.stopType), color: '#fff' }}
|
||||
>
|
||||
<Icon size={12} strokeWidth={2.1} aria-hidden />
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<span
|
||||
className={DISC}
|
||||
// theme-lint-disable — same palette, read-only.
|
||||
style={{ background: serviceColor(stop.stopType), color: '#fff' }}
|
||||
>
|
||||
<Icon size={12} strokeWidth={2.1} aria-label={label} />
|
||||
</span>
|
||||
)}
|
||||
<span className="flex-1" style={RAIL_DASH} aria-hidden />
|
||||
</span>
|
||||
<button
|
||||
@@ -576,7 +610,7 @@ function Arrival({ entry }: { entry: ScheduleEntry }): React.ReactElement {
|
||||
)
|
||||
}
|
||||
|
||||
function Stop({ stop, number, entry, late, driveFindings, selected, continues, starts, onSelect, onMove, canMove, onEditStay }: {
|
||||
function Stop({ stop, number, entry, late, driveFindings, selected, continues, starts, onSelect, onMove, canMove, onEditStay, onPickKind }: {
|
||||
stop: RoadtripStop
|
||||
/** Position within the day — the same count the map badges its markers with. */
|
||||
number: number
|
||||
@@ -589,6 +623,8 @@ function Stop({ stop, number, entry, late, driveFindings, selected, continues, s
|
||||
continues: boolean
|
||||
/** First row of the day: no line above it, because the chain starts here. */
|
||||
starts?: boolean
|
||||
/** Opens the kind picker on the number. Absent leaves the rail read-only. */
|
||||
onPickKind?: (anchor: HTMLElement) => void
|
||||
onSelect?: () => void
|
||||
/** Moves this stop by one place. Absent means the chain is read-only. */
|
||||
onMove?: (delta: number) => void
|
||||
@@ -623,12 +659,36 @@ function Stop({ stop, number, entry, late, driveFindings, selected, continues, s
|
||||
unbroken from stop to stop. */}
|
||||
<span className="flex flex-col items-center">
|
||||
{starts ? null : <span className="w-[1.5px] flex-1 rounded-sm bg-edge" aria-hidden />}
|
||||
<span
|
||||
className={`${DISC} my-1 bg-surface-tertiary font-geist font-semibold tabular-nums text-content-secondary`}
|
||||
style={{ fontSize: FS.marker }}
|
||||
>
|
||||
{number}
|
||||
</span>
|
||||
{onPickKind ? (
|
||||
// The number is the control, because the number is what changes: a service stop
|
||||
// has none. Clicking the 3 and picking the pump turns the 3 into an orange disc
|
||||
// and renumbers everything below it.
|
||||
<Tooltip label={t('roadtrip.stop.makeService')}>
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={t('roadtrip.stop.makeService')}
|
||||
onClick={e => { e.stopPropagation(); onPickKind(e.currentTarget as HTMLElement) }}
|
||||
onKeyDown={e => {
|
||||
if (e.key !== 'Enter' && e.key !== ' ') return
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
onPickKind(e.currentTarget as HTMLElement)
|
||||
}}
|
||||
className={`${DISC} my-1 cursor-pointer bg-surface-tertiary font-geist font-semibold tabular-nums text-content-secondary transition-colors hover:bg-accent hover:text-accent-text focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent`}
|
||||
style={{ fontSize: FS.marker }}
|
||||
>
|
||||
{number}
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<span
|
||||
className={`${DISC} my-1 bg-surface-tertiary font-geist font-semibold tabular-nums text-content-secondary`}
|
||||
style={{ fontSize: FS.marker }}
|
||||
>
|
||||
{number}
|
||||
</span>
|
||||
)}
|
||||
{continues ? <span className="w-[1.5px] flex-1 rounded-sm bg-edge" aria-hidden /> : null}
|
||||
</span>
|
||||
|
||||
@@ -685,7 +745,7 @@ function Stop({ stop, number, entry, late, driveFindings, selected, continues, s
|
||||
* (`dayOrderMap` numbers the selected day's assignments from 1). A rail counting across
|
||||
* the trip would put "17" beside a pin the map calls "3".
|
||||
*/
|
||||
function DaySection({ day, selectedAssignmentId, onSelectStop, onReorderStop, onMoveStopToDay, drag, onAskAlternatives, openAlternatives, onEditStay }: {
|
||||
function DaySection({ day, selectedAssignmentId, onSelectStop, onReorderStop, onMoveStopToDay, drag, onAskAlternatives, openAlternatives, onEditStay, onSetStopKind }: {
|
||||
day: RoadtripDay
|
||||
selectedAssignmentId?: number | null
|
||||
onSelectStop?: (placeId: number, assignmentId: number) => void
|
||||
@@ -696,9 +756,13 @@ function DaySection({ day, selectedAssignmentId, onSelectStop, onReorderStop, on
|
||||
onAskAlternatives?: RoadtripSidebarProps['onAskAlternatives']
|
||||
openAlternatives?: RoadtripSidebarProps['openAlternatives']
|
||||
onEditStay?: RoadtripSidebarProps['onEditStay']
|
||||
onSetStopKind?: RoadtripSidebarProps['onSetStopKind']
|
||||
}): React.ReactElement {
|
||||
const { from, setFrom, dropAt, setDropAt } = drag
|
||||
const dragging = from?.dayId === day.dayId ? from.index : null
|
||||
// Which disc the picker hangs under, and for which stop. One at a time: two open
|
||||
// popovers over the same rail is two answers to one question.
|
||||
const [picking, setPicking] = useState<{ anchor: HTMLElement; stop: RoadtripStop } | null>(null)
|
||||
const { t, language } = useTranslation()
|
||||
const distanceUnit = useSettingsStore(s => s.settings.distance_unit)
|
||||
const last = day.stops.length - 1
|
||||
@@ -806,6 +870,7 @@ function DaySection({ day, selectedAssignmentId, onSelectStop, onReorderStop, on
|
||||
selected={selectedAssignmentId === stop.assignmentId}
|
||||
onSelect={onSelectStop ? () => onSelectStop(stop.placeId, stop.assignmentId) : undefined}
|
||||
onEditStay={onEditStay ? () => onEditStay({ placeId: stop.placeId, name: stop.name, minutes: stop.dwellMinutes, arrival: day.schedule.entries[i]?.arrival ?? null }) : undefined}
|
||||
onPickKind={onSetStopKind ? anchor => setPicking({ anchor, stop }) : undefined}
|
||||
/>
|
||||
) : (
|
||||
<Stop
|
||||
@@ -821,6 +886,7 @@ function DaySection({ day, selectedAssignmentId, onSelectStop, onReorderStop, on
|
||||
onMove={onReorderStop ? delta => onReorderStop(day.dayId, stop.assignmentId, i + delta) : undefined}
|
||||
canMove={{ up: i > 0, down: i < last }}
|
||||
onEditStay={onEditStay ? () => onEditStay({ placeId: stop.placeId, name: stop.name, minutes: stop.dwellMinutes, arrival: day.schedule.entries[i]?.arrival ?? null }) : undefined}
|
||||
onPickKind={onSetStopKind ? anchor => setPicking({ anchor, stop }) : undefined}
|
||||
/>
|
||||
)}
|
||||
{i < last ? (
|
||||
@@ -839,6 +905,17 @@ function DaySection({ day, selectedAssignmentId, onSelectStop, onReorderStop, on
|
||||
)
|
||||
})}
|
||||
</ol>
|
||||
{picking ? (
|
||||
<StopKindPicker
|
||||
anchor={picking.anchor}
|
||||
current={picking.stop.stopType}
|
||||
onClose={() => setPicking(null)}
|
||||
onPick={kind => {
|
||||
setPicking(null)
|
||||
void onSetStopKind?.(picking.stop.placeId, kind)
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -917,6 +994,7 @@ function QuietDaySection({ day, onMoveStopToDay, drag }: {
|
||||
*/
|
||||
export default function RoadtripSidebar({
|
||||
routes, selectedAssignmentId, onSelectStop, onReorderStop, onMoveStopToDay, onAskAlternatives, openAlternatives, onEditStay,
|
||||
onSetStopKind,
|
||||
}: RoadtripSidebarProps): React.ReactElement {
|
||||
const { t } = useTranslation()
|
||||
// One drag state for the whole rail rather than one per day: a stop that cannot leave
|
||||
@@ -958,6 +1036,7 @@ export default function RoadtripSidebar({
|
||||
onAskAlternatives={onAskAlternatives}
|
||||
openAlternatives={openAlternatives}
|
||||
onEditStay={onEditStay}
|
||||
onSetStopKind={onSetStopKind}
|
||||
/>
|
||||
))}
|
||||
{routes.quietDays.map(day => (
|
||||
|
||||
@@ -168,4 +168,78 @@ describe('RoadtripStopPopup', () => {
|
||||
await act(async () => { release() })
|
||||
expect(add).not.toBeDisabled()
|
||||
})
|
||||
// ── Overnight mode ─────────────────────────────────────────────────────────
|
||||
//
|
||||
// Somewhere to sleep is the one hit that ends the day rather than interrupting the
|
||||
// drive, so the dialog grows a second mode instead of offering a hotel a dwell of
|
||||
// thirty minutes.
|
||||
|
||||
const overnight = {
|
||||
days: [
|
||||
{ id: 4, number: 1, date: '2026-08-31' },
|
||||
{ id: 5, number: 2, date: '2026-09-01' },
|
||||
{ id: 6, number: 3, date: '2026-09-02' },
|
||||
],
|
||||
defaultEndDayId: 5,
|
||||
}
|
||||
const nightDraft = (category: string) => draft({
|
||||
poi: poi({ category, name: 'Hotel Adlon' }),
|
||||
overnight,
|
||||
})
|
||||
|
||||
it('FE-ROADTRIP-STOPPOPUP-020: a hotel opens on the overnight mode', () => {
|
||||
wrap(<RoadtripStopPopup draft={nightDraft('hotel')} {...noop} onSaveNight={vi.fn()} />)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Overnight' })).toHaveAttribute('aria-pressed', 'true')
|
||||
expect(screen.getByText('Check out on')).toBeInTheDocument()
|
||||
// The two questions a pause asks are gone: a hotel has no kind and no dwell.
|
||||
expect(screen.queryByText('Kind of stop')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('FE-ROADTRIP-STOPPOPUP-021: a campsite opens on the pause mode but offers the night', () => {
|
||||
// It is a service stop today and stays one, but people do sleep at campsites, so the
|
||||
// choice is offered rather than decided.
|
||||
wrap(<RoadtripStopPopup draft={nightDraft('campsite')} {...noop} onSaveNight={vi.fn()} />)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Pause' })).toHaveAttribute('aria-pressed', 'true')
|
||||
expect(screen.getByText('Kind of stop')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'Overnight' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('FE-ROADTRIP-STOPPOPUP-022: a petrol station is never offered a night', () => {
|
||||
// Nothing to choose between, so no switch at all.
|
||||
wrap(<RoadtripStopPopup draft={draft()} {...noop} onSaveNight={vi.fn()} />)
|
||||
|
||||
expect(screen.queryByRole('button', { name: 'Overnight' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('FE-ROADTRIP-STOPPOPUP-023: booking a night reports the check-out day, defaulting to the next', () => {
|
||||
const onSaveNight = vi.fn()
|
||||
wrap(<RoadtripStopPopup draft={nightDraft('hotel')} {...noop} onSaveNight={onSaveNight} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Add' }))
|
||||
|
||||
expect(onSaveNight).toHaveBeenCalledWith({ endDayId: 5, checkIn: '', checkOut: '' })
|
||||
})
|
||||
|
||||
it('FE-ROADTRIP-STOPPOPUP-024: switching back to a pause saves a pause', () => {
|
||||
const onSave = vi.fn()
|
||||
const onSaveNight = vi.fn()
|
||||
wrap(<RoadtripStopPopup draft={nightDraft('hotel')} {...noop} onSave={onSave} onSaveNight={onSaveNight} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Pause' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Add' }))
|
||||
|
||||
expect(onSaveNight).not.toHaveBeenCalled()
|
||||
expect(onSave).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('FE-ROADTRIP-STOPPOPUP-025: without a way to book a night the switch stays away', () => {
|
||||
// An onSaveNight-less caller gets the dialog it always had, rather than a mode that
|
||||
// leads nowhere.
|
||||
wrap(<RoadtripStopPopup draft={nightDraft('hotel')} {...noop} />)
|
||||
|
||||
expect(screen.queryByRole('button', { name: 'Overnight' })).not.toBeInTheDocument()
|
||||
expect(screen.getByText('Kind of stop')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { MapPin } from 'lucide-react'
|
||||
import { useTranslation } from '../../i18n/TranslationContext'
|
||||
import { Tooltip } from '../shared/Tooltip'
|
||||
import { STOP_KINDS } from './stopKinds'
|
||||
import type { RoadtripStopType } from '@trek/shared'
|
||||
|
||||
/**
|
||||
* Turning a place on the drive into a pause, and back.
|
||||
*
|
||||
* Opened from the stop's own number, because the number is exactly what changes: a
|
||||
* service stop has none. That makes the control its own preview — click the 3, pick the
|
||||
* pump, and the 3 becomes an orange disc while everything below renumbers itself.
|
||||
*
|
||||
* The kinds are the discs the rail already draws rather than a list of words. At this
|
||||
* size the colour and the shape are the label: a traveller who has seen one petrol stop
|
||||
* on the map knows the orange pump before reading anything, and six rows of text for six
|
||||
* icons would be a menu where a palette does.
|
||||
*
|
||||
* Only for stops on a drive. A place with no coordinates never reaches the rail, and a
|
||||
* hotel is not offered here at all, because sleeping somewhere is not a pause in the
|
||||
* driving — that path goes through the accommodation it books.
|
||||
*/
|
||||
|
||||
const DISC = 'grid place-items-center rounded-full transition-transform'
|
||||
|
||||
export default function StopKindPicker({ anchor, current, onPick, onClose }: {
|
||||
/** The element the popover hangs under, usually the stop's number. */
|
||||
anchor: HTMLElement | null
|
||||
current: string | null
|
||||
onPick: (kind: RoadtripStopType | null) => void
|
||||
onClose: () => void
|
||||
}): React.ReactElement | null {
|
||||
const { t } = useTranslation()
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const [pos, setPos] = useState<{ top: number; left: number } | null>(null)
|
||||
|
||||
// Measured after paint, before the browser shows it: reading the size first and then
|
||||
// placing it is what stops the panel appearing at 0,0 for one frame.
|
||||
useLayoutEffect(() => {
|
||||
if (!anchor || !ref.current) return
|
||||
const a = anchor.getBoundingClientRect()
|
||||
const p = ref.current.getBoundingClientRect()
|
||||
const gap = 8
|
||||
let top = a.bottom + gap
|
||||
// Flips above when there is no room below, which is most of the rail on a short
|
||||
// window: a panel clipped by the viewport is one that cannot be used at all.
|
||||
if (top + p.height > window.innerHeight - 8) top = Math.max(8, a.top - p.height - gap)
|
||||
// Centred on the column, not on the number. The number sits hard against the left
|
||||
// edge of the rail, so a panel centred on it hangs half off the sidebar and points at
|
||||
// the map. Vertically it still follows the row it belongs to.
|
||||
const column = anchor.closest('section')?.getBoundingClientRect() ?? a
|
||||
const left = Math.min(
|
||||
Math.max(8, column.left + column.width / 2 - p.width / 2),
|
||||
window.innerWidth - p.width - 8,
|
||||
)
|
||||
setPos({ top, left })
|
||||
}, [anchor])
|
||||
|
||||
useEffect(() => {
|
||||
if (!anchor) return
|
||||
const onDown = (e: MouseEvent) => {
|
||||
if (ref.current?.contains(e.target as Node)) return
|
||||
if (anchor.contains(e.target as Node)) return
|
||||
onClose()
|
||||
}
|
||||
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose() }
|
||||
document.addEventListener('mousedown', onDown)
|
||||
document.addEventListener('keydown', onKey)
|
||||
// Any scroll moves the anchor out from under the panel, and following it would mean
|
||||
// measuring on every frame for a menu that is open for two seconds.
|
||||
window.addEventListener('scroll', onClose, true)
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', onDown)
|
||||
document.removeEventListener('keydown', onKey)
|
||||
window.removeEventListener('scroll', onClose, true)
|
||||
}
|
||||
}, [anchor, onClose])
|
||||
|
||||
if (!anchor) return null
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
ref={ref}
|
||||
role="dialog"
|
||||
aria-label={t('roadtrip.stop.kind')}
|
||||
className="fixed z-[70] rounded-2xl border border-edge bg-surface-card p-2 shadow-xl"
|
||||
style={{ top: pos?.top ?? -9999, left: pos?.left ?? -9999, visibility: pos ? 'visible' : 'hidden' }}
|
||||
>
|
||||
<div className="flex gap-1">
|
||||
{STOP_KINDS.map(({ key, labelKey, Icon, color }) => {
|
||||
const on = current === key
|
||||
return (
|
||||
<Tooltip key={key} label={t(labelKey)}>
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed={on}
|
||||
aria-label={t(labelKey)}
|
||||
onClick={() => onPick(on ? null : key)}
|
||||
className="group grid h-11 w-11 place-items-center rounded-xl transition-colors hover:bg-surface-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent"
|
||||
>
|
||||
<span
|
||||
className={`${DISC} h-7 w-7 group-hover:scale-110 ${on ? 'ring-2 ring-offset-2 ring-offset-surface-card' : ''}`}
|
||||
// theme-lint-disable — the road-signage palette from `roadtripModel`, the
|
||||
// same one the rail disc and the map pin use, so the choice looks like
|
||||
// what it will become.
|
||||
style={{ background: color, color: '#fff', ...(on ? { boxShadow: `0 0 0 2px ${color}` } : {}) }}
|
||||
>
|
||||
<Icon size={14} strokeWidth={2.2} aria-hidden />
|
||||
</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Only once there is something to undo. On an ordinary place it would be a button
|
||||
that says "leave everything as it is". */}
|
||||
{current ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onPick(null)}
|
||||
className="mt-1 flex w-full items-center gap-2 rounded-xl px-2.5 py-2 text-caption text-content-secondary transition-colors hover:bg-surface-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent"
|
||||
>
|
||||
<MapPin size={14} className="shrink-0 text-content-faint" aria-hidden />
|
||||
{t('roadtrip.stop.backToDestination')}
|
||||
</button>
|
||||
) : null}
|
||||
</div>,
|
||||
document.body,
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user