diff --git a/client/src/components/Map/centredMapCard.test.ts b/client/src/components/Map/centredMapCard.test.ts new file mode 100644 index 000000000..fef18008a --- /dev/null +++ b/client/src/components/Map/centredMapCard.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from 'vitest'; +import { centredMapCardBox } from './centredMapCard'; + +// Resolves the CSS the helper returns for a given container width, so the +// geometry is checked in pixels rather than as strings. +function resolve(css: string, extent: number): number { + const expr = css + .replace(/100%/g, `${extent}px`) + .replace(/px/g, '') + .replace(/calc/g, '') + .replace(/min\(/g, 'Math.min(') + .replace(/max\(/g, 'Math.max('); + return Function(`return (${expr})`)() as number; +} + +describe('centredMapCardBox', () => { + it('keeps the old centred box when there is no pill to clear', () => { + const box = centredMapCardBox(320, 320); + expect(resolve(box.width, 1280)).toBe(608); + expect(resolve(box.left, 1280)).toBe(640); + }); + + it('a narrow desktop map moves the card off the base-layer pill (#2604)', () => { + const box = centredMapCardBox(350, 310, { clearPill: true }); + const width = resolve(box.width, 1280); + const leftEdge = resolve(box.left, 1280) - width / 2; + // The pill spans 20 to 62 px right of the left panel. + expect(leftEdge).toBeGreaterThanOrEqual(350 + 62); + // And the card still stops 16 px short of the right panel. + expect(leftEdge + width).toBeLessThanOrEqual(1280 - 310 - 16); + }); + + it('a wide map keeps the card exactly centred and 800 wide', () => { + const box = centredMapCardBox(320, 320, { clearPill: true }); + expect(resolve(box.width, 1920)).toBe(800); + expect(resolve(box.left, 1920)).toBe(960); + }); +}); diff --git a/client/src/components/Map/centredMapCard.ts b/client/src/components/Map/centredMapCard.ts new file mode 100644 index 000000000..c47f1e634 --- /dev/null +++ b/client/src/components/Map/centredMapCard.ts @@ -0,0 +1,34 @@ +import { MAP_LAYER_SWITCHER_INSET, MAP_ROUND_CONTROL_SIZE } from './MapLayerSwitcher'; + +/** The card's gap to the side panels where nothing else sits at the map's edge. */ +const EDGE_GAP = 16; +/** How far the card keeps from the left panel, so it ends short of the base-layer pill. */ +const PILL_CLEARANCE = MAP_LAYER_SWITCHER_INSET + MAP_ROUND_CONTROL_SIZE + 8; + +/** + * Where a card opened over the map sits (the place inspector, a tour's detail, + * the day panel): centred between the two side panels, at most 800 wide. + * + * On the desktop map the base-layer pill stays put in the bottom-left corner. On + * a map wide enough the centred card never reaches that corner, but on a narrower + * one (both sidebars open in a 1280 window) it ran under the pill, which then + * covered the first button of the card's footer (#2604). With `clearPill` the + * card's left edge never comes closer than the pill: a narrow card moves right of + * it and gives up the few pixels it needs, a wide map keeps it exactly centred. + * The phone shows these cards as sheets with no pill beside them, so it leaves + * `clearPill` off. + * + * `extent` is what the card is placed in: '100%' of the map, or '100vw' for the + * fixed day panel. + */ +export function centredMapCardBox( + leftWidth: number, + rightWidth: number, + { extent = '100%', clearPill = false }: { extent?: string; clearPill?: boolean } = {} +): { left: string; width: string } { + const free = `(${extent} - ${leftWidth}px - ${rightWidth}px)`; + const centre = `calc(${leftWidth}px + ${free} / 2)`; + if (!clearPill) return { left: centre, width: `min(800px, calc(${free} - ${2 * EDGE_GAP}px))` }; + const width = `min(800px, calc(${free} - ${PILL_CLEARANCE + EDGE_GAP}px))`; + return { width, left: `max(${centre}, calc(${leftWidth}px + ${PILL_CLEARANCE}px + ${width} / 2))` }; +} diff --git a/client/src/components/Planner/DayDetailPanel.tsx b/client/src/components/Planner/DayDetailPanel.tsx index 300f213b6..5135dfae8 100644 --- a/client/src/components/Planner/DayDetailPanel.tsx +++ b/client/src/components/Planner/DayDetailPanel.tsx @@ -26,6 +26,7 @@ import { BOX, Eyebrow, Field, RoundAction, TypeTile, toneOf, toneColor, toneTint import { SoftPill, TimePill, tintOf } from './planParts' import { typeInfo } from './bookings/bookingsModel' import { weatherIconFor } from '../Weather/weatherIcons' +import { centredMapCardBox } from '../Map/centredMapCard' function WIcon({ main, size = 14 }: { main: string; size?: number }) { const Icon = weatherIconFor(main) @@ -162,7 +163,7 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri const removeAccommodation = (acc: Accommodation) => { handleRemoveAccommodation(acc).catch(showError) } return ( -