feat(planner): the panel widths can be dragged with a finger, also on a tablet

The handles on the day plan and the places panel only listened to the mouse, so on an iPad they did nothing, and in the portrait band they were left out. They now follow a finger as well, show a grip on touch screens, keep the page and the map from taking the gesture, and stay in the narrow layout within the room the map needs. As separators they also move with the arrow keys. (#1012)
This commit is contained in:
Maurice
2026-10-01 10:04:18 +02:00
parent 542a57b74e
commit 9f0fe456a4
33 changed files with 193 additions and 35 deletions
@@ -0,0 +1,51 @@
import type { KeyboardEvent } from 'react'
import { useTranslation } from '../../i18n'
const STEP = 16
interface PanelResizeHandleProps {
/** The panel this handle sits on: 'left' grows to the right, 'right' to the left. */
side: 'left' | 'right'
width: number
min: number
max: number
onStart: () => void
onNudge: (delta: number) => void
}
/**
* The grip on a planner panel's inner edge (#1012). A mouse drags it, a finger on
* a tablet drags it (touch-action: none keeps the page and the map from taking
* the gesture), and the arrow keys move it, since it is a separator a keyboard
* can reach. On a touch screen the grip shows; with a mouse it lights on hover.
*/
export default function PanelResizeHandle({ side, width, min, max, onStart, onNudge }: PanelResizeHandleProps) {
const { t } = useTranslation()
const onKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
const grow = side === 'left' ? 'ArrowRight' : 'ArrowLeft'
const shrink = side === 'left' ? 'ArrowLeft' : 'ArrowRight'
if (e.key !== grow && e.key !== shrink) return
e.preventDefault()
onNudge(e.key === grow ? STEP : -STEP)
}
return (
<div
role="separator"
aria-orientation="vertical"
aria-label={t('trip.panelWidth')}
aria-valuenow={Math.round(width)}
aria-valuemin={min}
aria-valuemax={max}
tabIndex={0}
data-resize={side}
onMouseDown={onStart}
onTouchStart={onStart}
onKeyDown={onKeyDown}
className="group flex justify-center outline-none"
style={{ position: 'absolute', [side === 'left' ? 'right' : 'left']: 0, top: 0, bottom: 0, width: 10, cursor: 'col-resize', zIndex: 2, touchAction: 'none' }}
>
<span aria-hidden className="h-full w-[3px] rounded-full transition-colors group-hover:bg-edge group-focus-visible:bg-accent" />
<span aria-hidden className="pointer-events-none absolute top-1/2 hidden h-10 w-[5px] -translate-y-1/2 rounded-full bg-content-faint opacity-60 [@media(pointer:coarse)]:block" />
</div>
)
}
+38 -12
View File
@@ -12,6 +12,16 @@ const NARROW_QUERY = '(min-width: 768px) and (max-width: 1023px)'
/** However wide a panel was dragged on a big screen, the map keeps this much. */
const MIN_MAP = 360
function clampWidth(w: number, max: number = MAX_SIDEBAR): number {
return Math.max(MIN_SIDEBAR, Math.min(max, w))
}
/** Stores a dragged width and hands it back; a blocked storage only costs the memory of it. */
function remember(key: string, w: number): number {
try { localStorage.setItem(key, String(w)) } catch { /* not remembered, still applied */ }
return w
}
export function useResizablePanels() {
const [leftWidth, setLeftWidth] = useState<number>(() => Number.parseInt(localStorage.getItem('sidebarLeftWidth') || '') || 340)
const [rightWidth, setRightWidth] = useState<number>(() => Number.parseInt(localStorage.getItem('sidebarRightWidth') || '') || 300)
@@ -48,18 +58,21 @@ export function useResizablePanels() {
return () => window.removeEventListener('resize', measure)
}, [narrow])
// A mouse drags through mousemove; a finger on a tablet sends touch events and
// never a mousemove, which is why the handles used to be dead on an iPad (#1012).
useEffect(() => {
const onMove = (e: MouseEvent) => {
if (isResizingLeft.current) {
const w = Math.max(MIN_SIDEBAR, Math.min(MAX_SIDEBAR, e.clientX - 10))
setLeftWidth(w)
localStorage.setItem('sidebarLeftWidth', String(w))
}
if (isResizingRight.current) {
const w = Math.max(MIN_SIDEBAR, Math.min(MAX_SIDEBAR, window.innerWidth - e.clientX - 10))
setRightWidth(w)
localStorage.setItem('sidebarRightWidth', String(w))
}
const moveTo = (clientX: number) => {
if (isResizingLeft.current) setLeftWidth(remember('sidebarLeftWidth', clampWidth(clientX - 10)))
if (isResizingRight.current) setRightWidth(remember('sidebarRightWidth', clampWidth(window.innerWidth - clientX - 10)))
}
const onMove = (e: MouseEvent) => moveTo(e.clientX)
const onTouchMove = (e: TouchEvent) => {
if (!isResizingLeft.current && !isResizingRight.current) return
const touch = e.touches[0]
if (!touch) return
// The drag owns the finger: no page scroll and no map pan underneath it.
e.preventDefault()
moveTo(touch.clientX)
}
const onUp = () => {
isResizingLeft.current = false
@@ -69,15 +82,27 @@ export function useResizablePanels() {
}
document.addEventListener('mousemove', onMove)
document.addEventListener('mouseup', onUp)
document.addEventListener('touchmove', onTouchMove, { passive: false })
document.addEventListener('touchend', onUp)
document.addEventListener('touchcancel', onUp)
return () => {
document.removeEventListener('mousemove', onMove)
document.removeEventListener('mouseup', onUp)
document.removeEventListener('touchmove', onTouchMove)
document.removeEventListener('touchend', onUp)
document.removeEventListener('touchcancel', onUp)
}
}, [])
const startResizeLeft = () => { isResizingLeft.current = true; document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none' }
const startResizeRight = () => { isResizingRight.current = true; document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none' }
// The handles are separators a keyboard can move too: an arrow key grows or
// shrinks the panel by a step, from the width it shows right now.
const resizeMax = Math.max(MIN_SIDEBAR, Math.min(MAX_SIDEBAR, maxPanel))
const nudgeLeft = (delta: number) => setLeftWidth(remember('sidebarLeftWidth', clampWidth(Math.min(leftWidth, maxPanel) + delta, resizeMax)))
const nudgeRight = (delta: number) => setRightWidth(remember('sidebarRightWidth', clampWidth(Math.min(rightWidth, maxPanel) + delta, resizeMax)))
// What the layout actually shows. The collapse flags stay the caller's intent —
// handlePlaceClick reopening "both" must not re-crowd a narrow screen.
const leftHidden = narrow ? narrowPanel !== 'left' : leftCollapsed
@@ -99,6 +124,7 @@ export function useResizablePanels() {
rightWidth: Math.min(rightWidth, maxPanel),
leftCollapsed, rightCollapsed, setLeftCollapsed, setRightCollapsed,
leftHidden, rightHidden, toggleLeft, toggleRight, narrow,
startResizeLeft, startResizeRight,
startResizeLeft, startResizeRight, nudgeLeft, nudgeRight,
resizeMin: MIN_SIDEBAR, resizeMax,
}
}
+7 -16
View File
@@ -58,6 +58,7 @@ import { usePoiExplore } from '../components/Map/usePoiExplore'
import { useMergedMapPois } from '../components/Map/useMergedMapPois'
import PoiCategoryPill from '../components/Map/PoiCategoryPill'
import { useTouchDragBridge } from '../hooks/useTouchDragBridge'
import PanelResizeHandle from '../components/Planner/PanelResizeHandle'
// The tab panels are the planner's dead weight: each one mounts only while its
// own tab is active, so the page chunk carried code most sessions never run. They
@@ -285,8 +286,8 @@ function TripPlannerPageDesktop(): React.ReactElement | null {
allowedFileTypes, tripMembers, setTripMembers, refreshMembers, loadAccommodations,
TRANSPORT_TYPES, TRIP_TABS, activeTab, setActiveTab, handleTabChange,
leftWidth, rightWidth,
leftHidden, rightHidden, toggleLeft, toggleRight, narrowPanels,
startResizeLeft, startResizeRight,
leftHidden, rightHidden, toggleLeft, toggleRight,
startResizeLeft, startResizeRight, nudgeLeft, nudgeRight, resizeMin, resizeMax,
selectedPlaceId, selectedAssignmentId, setSelectedPlaceId, selectAssignment,
showDayDetail, setShowDayDetail, dayDetailCollapsed, setDayDetailCollapsed,
stayPickerDayId, setStayPickerDayId,
@@ -741,13 +742,8 @@ function TripPlannerPageDesktop(): React.ReactElement | null {
onCreatePlaceForDay={can('place_edit', trip) ? (dayId) => { setEditingPlace(null); setPlaceFormDayId(dayId); setShowPlaceForm(true) } : undefined}
/>
)}
{!leftHidden && !narrowPanels && (
<div
role="presentation"
onMouseDown={startResizeLeft}
className="transition-colors hover:bg-edge"
style={{ position: 'absolute', right: 0, top: 0, bottom: 0, width: 4, cursor: 'col-resize', zIndex: 2 }}
/>
{!leftHidden && (
<PanelResizeHandle side="left" width={leftWidth} min={resizeMin} max={resizeMax} onStart={startResizeLeft} onNudge={nudgeLeft} />
)}
</div>
</div>
@@ -781,13 +777,8 @@ function TripPlannerPageDesktop(): React.ReactElement | null {
transition: 'width 0.25s ease',
opacity: rightHidden ? 0 : 1,
}}>
{!rightHidden && !narrowPanels && (
<div
role="presentation"
onMouseDown={startResizeRight}
className="transition-colors hover:bg-edge"
style={{ position: 'absolute', left: 0, top: 0, bottom: 0, width: 4, cursor: 'col-resize', zIndex: 2 }}
/>
{!rightHidden && (
<PanelResizeHandle side="right" width={rightWidth} min={resizeMin} max={resizeMax} onStart={startResizeRight} onNudge={nudgeRight} />
)}
<div style={{ flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
{roadtripActive ? (
@@ -227,6 +227,10 @@ function baseState(): HookState {
narrowPanels: false,
startResizeLeft: vi.fn(),
startResizeRight: vi.fn(),
nudgeLeft: vi.fn(),
nudgeRight: vi.fn(),
resizeMin: 200,
resizeMax: 520,
selectedPlaceId: null,
selectedAssignmentId: null,
setSelectedPlaceId: vi.fn(),
@@ -578,11 +582,28 @@ describe('TripPlannerPage — plan tab', () => {
expect(screen.getByTestId('day-plan-sidebar')).toBeInTheDocument()
})
// Mouse-only handles (mousedown + document mousemove) are inert on a tablet, and
// a 4px invisible strip on the panel edge is a trap for a fat finger (#2247).
it('FE-PAGE-TPW-015b: the narrow layout drops the mouse-only resize handles', () => {
const { container } = renderPage({ narrowPanels: true })
expect(container.querySelectorAll('div[style*="col-resize"]')).toHaveLength(0)
// The handles take a finger now, with a visible grip on touch screens, so the
// tablet band keeps the one it shows (#1012); before, a 4px mouse-only strip there
// was a trap for a fat finger (#2247).
it('FE-PAGE-TPW-015b: the narrow layout keeps a touch-ready handle on the panel it shows', () => {
const { container } = renderPage({ narrowPanels: true, rightHidden: true })
const handles = container.querySelectorAll<HTMLElement>('div[role="separator"]')
expect(handles).toHaveLength(1)
expect(handles[0].style.touchAction).toBe('none')
fireEvent.touchStart(handles[0])
expect(hookState.startResizeLeft).toHaveBeenCalled()
})
it('FE-PAGE-TPW-015c: the arrow keys move a handle like a separator (#1012)', () => {
const { container } = renderPage()
const [left, right] = container.querySelectorAll<HTMLElement>('div[role="separator"]')
expect(left).toHaveAttribute('aria-valuenow')
fireEvent.keyDown(left, { key: 'ArrowRight' })
expect(hookState.nudgeLeft).toHaveBeenCalledWith(16)
fireEvent.keyDown(right, { key: 'ArrowLeft' })
expect(hookState.nudgeRight).toHaveBeenCalledWith(16)
fireEvent.keyDown(right, { key: 'ArrowRight' })
expect(hookState.nudgeRight).toHaveBeenCalledWith(-16)
})
it('FE-PAGE-TPW-015: the resize handles start a drag on mouse-down and highlight on hover', () => {
@@ -310,7 +310,7 @@ export function useTripPlanner() {
const {
leftWidth, rightWidth, leftCollapsed, rightCollapsed, setLeftCollapsed, setRightCollapsed,
leftHidden, rightHidden, toggleLeft, toggleRight, narrow: narrowPanels,
startResizeLeft, startResizeRight,
startResizeLeft, startResizeRight, nudgeLeft, nudgeRight, resizeMin, resizeMax,
} = useResizablePanels()
const { selectedPlaceId, selectedAssignmentId, setSelectedPlaceId, selectAssignment } = usePlaceSelection()
const [dayDetail, setShowDayDetail] = useState<Day | null>(null)
@@ -2957,7 +2957,7 @@ export function useTripPlanner() {
TRANSPORT_TYPES, TRIP_TABS, activeTab, setActiveTab, handleTabChange,
leftWidth, rightWidth, leftCollapsed, rightCollapsed, setLeftCollapsed, setRightCollapsed,
leftHidden, rightHidden, toggleLeft, toggleRight, narrowPanels,
startResizeLeft, startResizeRight,
startResizeLeft, startResizeRight, nudgeLeft, nudgeRight, resizeMin, resizeMax,
selectedPlaceId, selectedAssignmentId, setSelectedPlaceId, selectAssignment,
showDayDetail, setShowDayDetail, dayDetailCollapsed, setDayDetailCollapsed,
stayPickerDayId, setStayPickerDayId,
@@ -162,6 +162,48 @@ describe('useResizablePanels', () => {
expect(result.current.rightWidth).toBe(initialRight);
});
it('FE-HOOK-PANELS-011b: a finger drags the left panel on a tablet, and lifting it ends the drag (#1012)', () => {
const { result } = renderHook(() => useResizablePanels());
act(() => {
result.current.startResizeLeft();
});
act(() => {
fireEvent.touchMove(document, { touches: [{ clientX: 410 }] });
});
expect(result.current.leftWidth).toBe(400);
expect(localStorage.getItem('sidebarLeftWidth')).toBe('400');
act(() => {
fireEvent.touchEnd(document);
});
act(() => {
fireEvent.touchMove(document, { touches: [{ clientX: 300 }] });
});
expect(result.current.leftWidth).toBe(400);
expect(document.body.style.cursor).toBe('');
});
it('FE-HOOK-PANELS-011c: the keyboard nudges a panel within its bounds (#1012)', () => {
const { result } = renderHook(() => useResizablePanels());
const start = result.current.leftWidth;
act(() => {
result.current.nudgeLeft(16);
});
expect(result.current.leftWidth).toBe(start + 16);
act(() => {
result.current.nudgeLeft(-10_000);
});
expect(result.current.leftWidth).toBe(result.current.resizeMin);
act(() => {
result.current.nudgeRight(10_000);
});
expect(result.current.rightWidth).toBe(result.current.resizeMax);
});
it('FE-HOOK-PANELS-012: body userSelect set to none during resize, cleared on mouseup', () => {
const { result } = renderHook(() => useResizablePanels());
+1
View File
@@ -18,6 +18,7 @@ const trip: TranslationStrings = {
'trip.loadingSteps.arrive': 'أوشكنا على الوصول...',
'trip.mobilePlan': 'الخطة',
'trip.mobilePlaces': 'الأماكن',
'trip.panelWidth': 'عرض اللوحة',
'trip.toast.placeUpdated': 'تم تحديث المكان',
'trip.toast.tripUpdated': 'تم تحديث الرحلة',
'trip.toast.placeAdded': 'تمت إضافة المكان',
+1
View File
@@ -20,6 +20,7 @@ const trip: TranslationStrings = {
'trip.mobilePlan': 'Plan',
'trip.mobilePlaces': 'Məkanlar',
'trip.panelWidth': 'Panelin eni',
'trip.toast.placeUpdated': 'Məkan yeniləndi',
'trip.toast.tripUpdated': 'Səyahət yeniləndi',
+1
View File
@@ -14,6 +14,7 @@ const trip: TranslationStrings = {
'trip.loading': 'Carregando viagem...',
'trip.mobilePlan': 'Plano',
'trip.mobilePlaces': 'Lugares',
'trip.panelWidth': 'Largura do painel',
'trip.toast.placeUpdated': 'Lugar atualizado',
'trip.toast.tripUpdated': 'Viagem atualizada',
'trip.toast.placeAdded': 'Lugar adicionado',
+1
View File
@@ -15,6 +15,7 @@ const trip: TranslationStrings = {
'trip.loadingPhotos': 'Carregant les fotos del lloc...',
'trip.mobilePlan': 'Planificació',
'trip.mobilePlaces': 'Llocs',
'trip.panelWidth': 'Amplada del tauler',
'trip.toast.placeUpdated': 'Lloc actualitzat',
'trip.toast.placeAdded': 'Lloc afegit',
'trip.toast.placeDeleted': 'Lloc eliminat',
+1
View File
@@ -18,6 +18,7 @@ const trip: TranslationStrings = {
'trip.loadingSteps.arrive': 'Už jsme skoro tam...',
'trip.mobilePlan': 'Plán',
'trip.mobilePlaces': 'Místa',
'trip.panelWidth': 'Šířka panelu',
'trip.toast.placeUpdated': 'Místo bylo aktualizováno',
'trip.toast.tripUpdated': 'Cesta aktualizována',
'trip.toast.placeAdded': 'Místo bylo přidáno',
+1
View File
@@ -18,6 +18,7 @@ const trip: TranslationStrings = {
'trip.loadingSteps.arrive': 'Fast am Ziel...',
'trip.mobilePlan': 'Planung',
'trip.mobilePlaces': 'Orte',
'trip.panelWidth': 'Breite des Bereichs',
'trip.toast.placeUpdated': 'Ort aktualisiert',
'trip.toast.tripUpdated': 'Reise aktualisiert',
'trip.toast.placeAdded': 'Ort hinzugefügt',
+1
View File
@@ -18,6 +18,7 @@ const trip: TranslationStrings = {
'trip.loadingSteps.arrive': 'Almost there...',
'trip.mobilePlan': 'Plan',
'trip.mobilePlaces': 'Places',
'trip.panelWidth': 'Panel width',
'trip.toast.placeUpdated': 'Place updated',
'trip.toast.tripUpdated': 'Trip updated',
'trip.toast.placeAdded': 'Place added',
+1
View File
@@ -18,6 +18,7 @@ const trip: TranslationStrings = {
'trip.loadingSteps.arrive': 'Ya casi llegamos...',
'trip.mobilePlan': 'Plan',
'trip.mobilePlaces': 'Lugares',
'trip.panelWidth': 'Ancho del panel',
'trip.toast.placeUpdated': 'Lugar actualizado',
'trip.toast.tripUpdated': 'Viaje actualizado',
'trip.toast.placeAdded': 'Lugar añadido',
+1
View File
@@ -18,6 +18,7 @@ const trip: TranslationStrings = {
'trip.loadingSteps.arrive': 'Peaaegu kohal...',
'trip.mobilePlan': 'Plaan',
'trip.mobilePlaces': 'Kohad',
'trip.panelWidth': 'Paneeli laius',
'trip.toast.placeUpdated': 'Koht uuendatud',
'trip.toast.tripUpdated': 'Reis uuendatud',
'trip.toast.placeAdded': 'Koht lisatud',
+1
View File
@@ -18,6 +18,7 @@ const trip: TranslationStrings = {
'trip.loadingSteps.arrive': 'On y est presque...',
'trip.mobilePlan': 'Plan',
'trip.mobilePlaces': 'Lieux',
'trip.panelWidth': 'Largeur du panneau',
'trip.toast.placeUpdated': 'Lieu mis à jour',
'trip.toast.tripUpdated': 'Voyage mis à jour',
'trip.toast.placeAdded': 'Lieu ajouté',
+1
View File
@@ -18,6 +18,7 @@ const trip: TranslationStrings = {
'trip.loadingSteps.arrive': 'Σχεδόν φτάσαμε...',
'trip.mobilePlan': 'Πλάνο',
'trip.mobilePlaces': 'Μέρη',
'trip.panelWidth': 'Πλάτος πλαισίου',
'trip.toast.placeUpdated': 'Το μέρος ενημερώθηκε',
'trip.toast.tripUpdated': 'Το ταξίδι ενημερώθηκε',
'trip.toast.placeAdded': 'Το μέρος προστέθηκε',
+1
View File
@@ -14,6 +14,7 @@ const trip: TranslationStrings = {
'trip.loading': 'Utazás betöltése...',
'trip.mobilePlan': 'Tervezés',
'trip.mobilePlaces': 'Helyek',
'trip.panelWidth': 'Panel szélessége',
'trip.toast.placeUpdated': 'Hely frissítve',
'trip.toast.tripUpdated': 'Utazás frissítve',
'trip.toast.placeAdded': 'Hely hozzáadva',
+1
View File
@@ -18,6 +18,7 @@ const trip: TranslationStrings = {
'trip.loadingSteps.arrive': 'Hampir sampai...',
'trip.mobilePlan': 'Rencana',
'trip.mobilePlaces': 'Tempat',
'trip.panelWidth': 'Lebar panel',
'trip.toast.placeUpdated': 'Tempat diperbarui',
'trip.toast.tripUpdated': 'Perjalanan diperbarui',
'trip.toast.placeAdded': 'Tempat ditambahkan',
+1
View File
@@ -14,6 +14,7 @@ const trip: TranslationStrings = {
'trip.loading': 'Caricamento viaggio...',
'trip.mobilePlan': 'Programma',
'trip.mobilePlaces': 'Luoghi',
'trip.panelWidth': 'Larghezza del pannello',
'trip.toast.placeUpdated': 'Luogo aggiornato',
'trip.toast.tripUpdated': 'Viaggio aggiornato',
'trip.toast.placeAdded': 'Luogo aggiunto',
+1
View File
@@ -18,6 +18,7 @@ const trip: TranslationStrings = {
'trip.loadingSteps.arrive': 'もうすぐ到着...',
'trip.mobilePlan': '計画',
'trip.mobilePlaces': '場所',
'trip.panelWidth': 'パネルの幅',
'trip.toast.placeUpdated': '場所を更新しました',
'trip.toast.tripUpdated': '旅行を更新しました',
'trip.toast.placeAdded': '場所を追加しました',
+1
View File
@@ -18,6 +18,7 @@ const trip: TranslationStrings = {
'trip.loadingSteps.arrive': '거의 다 왔어요...',
'trip.mobilePlan': '계획',
'trip.mobilePlaces': '장소',
'trip.panelWidth': '패널 너비',
'trip.toast.placeUpdated': '장소가 업데이트되었습니다',
'trip.toast.tripUpdated': '여행이 업데이트됨',
'trip.toast.placeAdded': '장소가 추가되었습니다',
+1
View File
@@ -18,6 +18,7 @@ const trip: TranslationStrings = {
'trip.loadingSteps.arrive': 'Bijna aangekomen...',
'trip.mobilePlan': 'Plan',
'trip.mobilePlaces': 'Plaatsen',
'trip.panelWidth': 'Paneelbreedte',
'trip.toast.placeUpdated': 'Plaats bijgewerkt',
'trip.toast.tripUpdated': 'Reis bijgewerkt',
'trip.toast.placeAdded': 'Plaats toegevoegd',
+1
View File
@@ -14,6 +14,7 @@ const trip: TranslationStrings = {
'trip.loading': 'Ładowanie podróży...',
'trip.mobilePlan': 'Plan',
'trip.mobilePlaces': 'Miejsca',
'trip.panelWidth': 'Szerokość panelu',
'trip.toast.placeUpdated': 'Miejsce zostało zaktualizowane',
'trip.toast.tripUpdated': 'Podróż zaktualizowana',
'trip.toast.placeAdded': 'Miejsce zostało dodane',
+1
View File
@@ -18,6 +18,7 @@ const trip: TranslationStrings = {
'trip.loadingSteps.arrive': 'Почти на месте...',
'trip.mobilePlan': 'План',
'trip.mobilePlaces': 'Места',
'trip.panelWidth': 'Ширина панели',
'trip.toast.placeUpdated': 'Место обновлено',
'trip.toast.tripUpdated': 'Поездка обновлена',
'trip.toast.placeAdded': 'Место добавлено',
+1
View File
@@ -18,6 +18,7 @@ const trip: TranslationStrings = {
'trip.loadingSteps.arrive': 'Už sme skoro tam...',
'trip.mobilePlan': 'Plán',
'trip.mobilePlaces': 'Miesta',
'trip.panelWidth': 'Šírka panela',
'trip.toast.placeUpdated': 'Miesto bolo aktualizované',
'trip.toast.tripUpdated': 'Cesta aktualizovaná',
'trip.toast.placeAdded': 'Miesto bolo pridané',
+1
View File
@@ -18,6 +18,7 @@ const trip: TranslationStrings = {
'trip.loadingSteps.arrive': 'Snart framme...',
'trip.mobilePlan': 'Plan',
'trip.mobilePlaces': 'Platser',
'trip.panelWidth': 'Panelens bredd',
'trip.toast.placeUpdated': 'Plats uppdaterad',
'trip.toast.tripUpdated': 'Resan uppdaterad',
'trip.toast.placeAdded': 'Plats tillagd',
+1
View File
@@ -18,6 +18,7 @@ const trip: TranslationStrings = {
'trip.loadingSteps.arrive': "เกือบจะถึงแล้ว...",
'trip.mobilePlan': "แผน",
'trip.mobilePlaces': "สถานที่",
'trip.panelWidth': 'ความกว้างของแผง',
'trip.toast.placeUpdated': "อัปเดตสถานที่แล้ว",
'trip.toast.tripUpdated': "อัปเดตการเดินทางแล้ว",
'trip.toast.placeAdded': "เพิ่มสถานที่แล้ว",
+1
View File
@@ -18,6 +18,7 @@ const trip: TranslationStrings = {
'trip.loadingSteps.arrive': 'Az kaldı...',
'trip.mobilePlan': 'Plan',
'trip.mobilePlaces': 'Yerler',
'trip.panelWidth': 'Panel genişliği',
'trip.toast.placeUpdated': 'Yer güncellendi',
'trip.toast.tripUpdated': 'Gezi güncellendi',
'trip.toast.placeAdded': 'Yer eklendi',
+1
View File
@@ -18,6 +18,7 @@ const trip: TranslationStrings = {
'trip.loadingSteps.arrive': 'Майже на місці...',
'trip.mobilePlan': 'План',
'trip.mobilePlaces': 'Місця',
'trip.panelWidth': 'Ширина панелі',
'trip.toast.placeUpdated': 'Місце оновлено',
'trip.toast.tripUpdated': 'Подорож оновлено',
'trip.toast.placeAdded': 'Місце додано',
+1
View File
@@ -18,6 +18,7 @@ const trip: TranslationStrings = {
'trip.loadingSteps.arrive': 'Sắp đến nơi rồi...',
'trip.mobilePlan': 'Kế hoạch',
'trip.mobilePlaces': 'Địa điểm',
'trip.panelWidth': 'Độ rộng bảng',
'trip.toast.placeUpdated': 'Đã cập nhật địa điểm',
'trip.toast.tripUpdated': 'Đã cập nhật chuyến đi',
'trip.toast.placeAdded': 'Đã thêm địa điểm',
+1
View File
@@ -18,6 +18,7 @@ const trip: TranslationStrings = {
'trip.loadingSteps.arrive': '就快抵達了...',
'trip.mobilePlan': '計劃',
'trip.mobilePlaces': '地點',
'trip.panelWidth': '面板寬度',
'trip.toast.placeUpdated': '地點已更新',
'trip.toast.tripUpdated': '行程已更新',
'trip.toast.placeAdded': '地點已新增',
+1
View File
@@ -18,6 +18,7 @@ const trip: TranslationStrings = {
'trip.loadingSteps.arrive': '就快到啦...',
'trip.mobilePlan': '计划',
'trip.mobilePlaces': '地点',
'trip.panelWidth': '面板宽度',
'trip.toast.placeUpdated': '地点已更新',
'trip.toast.tripUpdated': '行程已更新',
'trip.toast.placeAdded': '地点已添加',