diff --git a/client/scripts/size-baseline.json b/client/scripts/size-baseline.json
index 340963a11..31fb619e0 100644
--- a/client/scripts/size-baseline.json
+++ b/client/scripts/size-baseline.json
@@ -12,7 +12,7 @@
"src/components/Planner/DayPlanSidebar.test.tsx": 5349,
"src/components/Planner/DayPlanSidebar.tsx": 2910,
"src/components/Planner/PlaceFormModal.test.tsx": 2836,
- "src/components/Planner/PlaceInspector.tsx": 1287,
+ "src/components/Planner/PlaceInspector.tsx": 1129,
"src/components/Studio/TravelElements.tsx": 1559,
"src/components/Studio/countryShapes.ts": 2595,
"src/index.css": 1636,
diff --git a/client/src/components/Planner/PlaceInspector.test.tsx b/client/src/components/Planner/PlaceInspector.test.tsx
index f360162e0..3d842c475 100644
--- a/client/src/components/Planner/PlaceInspector.test.tsx
+++ b/client/src/components/Planner/PlaceInspector.test.tsx
@@ -1831,3 +1831,43 @@ describe('PlaceInspector blur booking codes (#2457)', () => {
expect(isBlurred(screen.getByText(/TABLE-SECRET/))).toBe(false);
});
});
+
+// ── Folding the card over the map (#3039) ─────────────────────────────────────
+
+describe('PlaceInspector folded to its head (#3039)', () => {
+ it('FE-PLANNER-INSPECTOR-112: Collapse folds the card to its name and keeps the place open; Expand unfolds it', async () => {
+ const user = userEvent.setup();
+ const onClose = vi.fn();
+ const { container } = render();
+
+ await user.click(screen.getByRole('button', { name: 'Collapse' }));
+
+ expect(screen.getByText('Eiffel Tower')).toBeInTheDocument();
+ expect(screen.queryByText('Champ de Mars, Paris')).toBeNull();
+ // Hidden, not unmounted: the body and its plugin frames keep their state.
+ expect(screen.getByTestId('inspector-scroll')).toHaveClass('hidden');
+ expect(container.querySelector('footer')).toHaveClass('hidden');
+ expect(screen.getByRole('button', { name: 'Expand' })).toHaveAttribute('aria-expanded', 'false');
+ expect(onClose).not.toHaveBeenCalled();
+
+ await user.click(screen.getByRole('button', { name: 'Expand' }));
+
+ expect(screen.getByText('Champ de Mars, Paris')).toBeInTheDocument();
+ expect(screen.getByTestId('inspector-scroll')).toHaveClass('flex');
+ expect(container.querySelector('footer')).toHaveClass('flex');
+ });
+
+ it('FE-PLANNER-INSPECTOR-113: the next place opens in full, and a card that may not fold offers no button', async () => {
+ const user = userEvent.setup();
+ const { rerender } = render();
+ await user.click(screen.getByRole('button', { name: 'Collapse' }));
+
+ const louvre = buildPlace({ id: 2, name: 'Louvre', address: 'Rue de Rivoli, Paris', lat: 48.86, lng: 2.34 });
+ rerender();
+ expect(screen.getByText('Rue de Rivoli, Paris')).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: 'Collapse' })).toHaveAttribute('aria-expanded', 'true');
+
+ rerender();
+ expect(screen.queryByRole('button', { name: 'Collapse' })).toBeNull();
+ });
+});
diff --git a/client/src/components/Planner/PlaceInspector.tsx b/client/src/components/Planner/PlaceInspector.tsx
index dc1e87eb5..ec8e340e6 100644
--- a/client/src/components/Planner/PlaceInspector.tsx
+++ b/client/src/components/Planner/PlaceInspector.tsx
@@ -6,10 +6,8 @@ import Markdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import remarkBreaks from 'remark-breaks'
import { markdownLinkComponents } from '../shared/markdownLink'
-import { X, Clock, MapPin, ExternalLink, Phone, Mail, Banknote, Pencil, Plus, Minus, ChevronDown, ChevronUp, ChevronRight, FileText, Upload, File, FileImage, Star, Navigation, Mountain, Bookmark, BookmarkCheck, Copy } from 'lucide-react'
+import { Clock, MapPin, ExternalLink, Pencil, Plus, Minus, ChevronDown, ChevronUp, ChevronRight, FileText, Upload, File, FileImage, Navigation, Mountain, Bookmark, BookmarkCheck, Copy } from 'lucide-react'
import { hoursLines, periodsFromWeek } from './placeHours'
-import PlaceAvatar from '../shared/PlaceAvatar'
-import PlacePhotoGallery from './PlacePhotoGallery'
import { BlurredCode } from '../shared/BookingCode'
import PlaceRating from '../shared/StarRating'
import TrackColorPicker from '../shared/TrackColorPicker'
@@ -23,13 +21,11 @@ import { useSettingsStore } from '../../store/settingsStore'
import { useAddonStore } from '../../store/addonStore'
import { useSaveToCollectionStore } from '../../store/saveToCollectionStore'
import { placeToSaveTarget } from '../Collections/saveTarget'
-import DawarichIcon from '../shared/DawarichIcon'
import { Tooltip } from '../shared/Tooltip'
import { useToast } from '../shared/Toast'
import { useTranslation } from '../../i18n'
import { usePluginStore } from '../../store/pluginStore'
import PluginFrame from '../Plugins/PluginFrame'
-import AssistantAskButton from '../Assistant/AssistantAskButton'
import type { Place, Category, Day, Reservation, TripFile, AssignmentsMap, DistanceUnit } from '../../types'
import type { CollectionStatus } from '@trek/shared'
import { splitReservationDateTime, formatTime, formatMoney } from '../../utils/formatters'
@@ -44,10 +40,11 @@ import { resolveOpenNow, resolvePlaceTimeZone, getWeekdayIndex, type OpeningPeri
import { convertHoursLine } from './placeHoursFormat'
import type { EndDayControlProps } from '../Roadtrip/EndDayControl'
import VisitControls, { type RoadtripStayControl } from '../Roadtrip/VisitControls'
-import { DialogButton, DialogSection, DeleteButton, FooterSpacer, NEUTRAL_TINT, fs } from '../shared/DialogShell'
+import { DialogButton, DialogSection, DeleteButton, FooterSpacer, fs } from '../shared/DialogShell'
import { BOX, Field, TypeTile, toneOf, toneTint, useOpenFile } from './bookings/bookingParts'
import { parseMeta } from './bookings/bookingsModel'
-import { SoftPill, TimePill, tintOf } from './planParts'
+import { SoftPill } from './planParts'
+import { InspectorHead } from './PlaceInspectorHead'
import { usePlaceLanguage } from '../../hooks/usePlaceLanguage'
import { participantsWith, participantsWithout, splitParticipants, usePlaceFileUpload } from './usePlaceActions'
@@ -188,6 +185,8 @@ interface PlaceInspectorProps extends MapCardPlacement {
onUpdatePlace?: (placeId: number, data: Partial) => void
/** Cast/clear the current user's star vote (#1435); enables the rating row. */
onRate?: (placeId: number, rating: number | null) => Promise | void
+ /** The card over the map folds to its head (#3039); the phone's modal sheet does not. */
+ collapsible?: boolean
// ── Collection-mode props ──
collectionStatus?: CollectionStatus
onCopyToTrip?: () => void
@@ -195,9 +194,6 @@ interface PlaceInspectorProps extends MapCardPlacement {
onRemoveFromList?: () => void
}
-/** The small pill of the head band, the size of a SoftPill but raised on the tint. */
-const HEAD_PILL = 'inline-flex flex-none items-center gap-1 whitespace-nowrap rounded-full bg-surface-card px-2 py-[2px] font-semibold text-content shadow-sm'
-
/**
* The card a selected place opens over the map, in the planner's dialog
* language: a head band tinted by its category with the avatar, the name, the
@@ -209,7 +205,7 @@ export default function PlaceInspector({
assignments = {}, reservations = [], onEditTransport, onEditReservation, onOpenBooking,
onClose, onEdit: editPlace, onDelete: deletePlace, onAssignToDay, onRemoveAssignment,
files = [], onFileUpload, tripMembers = [], onSetParticipants, onUpdatePlace: updatePlace, onRate,
- leftWidth = 0, rightWidth = 0, clearMapPill = false, cardRef,
+ leftWidth = 0, rightWidth = 0, clearMapPill = false, cardRef, collapsible,
collectionStatus, onCopyToTrip, onSetStatus, onRemoveFromList, roadtripEndDay, roadtripStay, roadtripActive,
}: PlaceInspectorProps) {
// Editing the place is a place right. The planner hands the handlers over regardless, and a member
@@ -256,6 +252,8 @@ export default function PlaceInspector({
const saveVersion = useSaveToCollectionStore(s => s.version)
const [savedInCollection, setSavedInCollection] = useState(false)
const [hoursExpanded, setHoursExpanded] = useState(false)
+ // Folded by place, so the next place selected opens in full.
+ const [collapsedPlaceId, setCollapsedPlaceId] = useState(null)
const [editingName, setEditingName] = useState(false)
const [nameValue, setNameValue] = useState('')
const nameInputRef = useRef(null)
@@ -364,6 +362,7 @@ export default function PlaceInspector({
const description = place.description || googleDetails?.summary
const showRating = (mode === 'trip' && !!onRate) || !!shortReview
const mapCard = centredMapCard(leftWidth, rightWidth, { clearPill: clearMapPill })
+ const collapsed = collapsedPlaceId === place.id
return (
} onUpdatePlace={onUpdatePlace} collapsed={collapsed}
+ onToggleCollapsed={collapsible ? () => setCollapsedPlaceId(collapsed ? null : place.id) : undefined}
onClose={onClose} />
{/* The body scrolls inside the capped card; each block keeps its natural height (#1195). */}
-
+
{/* Collaborative rating (#1435): every member's own vote, shown as the
average, and the first review worth quoting. */}
@@ -476,7 +476,7 @@ export default function PlaceInspector({
{/* Footer: the day action and the ways out on the left, delete and edit on the right. */}
-