Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions src/components/profile/ProfilePanel.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
23 changes: 20 additions & 3 deletions src/components/profile/ProfilePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 },
Expand Down Expand Up @@ -263,7 +271,15 @@ export function ProfilePanel({ mapRef }: Props) {
onPointerCancel={onTitlebarPointerUp}
>
<span className={styles.titlebarLabel}>Vertical Profile</span>
<button className={styles.closeBtn} onClick={() => clearSelection()} aria-label="Close profile panel">
<button
className={styles.closeBtn}
// Stop the titlebar's drag-start handler (which preventDefault()s the
// pointerdown, cancelling the button's click on touchscreens) from
// seeing this tap, so the native click fires normally.
onPointerDown={(e) => e.stopPropagation()}
onClick={() => clearSelection()}
aria-label="Close profile panel"
>
</button>
</div>
Expand All @@ -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)}
/>
Expand Down
11 changes: 8 additions & 3 deletions src/components/profile/ProfileSvg.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
21 changes: 19 additions & 2 deletions src/components/profile/ProfileSvg.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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 —
Expand All @@ -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 (
<g>
<g className={onSelect ? styles.aircraftClickable : undefined} onClick={onSelect}>
{/* 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 && (
<rect
x={x - AIRCRAFT_BODY_LEN - 8}
y={y - AIRCRAFT_BODY_HALF_H - 12}
width={AIRCRAFT_BODY_LEN + 20}
height={AIRCRAFT_BODY_HALF_H * 2 + 24}
fill="transparent"
/>
)}
<polygon className={isSelected ? styles.aircraft : styles.aircraftInactive} points={points} />
<text
className={isSelected ? styles.aircraftLabel : styles.aircraftLabelDimmed}
Expand Down Expand Up @@ -459,7 +475,7 @@ function HoldInLieuFigure({

// Memoized: the live-aircraft tick re-renders the parent every second, but
// the static profile geometry only depends on model/width/height.
export const ProfileSvg = memo(function ProfileSvg({ model, liveAircraft = [], selectedTrail = [], width, height }: Props) {
export const ProfileSvg = memo(function ProfileSvg({ model, liveAircraft = [], selectedTrail = [], onSelectAircraft, width, height }: Props) {
if (model.fixes.length < 2) {
return (
<svg className={styles.svg} width={width} height={height} viewBox={`0 0 ${width} ${height}`}>
Expand Down Expand Up @@ -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}
/>
))
})()}
Expand Down
Loading