Files
TREK/client/src/components/Planner/PlacesSidebar.tsx
T
Maurice 2879433bd3 fix(dawarich): the panel opens to its full height and the rail scrolls past it
Capping it was the wrong half of the fix: the stays were reachable but the panel was cut
off at half the rail, which is not what opening something means.

It sits inside the places list's own scroller now, above the rows, so it simply grows to
whatever the day's stays need — 3350px for forty of them — and the rail scrolls past it
to the places. No second scrollbar, no fold. As a band of its own it could never do that:
the list is the flex child that scrolls, so anything unbounded above it squeezed the list
to nothing and took the rail's scrolling with it.
2026-09-17 15:05:29 +02:00

130 lines
5.8 KiB
TypeScript

import React from 'react'
import { ContextMenu } from '../shared/ContextMenu'
import FileImportModal from './FileImportModal'
import ConfirmDialog from '../shared/ConfirmDialog'
import { usePlacesSidebar, type PlacesSidebarProps } from './usePlacesSidebar'
import { PlacesDropOverlay, PlacesHeader } from './PlacesSidebarHeader'
import { PlacesSelectionBar } from './PlacesSidebarSelectionBar'
import { PlacesList } from './PlacesSidebarList'
import { MobileDayPickerSheet } from './PlacesSidebarMobileDayPicker'
import { ListImportModal } from './PlacesSidebarListImportModal'
import { PlacesBulkCategoryModal } from './PlacesBulkCategoryModal'
import SaveTripPlacesToListModal from '../Collections/SaveTripPlacesToListModal'
import DawarichSuggestionsPanel from '../Dawarich/DawarichSuggestionsPanel'
import { formatDayOption } from '../Dawarich/dawarichSuggestionModel'
import { refreshTripAfterAccept } from '../Dawarich/dawarichTripRefresh'
import { useTranslation } from '../../i18n'
const PlacesSidebar = React.memo(function PlacesSidebar(props: PlacesSidebarProps) {
const S = usePlacesSidebar(props)
const {
sidebarDragOver, handleSidebarDragEnter, handleSidebarDragOver, handleSidebarDragLeave, handleSidebarDrop,
selectMode, filtered, t, dayPickerPlace, listImportOpen,
fileImportOpen, setFileImportOpen, sidebarDropFile, setSidebarDropFile, tripId, pushUndo,
ctxMenu, isMobile, pendingDeleteIds, setPendingDeleteIds, onBulkDeleteConfirm,
categories, selectedIds, exitSelectMode, onBulkChangeCategory, categoryPickerOpen, setCategoryPickerOpen,
collectionsEnabled, saveToListOpen, setSaveToListOpen, days,
} = S
// The sidebar hook carries `t` but not the locale; day labels need both.
const { locale } = useTranslation()
// Below lg the places sit in their own tab with no plan beside them to drag
// into. A coarse pointer no longer disables the drag on its own — tablets
// reach it through a long press (#1616).
const dragDisabled = isMobile
return (
<div
data-touch-drag={dragDisabled ? undefined : ''}
onDragEnter={dragDisabled ? undefined : handleSidebarDragEnter}
onDragOver={dragDisabled ? undefined : handleSidebarDragOver}
onDragLeave={dragDisabled ? undefined : handleSidebarDragLeave}
onDrop={dragDisabled ? undefined : handleSidebarDrop}
style={{ display: 'flex', flexDirection: 'column', height: '100%', fontFamily: "var(--font-system)", position: 'relative' }}
>
{!dragDisabled && sidebarDragOver && <PlacesDropOverlay {...S} />}
{/* Kopfbereich */}
<PlacesHeader {...S} />
{/* Anzahl / Auswahl-Leiste */}
{selectMode ? (
<PlacesSelectionBar {...S} />
) : (
<div style={{ padding: '6px 16px', flexShrink: 0 }}>
{/* A badge across the whole rail rather than a line of text hugging the
left edge: it reads as the list's header instead of as a stray label.
Outlined rather than filled, because the tertiary surface is a slate
tone and put a blue cast on the panel. */}
<div className="text-content-faint" style={{
display: 'flex', alignItems: 'center', justifyContent: 'center',
padding: '4px 10px', borderRadius: 99,
background: 'transparent', border: '1px solid var(--border-faint)',
fontSize: 'calc(11px * var(--fs-scale-caption, 1))', fontWeight: 600,
textTransform: 'uppercase', letterSpacing: '0.06em',
}}>
{filtered.length === 1 ? t('places.countSingular') : t('places.count', { count: filtered.length })}
</div>
</div>
)}
{/* Liste, with the Dawarich stays riding on top of it inside the same scroller —
see the `header` prop for why they are not a band of their own. */}
<PlacesList
{...S}
header={(
<div style={{ padding: '0 12px 8px' }}>
<DawarichSuggestionsPanel
tripId={tripId}
trips={[{ id: tripId, label: t('dawarich.accept.thisTrip') }]}
daysForTrip={() => days.map(day => ({
id: day.id,
...formatDayOption(day.day_number, day.date, locale, t),
}))}
// The place it just created belongs on the map and in the list
// now, not after a reload.
onAccepted={() => { void refreshTripAfterAccept(tripId) }}
initiallyCollapsed
/>
</div>
)}
/>
{dayPickerPlace && <MobileDayPickerSheet {...S} />}
{listImportOpen && <ListImportModal {...S} />}
<FileImportModal
isOpen={fileImportOpen}
onClose={() => { setFileImportOpen(false); setSidebarDropFile(null) }}
tripId={tripId}
pushUndo={pushUndo}
initialFile={sidebarDropFile}
/>
<ContextMenu menu={ctxMenu.menu} onClose={ctxMenu.close} />
{categoryPickerOpen && (
<PlacesBulkCategoryModal
count={selectedIds.size}
categories={categories}
onClose={() => setCategoryPickerOpen(false)}
onPick={(catId) => { onBulkChangeCategory?.(Array.from(selectedIds), catId); setCategoryPickerOpen(false); exitSelectMode() }}
/>
)}
{collectionsEnabled && (
<SaveTripPlacesToListModal
isOpen={saveToListOpen}
tripId={tripId}
placeIds={Array.from(selectedIds)}
onClose={() => setSaveToListOpen(false)}
onDone={exitSelectMode}
/>
)}
{isMobile && (
<ConfirmDialog
isOpen={!!pendingDeleteIds?.length}
onClose={() => setPendingDeleteIds(null)}
onConfirm={() => { onBulkDeleteConfirm?.(pendingDeleteIds!); setPendingDeleteIds(null) }}
message={t('trip.confirm.deletePlaces', { count: pendingDeleteIds?.length ?? 0 })}
/>
)}
</div>
)
})
export default PlacesSidebar