diff --git a/src/components/profile/ProfilePanel.module.css b/src/components/profile/ProfilePanel.module.css index aea5d08..3d2610c 100644 --- a/src/components/profile/ProfilePanel.module.css +++ b/src/components/profile/ProfilePanel.module.css @@ -38,14 +38,22 @@ } .closeBtn { + display: inline-flex; + align-items: center; + justify-content: center; + /* Large touch target (≈44px) for easy tapping; negative margins pull it back + into the titlebar padding so the bar itself stays compact. */ + min-width: 44px; + min-height: 44px; + margin: -8px -6px -8px 0; background: none; border: none; color: #94a3b8; - font-size: 13px; + font-size: 16px; line-height: 1; - padding: 2px 5px; - border-radius: 3px; + border-radius: 6px; cursor: pointer; + touch-action: manipulation; } .closeBtn:hover { diff --git a/src/components/profile/ProfilePanel.tsx b/src/components/profile/ProfilePanel.tsx index 0d85d1b..466c723 100644 --- a/src/components/profile/ProfilePanel.tsx +++ b/src/components/profile/ProfilePanel.tsx @@ -45,6 +45,7 @@ const DEFAULT_RECT: PanelRect = { export function ProfilePanel({ mapRef }: Props) { const procedure = useProfileProcedure() const clearSelection = useSelectionStore((s) => s.clear) + const selectAircraft = useSelectionStore((s) => s.select) const selectedHex = useSelectionStore((s) => selectedHexOf(s.selected)) const procedures = useProcedureStore((s) => s.procedures) @@ -111,8 +112,15 @@ export function ProfilePanel({ mapRef }: Props) { const containerW = containerRect.width const containerH = containerRect.height - const width = Math.min(720, Math.max(PROFILE_PANEL_MIN_W, containerW - 2 * PROFILE_MARGIN_PX)) - const height = Math.min(PROFILE_PANEL_MIN_H, Math.max(160, containerH - 2 * PROFILE_MARGIN_PX)) + // Fit within the viewport width. On phones (portrait ≈ 360 CSS px) the + // 520px preferred width overflows the screen edge, so cap to what's + // available rather than forcing the desktop minimum. + const available = Math.max(160, containerW - 2 * PROFILE_MARGIN_PX) + const width = Math.min(720, available) + // A narrow (phone) layout wraps the header boxes onto extra rows; give it + // more vertical room so the wrapped header doesn't squeeze the plot. + const maxHeight = width < PROFILE_PANEL_MIN_W ? 400 : PROFILE_PANEL_MIN_H + const height = Math.min(maxHeight, Math.max(160, containerH - 2 * PROFILE_MARGIN_PX)) const candidates: Rect[] = [ { x: PROFILE_MARGIN_PX, y: PROFILE_MARGIN_PX, w: width, h: height }, @@ -263,7 +271,15 @@ export function ProfilePanel({ mapRef }: Props) { onPointerCancel={onTitlebarPointerUp} > Vertical Profile - @@ -277,6 +293,7 @@ export function ProfilePanel({ mapRef }: Props) { model={model} liveAircraft={liveAircraft} selectedTrail={selectedTrail} + onSelectAircraft={(hex) => selectAircraft({ kind: 'aircraft', hex })} width={Math.max(svgSize.width, 1)} height={Math.max(svgSize.height, 1)} /> diff --git a/src/components/profile/ProfileSvg.module.css b/src/components/profile/ProfileSvg.module.css index e91cfca..de684c7 100644 --- a/src/components/profile/ProfileSvg.module.css +++ b/src/components/profile/ProfileSvg.module.css @@ -14,13 +14,18 @@ (altitude IS the y axis already) — visible as a deviation trace but never competing with the descent path or a live aircraft's own glyph. */ .selectedTrail { - stroke: #38bdf8; /* same sky blue as the selected aircraft glyph (.aircraft), dimmed */ - stroke-width: 1.5; - stroke-opacity: 0.4; + stroke: #7dd3fc; /* brightened sky blue — the flown trail must stay clearly visible */ + stroke-width: 2; + stroke-opacity: 0.9; stroke-linecap: round; fill: none; } +/* Aircraft glyph with a tap/click hit target (profile selection). */ +.aircraftClickable { + cursor: pointer; +} + /* ── ground / terrain reference ─────────────────────────────────── */ .groundLine { stroke: #64748b; diff --git a/src/components/profile/ProfileSvg.tsx b/src/components/profile/ProfileSvg.tsx index b23d463..e4e7e17 100644 --- a/src/components/profile/ProfileSvg.tsx +++ b/src/components/profile/ProfileSvg.tsx @@ -19,6 +19,9 @@ interface Props { * time/distance gaps — see src/geo/profileTrail.ts. Empty/omitted when no * aircraft is selected. Drawn as the lowest layer in the plot. */ selectedTrail?: ProfileTrailPoint[][] + /** Select an aircraft (by hex) when its profile glyph is tapped/clicked — + * updates the map selection and swaps in that plane's flown trail. */ + onSelectAircraft?: (hex: string) => void width: number height: number } @@ -186,12 +189,14 @@ function AircraftGlyph({ label, isSelected, labelAbove, + onSelect, }: { x: number y: number label: string isSelected: boolean labelAbove: boolean + onSelect?: () => void }) { const labelY = y + (labelAbove ? AIRCRAFT_LABEL_ABOVE_DY : AIRCRAFT_LABEL_BELOW_DY) // Feather points RIGHT with its tip at (x, y). The tip — not the body center — @@ -201,7 +206,18 @@ function AircraftGlyph({ `${x - AIRCRAFT_BODY_LEN},${y + AIRCRAFT_BODY_HALF_H} ${x},${y} ` + `${x - AIRCRAFT_BODY_LEN},${y - AIRCRAFT_BODY_HALF_H} ${x - AIRCRAFT_NOTCH_LEN},${y}` return ( - + + {/* Transparent hit target — a generous touch/click area centered on the + glyph body so the small feather is easy to tap on a touchscreen. */} + {onSelect && ( + + )} @@ -879,6 +895,7 @@ export const ProfileSvg = memo(function ProfileSvg({ model, liveAircraft = [], s label={ac.label} isSelected={ac.isSelected} labelAbove={labelSides[i] === 'above'} + onSelect={onSelectAircraft ? () => onSelectAircraft(ac.hex) : undefined} /> )) })()}