feat(planner): fold the place inspector to its head and keep the place selected (#3039)

On a landscape phone or a small tablet the inspector covered most of the map, and the
only way to clear it was closing it, which deselected the place. A chevron in the head
now folds the card to the avatar and name; the next place opens in full. The head band
moves to PlaceInspectorHead so the inspector stays inside its size budget.
This commit is contained in:
Maurice
2026-11-13 09:34:55 +01:00
parent 64b86c2403
commit 0eac3a9f49
6 changed files with 304 additions and 161 deletions
+1 -1
View File
@@ -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,
@@ -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(<PlaceInspector {...defaultProps} onClose={onClose} collapsible />);
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(<PlaceInspector {...defaultProps} collapsible />);
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(<PlaceInspector {...defaultProps} place={louvre} collapsible />);
expect(screen.getByText('Rue de Rivoli, Paris')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Collapse' })).toHaveAttribute('aria-expanded', 'true');
rerender(<PlaceInspector {...defaultProps} place={louvre} />);
expect(screen.queryByRole('button', { name: 'Collapse' })).toBeNull();
});
});
+14 -158
View File
@@ -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<Place>) => void
/** Cast/clear the current user's star vote (#1435); enables the rating row. */
onRate?: (placeId: number, rating: number | null) => Promise<void> | 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<number | null>(null)
const [editingName, setEditingName] = useState(false)
const [nameValue, setNameValue] = useState('')
const nameInputRef = useRef<HTMLInputElement>(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 (
<div ref={cardRef}
@@ -384,11 +383,12 @@ export default function PlaceInspector({
time={place.place_time ? `${formatTime(place.place_time, locale, timeFormat)}${place.end_time ? ` – ${formatTime(place.end_time, locale, timeFormat)}` : ''}` : null}
editingName={editingName} nameInputRef={nameInputRef} nameValue={nameValue} setNameValue={setNameValue}
commitNameEdit={commitNameEdit} handleNameKeyDown={handleNameKeyDown} startNameEdit={startNameEdit}
onUpdatePlace={onUpdatePlace}
photoCredit={<PhotoCredit imageUrl={place.image_url} />} 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). */}
<div data-testid="inspector-scroll" className="flex min-h-0 flex-1 flex-col gap-3.5 overflow-y-auto overscroll-contain px-4 py-3.5 empty:hidden">
<div data-testid="inspector-scroll" className={`${collapsed ? 'hidden' : 'flex'} min-h-0 flex-1 flex-col gap-3.5 overflow-y-auto overscroll-contain px-4 py-3.5 empty:hidden`}>
{/* 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({
</div>
{/* Footer: the day action and the ways out on the left, delete and edit on the right. */}
<footer className="flex flex-none flex-wrap items-center gap-2 border-t border-edge-faint px-4 py-2.5">
<footer className={`${collapsed ? 'hidden' : 'flex'} flex-none flex-wrap items-center gap-2 border-t border-edge-faint px-4 py-2.5`}>
{/* Collection mode — copy to trip + per-place status */}
{mode === 'collection' && onCopyToTrip && (
<DialogButton variant="primary" onClick={onCopyToTrip} icon={<Copy size={14} strokeWidth={2} />} aria-label={t('collections.copyToTrip')}>
@@ -569,150 +569,6 @@ function MarkdownBox({ children }: { children: string }) {
)
}
interface InspectorHeadProps {
place: Place
category: Category | undefined
openNow: boolean | null
phone: string | null | undefined
/** Typed in by hand (#2472); there is no looked-up one. */
email?: string | null
rating: number | null
ratingCount: number | null
price: string | null
time: string | null
editingName: boolean
nameInputRef: React.RefObject<HTMLInputElement | null>
nameValue: string
setNameValue: (value: string) => void
commitNameEdit: () => void
handleNameKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => void
startNameEdit: () => void
onUpdatePlace?: (placeId: number, data: Partial<Place>) => void
onClose: () => void
}
/**
* The head band: the avatar with its open/closed ring and photo credit, the
* name (a double-click renames it), the address, and the facts as pills. The
* avatar opens the place's photos; its picture is changed in the editor.
*/
function InspectorHead({ place, category, openNow, phone, email, rating, ratingCount, price, time, editingName, nameInputRef,
nameValue, setNameValue, commitNameEdit, handleNameKeyDown, startNameEdit, onUpdatePlace, onClose }: InspectorHeadProps) {
const { t, locale, language } = useTranslation()
const ring = openNow === true ? 'bg-success' : openNow === false ? 'bg-danger' : 'bg-surface-card shadow-sm'
const hasCoords = !!(place.lat && place.lng)
const hasPills = openNow !== null || !!category || !!time || rating != null || !!price || place.source === 'dawarich' || !!phone || !!email || hasCoords
return (
<header className="flex-none border-b border-edge-faint px-4 pb-3 pt-3.5" style={{ background: category?.color ? tintOf(category.color) : NEUTRAL_TINT }}>
<div className="flex items-start gap-3.5">
<div className="flex w-[57px] flex-none flex-col items-center">
<div className={`rounded-full p-[2.5px] ${ring}`}>
<PlacePhotoGallery place={place} language={language} label={t('places.openPhotos')} className="rounded-full">
<PlaceAvatar place={place} category={category} size={52} />
</PlacePhotoGallery>
</div>
<PhotoCredit imageUrl={place.image_url} />
</div>
<div className="min-w-0 flex-1 pt-0.5">
{editingName ? (
<input
ref={nameInputRef}
value={nameValue}
onChange={e => setNameValue(e.target.value)}
onBlur={commitNameEdit}
onKeyDown={handleNameKeyDown}
aria-label={t('places.formName')}
className="-mx-1.5 block w-[calc(100%+12px)] rounded-[8px] border-0 bg-surface-card px-1.5 py-0.5 font-bold tracking-[-0.01em] text-content shadow-sm outline-none"
style={fs(17, 'subtitle')}
/>
) : (
<h2 onDoubleClick={startNameEdit}
className={`m-0 break-words font-bold leading-snug tracking-[-0.01em] text-content ${onUpdatePlace ? 'cursor-text' : ''}`}
style={fs(17, 'subtitle')}>
{place.name}
</h2>
)}
{place.address && (
// One line, whatever its length; the whole address is in the tooltip.
<div className="mt-0.5 flex min-w-0 items-center gap-1 text-content-muted" style={fs(12.5, 'body')}>
<MapPin size={12} strokeWidth={2} className="flex-none text-content-faint" />
<Tooltip label={place.address}>
<span className="min-w-0 truncate">{place.address}</span>
</Tooltip>
</div>
)}
{hasPills && (
<div className="mt-2 flex flex-wrap items-center gap-1.5">
{openNow !== null && (
<SoftPill tone={openNow ? 'success' : 'danger'} icon={<span className="h-1.5 w-1.5 flex-none rounded-full bg-current" />}>
{openNow ? t('inspector.opened') : t('inspector.closed')}
</SoftPill>
)}
{category && (
<span className={HEAD_PILL} style={fs(10.5)}>
<span className="h-2 w-2 flex-none rounded-full" style={{ background: category.color || 'var(--text-faint)' }} />
{category.name}
</span>
)}
{time && <TimePill>{time}</TimePill>}
{rating != null && rating > 0 && (
<span className={HEAD_PILL} style={fs(10.5)}>
<Star size={11} strokeWidth={2} fill="currentColor" className="flex-none text-warning" />
{rating.toFixed(1)}
{ratingCount ? <span className="font-medium text-content-faint">({ratingCount.toLocaleString(locale)})</span> : null}
</span>
)}
{price && (
<span className={HEAD_PILL} style={fs(10.5)}>
<Banknote size={11} strokeWidth={2} className="flex-none text-success" />
{price}
</span>
)}
{/* Where the place came from, when it did not come from somebody typing it: a stay
accepted out of their own recordings. The mark alone — the name beside it is
already the place's name, and a word here would only repeat the tooltip. */}
{place.source === 'dawarich' && (
<Tooltip label={t('dawarich.place.fromDawarich')} placement="top">
<span role="img" aria-label={t('dawarich.place.fromDawarich')} className={`${HEAD_PILL} px-1 py-[3px]`}>
<span className="inline-flex overflow-hidden rounded-[4px]"><DawarichIcon size={13} /></span>
</span>
</Tooltip>
)}
{phone && (
<a href={`tel:${phone}`} className={`${HEAD_PILL} hover:text-content-secondary`} style={fs(10.5)}>
<Phone size={11} strokeWidth={2} className="flex-none text-content-faint" />
{phone}
</a>
)}
{email && (
<a href={`mailto:${email}`} className={`${HEAD_PILL} min-w-0 hover:text-content-secondary`} style={fs(10.5)}>
<Mail size={11} strokeWidth={2} className="flex-none text-content-faint" />
<span className="truncate">{email}</span>
</a>
)}
{hasCoords && (
<span className="hidden flex-none items-center rounded-full border border-edge-faint px-2 py-[1px] font-geist tabular-nums text-content-faint sm:inline-flex" style={fs(10.5)}>
{Number(place.lat).toFixed(6)}, {Number(place.lng).toFixed(6)}
</span>
)}
</div>
)}
</div>
<AssistantAskButton place={place} />
<Tooltip label={t('common.close')}>
<button type="button" onClick={onClose} aria-label={t('common.close')}
className="grid h-8 w-8 flex-none place-items-center rounded-full bg-surface-card text-content-muted shadow-sm transition-colors hover:text-content">
<X size={15} strokeWidth={2.2} />
</button>
</Tooltip>
</div>
</header>
)
}
interface PlaceBookingsAndPeopleProps {
selectedAssignmentId: number | null
placeId: number
@@ -0,0 +1,247 @@
import { Banknote, ChevronDown, ChevronUp, Mail, MapPin, Phone, Star, X } from 'lucide-react';
import type { KeyboardEvent, ReactNode, RefObject } from 'react';
import { useTranslation } from '../../i18n';
import type { Category, Place } from '../../types';
import AssistantAskButton from '../Assistant/AssistantAskButton';
import DawarichIcon from '../shared/DawarichIcon';
import { NEUTRAL_TINT, fs } from '../shared/DialogShell';
import PlaceAvatar from '../shared/PlaceAvatar';
import { Tooltip } from '../shared/Tooltip';
import PlacePhotoGallery from './PlacePhotoGallery';
import { SoftPill, TimePill, tintOf } from './planParts';
/** The round buttons at the end of the band. */
const ROUND_BUTTON =
'grid h-8 w-8 flex-none place-items-center rounded-full bg-surface-card text-content-muted shadow-sm transition-colors hover:text-content';
/** 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';
interface InspectorHeadProps {
place: Place;
category: Category | undefined;
openNow: boolean | null;
phone: string | null | undefined;
/** Typed in by hand (#2472); there is no looked-up one. */
email?: string | null;
rating: number | null;
ratingCount: number | null;
price: string | null;
time: string | null;
/** Who made the avatar's picture, under it; the inspector looks it up. */
photoCredit: ReactNode;
/** Folded to the name alone, the place still selected on the map (#3039). */
collapsed: boolean;
/** Folds and unfolds the card; without it the card does not fold. */
onToggleCollapsed?: () => void;
editingName: boolean;
nameInputRef: RefObject<HTMLInputElement | null>;
nameValue: string;
setNameValue: (value: string) => void;
commitNameEdit: () => void;
handleNameKeyDown: (e: KeyboardEvent<HTMLInputElement>) => void;
startNameEdit: () => void;
onUpdatePlace?: (placeId: number, data: Partial<Place>) => void;
onClose: () => void;
}
/**
* The head band of the place inspector: the avatar with its open/closed ring and photo
* credit, the name (a double-click renames it), the address, and the facts as pills.
* The avatar opens the place's photos; its picture is changed in the editor.
*
* Folded, it is a slim bar of the avatar and the name over the map, so the place can stay
* selected, its marker highlighted, while the map around it is panned (#3039).
*/
export function InspectorHead({
place,
category,
openNow,
phone,
email,
rating,
ratingCount,
price,
time,
photoCredit,
collapsed,
onToggleCollapsed,
editingName,
nameInputRef,
nameValue,
setNameValue,
commitNameEdit,
handleNameKeyDown,
startNameEdit,
onUpdatePlace,
onClose,
}: InspectorHeadProps) {
const { t, locale, language } = useTranslation();
const ring = openNow === true ? 'bg-success' : openNow === false ? 'bg-danger' : 'bg-surface-card shadow-sm';
const hasCoords = !!(place.lat && place.lng);
const hasPills =
openNow !== null ||
!!category ||
!!time ||
rating != null ||
!!price ||
place.source === 'dawarich' ||
!!phone ||
!!email ||
hasCoords;
return (
<header
className={`flex-none px-4 ${collapsed ? 'py-2.5' : 'border-b border-edge-faint pb-3 pt-3.5'}`}
style={{ background: category?.color ? tintOf(category.color) : NEUTRAL_TINT }}
>
<div className={`flex gap-3.5 ${collapsed ? 'items-center' : 'items-start'}`}>
<div className={`flex flex-none flex-col items-center ${collapsed ? '' : 'w-[57px]'}`}>
<div className={`rounded-full p-[2.5px] ${ring}`}>
<PlacePhotoGallery
place={place}
language={language}
label={t('places.openPhotos')}
className="rounded-full"
>
<PlaceAvatar place={place} category={category} size={collapsed ? 32 : 52} />
</PlacePhotoGallery>
</div>
{!collapsed && photoCredit}
</div>
<div className="min-w-0 flex-1 pt-0.5">
{editingName ? (
<input
ref={nameInputRef}
value={nameValue}
onChange={(e) => setNameValue(e.target.value)}
onBlur={commitNameEdit}
onKeyDown={handleNameKeyDown}
aria-label={t('places.formName')}
className="-mx-1.5 block w-[calc(100%+12px)] rounded-[8px] border-0 bg-surface-card px-1.5 py-0.5 font-bold tracking-[-0.01em] text-content shadow-sm outline-none"
style={fs(17, 'subtitle')}
/>
) : (
<h2
onDoubleClick={startNameEdit}
className={`m-0 font-bold leading-snug tracking-[-0.01em] text-content ${collapsed ? 'truncate' : 'break-words'} ${onUpdatePlace ? 'cursor-text' : ''}`}
style={fs(17, 'subtitle')}
>
{place.name}
</h2>
)}
{!collapsed && place.address && (
// One line, whatever its length; the whole address is in the tooltip.
<div className="mt-0.5 flex min-w-0 items-center gap-1 text-content-muted" style={fs(12.5, 'body')}>
<MapPin size={12} strokeWidth={2} className="flex-none text-content-faint" />
<Tooltip label={place.address}>
<span className="min-w-0 truncate">{place.address}</span>
</Tooltip>
</div>
)}
{!collapsed && hasPills && (
<div className="mt-2 flex flex-wrap items-center gap-1.5">
{openNow !== null && (
<SoftPill
tone={openNow ? 'success' : 'danger'}
icon={<span className="h-1.5 w-1.5 flex-none rounded-full bg-current" />}
>
{openNow ? t('inspector.opened') : t('inspector.closed')}
</SoftPill>
)}
{category && (
<span className={HEAD_PILL} style={fs(10.5)}>
<span
className="h-2 w-2 flex-none rounded-full"
style={{ background: category.color || 'var(--text-faint)' }}
/>
{category.name}
</span>
)}
{time && <TimePill>{time}</TimePill>}
{rating != null && rating > 0 && (
<span className={HEAD_PILL} style={fs(10.5)}>
<Star size={11} strokeWidth={2} fill="currentColor" className="flex-none text-warning" />
{rating.toFixed(1)}
{ratingCount ? (
<span className="font-medium text-content-faint">({ratingCount.toLocaleString(locale)})</span>
) : null}
</span>
)}
{price && (
<span className={HEAD_PILL} style={fs(10.5)}>
<Banknote size={11} strokeWidth={2} className="flex-none text-success" />
{price}
</span>
)}
{/* Where the place came from, when it did not come from somebody typing it: a stay
accepted out of their own recordings. The mark alone — the name beside it is
already the place's name, and a word here would only repeat the tooltip. */}
{place.source === 'dawarich' && (
<Tooltip label={t('dawarich.place.fromDawarich')} placement="top">
<span
role="img"
aria-label={t('dawarich.place.fromDawarich')}
className={`${HEAD_PILL} px-1 py-[3px]`}
>
<span className="inline-flex overflow-hidden rounded-[4px]">
<DawarichIcon size={13} />
</span>
</span>
</Tooltip>
)}
{phone && (
<a href={`tel:${phone}`} className={`${HEAD_PILL} hover:text-content-secondary`} style={fs(10.5)}>
<Phone size={11} strokeWidth={2} className="flex-none text-content-faint" />
{phone}
</a>
)}
{email && (
<a
href={`mailto:${email}`}
className={`${HEAD_PILL} min-w-0 hover:text-content-secondary`}
style={fs(10.5)}
>
<Mail size={11} strokeWidth={2} className="flex-none text-content-faint" />
<span className="truncate">{email}</span>
</a>
)}
{hasCoords && (
<span
className="hidden flex-none items-center rounded-full border border-edge-faint px-2 py-[1px] font-geist tabular-nums text-content-faint sm:inline-flex"
style={fs(10.5)}
>
{Number(place.lat).toFixed(6)}, {Number(place.lng).toFixed(6)}
</span>
)}
</div>
)}
</div>
{!collapsed && <AssistantAskButton place={place} />}
{/* Folding is not closing: the place stays selected, and only the close deselects it. */}
{onToggleCollapsed && (
<Tooltip label={collapsed ? t('common.expand') : t('common.collapse')}>
<button
type="button"
onClick={onToggleCollapsed}
aria-expanded={!collapsed}
aria-label={collapsed ? t('common.expand') : t('common.collapse')}
className={ROUND_BUTTON}
>
{collapsed ? <ChevronUp size={15} strokeWidth={2.2} /> : <ChevronDown size={15} strokeWidth={2.2} />}
</button>
</Tooltip>
)}
<Tooltip label={t('common.close')}>
<button type="button" onClick={onClose} aria-label={t('common.close')} className={ROUND_BUTTON}>
<X size={15} strokeWidth={2.2} />
</button>
</Tooltip>
</div>
</header>
);
}
+1 -1
View File
@@ -547,7 +547,7 @@ function TripPlannerPageDesktop(): React.ReactElement | null {
onDelete={() => handleDeletePlace(selectedPlace.id)}
leftWidth={isMobile ? 0 : leftPanelPx}
rightWidth={isMobile ? 0 : rightPanelPx}
clearMapPill cardRef={pageState.inspectorCard.measure}
clearMapPill collapsible cardRef={pageState.inspectorCard.measure}
/>
)}
+1 -1
View File
@@ -275,7 +275,7 @@ Click a place in the places column, in a day, or on the map, and its inspector o
![The place inspector over the map: the photo, name, address, category and coordinates in its head, then the rating, the notes, the stay booked there under Bookings and the files, with Add to Day, Navigation, Save to Collection, Delete and Edit at its foot](assets/PlaceInspector.png)
- **The head band** shows the photo (a click opens it in a gallery, see [The photo gallery](#the-photo-gallery)), with a green ring while the place is open and a red one while it is closed, the name (double-click it to rename the place), the address on one line with the full address in a tooltip, and pills: **Open** or **Closed**, the category, the time of the visit, the Google rating, the phone number to call, the e-mail address, and the coordinates. **X** closes the inspector.
- **The head band** shows the photo (a click opens it in a gallery, see [The photo gallery](#the-photo-gallery)), with a green ring while the place is open and a red one while it is closed, the name (double-click it to rename the place), the address on one line with the full address in a tooltip, and pills: **Open** or **Closed**, the category, the time of the visit, the Google rating, the phone number to call, the e-mail address, and the coordinates. The chevron (**Collapse**) folds the inspector to a slim bar with the photo and the name, so the place stays selected and its marker highlighted while you pan the map; **Expand** unfolds it again, and the next place you pick opens in full. **X** closes the inspector and deselects the place.
- **Rating**: every member's vote, see [Rating a place](#rating-a-place).
- **Description**, **Notes** and **Notes for this day**, rendered as Markdown.
- **Bookings** pinned to this stop, as small cards with the status, date, time and booking code. For a hotel, the stays booked there are listed too (see [Accommodations](Accommodations)). Click a card (*Open booking*) to open the booking's detail. Next to them, **Participants** says who joins this stop.