mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
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:
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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': 'تمت إضافة المكان',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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é',
|
||||
|
||||
@@ -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': 'Το μέρος προστέθηκε',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': '場所を追加しました',
|
||||
|
||||
@@ -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': '장소가 추가되었습니다',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': 'Место добавлено',
|
||||
|
||||
@@ -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é',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': "เพิ่มสถานที่แล้ว",
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': 'Місце додано',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': '地點已新增',
|
||||
|
||||
@@ -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': '地点已添加',
|
||||
|
||||
Reference in New Issue
Block a user