Merge pull request 'v0.15.1 — ISS-004 right pane widened, On-board wrapping fixed' (#16) from fix/right-pane-width into master
CI / build + unit + e2e (push) Successful in 5m4s
CI / build + unit + e2e (push) Successful in 5m4s
This commit was merged in pull request #16.
This commit is contained in:
@@ -1,5 +1,14 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v0.15.1 — 2026-09-19 11:04
|
||||||
|
- **Right pane widened, "On board" wrapping fixed (ISS-004).** The 3-pane shell grid (`app/globals.css` `.app-shell`) moves from `300px / 1fr / 320px` to `280px / 1fr / 360px` — the map column absorbs the delta (MapLibre sizes itself from its container, no JS change). Three wrap offenders in the right pane's "On board" blocks are fixed:
|
||||||
|
- In-flight leg header (`components/RightPane.tsx`, `.contract-route` span): the class already had `white-space: nowrap; overflow: hidden; text-overflow: ellipsis`, but as a flex item with default `min-width: auto` it could not shrink below its full text width, so long routes (e.g. "Frankfurt → Madrid–Barajas") overflowed instead of ellipsizing — `.contract-route` now has `min-width: 0`
|
||||||
|
- Cargo-row buttons ("Fly to {dest}" / "Abandon −€X" in `.contract-main`): labels wrapped mid-text at the old width — `.pane-btn` now has `white-space: nowrap`
|
||||||
|
- Cargo sub-line route span had no white-space rule and wrapped mid-route — it now carries the existing `contract-route` class (nowrap + ellipsis)
|
||||||
|
- **Left pane sanity-checked at its new 280px**: fleet-row location span (`.plane-loc`) already had nowrap+ellipsis but lacked `min-width: 0`, so a long in-flight leg would have overflowed instead of truncating — same one-line guard added. Fuel/loadout spans are short fixed strings and need no guard
|
||||||
|
- Verified: `tsc --noEmit` clean; unit suite **238/238** green locally (no lib changes); e2e not run locally by design — CI verifies against an isolated stack
|
||||||
|
- **Unrelated test-stability fix included on this branch to unblock the pipeline** (`e2e/map-interactions.spec.ts`, no app changes): the mid-flight marker click now targets the plane's RENDERED position — it polls up to ~5 s for the rendered feature via `window.__flightMap.queryRenderedFeatures` on layers `planes-flying`/`planes-grounded` and projects that feature's coordinates, instead of fetching a fresh `/api/state`. With the sim paused the server position is static, but the map renders from its own ~1 s poll — right after the freeze confirmation the last rendered frame can still be up to one poll interval stale (tens of px at scale 1000 on EDDF→LEMD), so a click projected from a fresh fetch misses the small circle layer: the render-lag race that flaked CI run #37. Pause + freeze confirmation, popup assertions and resume are unchanged
|
||||||
|
|
||||||
## v0.15.0 — 2026-09-19 09:35
|
## v0.15.0 — 2026-09-19 09:35
|
||||||
- **Hover visualization from the side panels (ISS-002).** Hovering a fleet row in the left pane, an "On board" block (including its cargo rows) or an open-package row in the right pane now highlights the matching object on the map. The shared `HoverTarget` type (`components/hoverTypes.ts`: `{kind:'plane', id}` | `{kind:'package', originIcao, destIcao}`) is owned by the shell as a single `useState` and prop-drilled down — panes report via an `onHover` callback, `<Map>` consumes it. No React contexts introduced; the established prop-drilling pattern is kept
|
- **Hover visualization from the side panels (ISS-002).** Hovering a fleet row in the left pane, an "On board" block (including its cargo rows) or an open-package row in the right pane now highlights the matching object on the map. The shared `HoverTarget` type (`components/hoverTypes.ts`: `{kind:'plane', id}` | `{kind:'package', originIcao, destIcao}`) is owned by the shell as a single `useState` and prop-drilled down — panes report via an `onHover` callback, `<Map>` consumes it. No React contexts introduced; the established prop-drilling pattern is kept
|
||||||
- **Three top-most highlight layers** (`components/Map.tsx`, added inside idempotent `ensureLayers()` so they survive every `setStyle`): `planes-highlight` (circle over both plane layers, filter `['==',['get','id'],<planeId>]`, r=10, #38bdf8 fill + white stroke), `routes-highlight` (line over `routes-legs`, filter on the leg's `planeId`, width 6, #ef4444) and `airports-highlight` (circle ring over `airports-circles`, filter matching the hovered package's two ICAOs via `['any', …]`, r=8 stroke-only). All start filtered to match nothing — an empty-string sentinel no feature can have. Consumption follows the existing ref + effect pattern: a `[hovered]` effect calls `map.setFilter(...)` on the three layers (guarded by `getLayer`), and `ensureLayers` re-applies the current filter at its end so a hover outliving a style switch stays highlighted
|
- **Three top-most highlight layers** (`components/Map.tsx`, added inside idempotent `ensureLayers()` so they survive every `setStyle`): `planes-highlight` (circle over both plane layers, filter `['==',['get','id'],<planeId>]`, r=10, #38bdf8 fill + white stroke), `routes-highlight` (line over `routes-legs`, filter on the leg's `planeId`, width 6, #ef4444) and `airports-highlight` (circle ring over `airports-circles`, filter matching the hovered package's two ICAOs via `['any', …]`, r=8 stroke-only). All start filtered to match nothing — an empty-string sentinel no feature can have. Consumption follows the existing ref + effect pattern: a `[hovered]` effect calls `map.setFilter(...)` on the three layers (guarded by `getLayer`), and `ensureLayers` re-applies the current filter at its end so a hover outliving a style switch stays highlighted
|
||||||
|
|||||||
+4
-1
@@ -71,7 +71,7 @@ body {
|
|||||||
/* v0.9.0 — single-screen 3-pane shell: left Assets / center map / right Jobs & Market */
|
/* v0.9.0 — single-screen 3-pane shell: left Assets / center map / right Jobs & Market */
|
||||||
.app-shell {
|
.app-shell {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 300px minmax(0, 1fr) 320px;
|
grid-template-columns: 280px minmax(0, 1fr) 360px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,6 +201,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.plane-loc {
|
.plane-loc {
|
||||||
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -265,6 +266,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.contract-route {
|
.contract-route {
|
||||||
|
min-width: 0;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #f1f5f9;
|
color: #f1f5f9;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -284,6 +286,7 @@ body {
|
|||||||
|
|
||||||
.pane-btn {
|
.pane-btn {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
padding: 3px 10px;
|
padding: 3px 10px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|||||||
@@ -294,7 +294,7 @@ export default function RightPane({ state, loading, applySnapshot, onHover }: Ri
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="contract-sub">
|
<div className="contract-sub">
|
||||||
<span>{pkg.originName} → {pkg.destName}</span>
|
<span className="contract-route">{pkg.originName} → {pkg.destName}</span>
|
||||||
<span>{formatCash(pkg.reward)}</span>
|
<span>{formatCash(pkg.reward)}</span>
|
||||||
{pkg.status === 'expired' ? (
|
{pkg.status === 'expired' ? (
|
||||||
<span data-testid={`onboard-expired-${pkg.id}`}>expired</span>
|
<span data-testid={`onboard-expired-${pkg.id}`}>expired</span>
|
||||||
|
|||||||
@@ -266,20 +266,35 @@ test('plane markers move while flying and the plane popup shows status + route',
|
|||||||
}, { timeout: 15_000, message: 'waiting for the paused clock to freeze the plane position' })
|
}, { timeout: 15_000, message: 'waiting for the paused clock to freeze the plane position' })
|
||||||
.toBe(true);
|
.toBe(true);
|
||||||
|
|
||||||
// Fetch + project in one evaluate — with the sim paused above, the server's analytic
|
// Click the RENDERED marker position, not a fresh /api/state fetch. With the sim paused
|
||||||
// position is static, so the projected point matches what the map renders.
|
// above the server's analytic position is static — but the map renders from its own ~1 s
|
||||||
|
// poll, so right after the freeze confirmation the last rendered frame can still be up to
|
||||||
|
// one poll interval stale (tens of px at scale 1000 on this leg) and a click projected
|
||||||
|
// from a fresh fetch misses the small circle layer (CI run #37). Poll for the plane's
|
||||||
|
// rendered feature instead and project ITS coordinates — exactly what the map draws.
|
||||||
|
// Find + project in one evaluate so nothing can change between the two steps.
|
||||||
const pt = await page.evaluate(async () => {
|
const pt = await page.evaluate(async () => {
|
||||||
const m = (window as any).__flightMap;
|
const m = (window as any).__flightMap;
|
||||||
if (!m) return null;
|
if (!m) return null;
|
||||||
const res = await fetch('/api/state', { cache: 'no-store' });
|
const deadline = Date.now() + 5_000;
|
||||||
const body = (await res.json()) as StateBody;
|
for (;;) {
|
||||||
const p = body.planes.find((x) => x.id === 'plane-starter');
|
let feats: Array<{ properties?: Record<string, unknown>; geometry?: { coordinates: number[] } }> = [];
|
||||||
if (!p || p.status !== 'flying') return null;
|
try {
|
||||||
const projected = m.project([p.lon, p.lat]);
|
feats = m.queryRenderedFeatures(undefined, { layers: ['planes-flying', 'planes-grounded'] });
|
||||||
const rect = m.getContainer().getBoundingClientRect();
|
} catch {
|
||||||
return { x: rect.left + projected.x, y: rect.top + projected.y };
|
// map not ready yet — keep polling
|
||||||
|
}
|
||||||
|
const f = feats.find((x) => x.properties?.id === 'plane-starter');
|
||||||
|
if (f && f.geometry) {
|
||||||
|
const projected = m.project(f.geometry.coordinates);
|
||||||
|
const rect = m.getContainer().getBoundingClientRect();
|
||||||
|
return { x: rect.left + projected.x, y: rect.top + projected.y };
|
||||||
|
}
|
||||||
|
if (Date.now() >= deadline) return null;
|
||||||
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
expect(pt).not.toBeNull(); // the ~10 s window at scale 1000 is still open this early after departure
|
expect(pt, 'plane marker not rendered after pause').not.toBeNull();
|
||||||
|
|
||||||
await page.mouse.click(pt!.x, pt!.y);
|
await page.mouse.click(pt!.x, pt!.y);
|
||||||
const popup = page.getByTestId('plane-popup');
|
const popup = page.getByTestId('plane-popup');
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "flight-ops",
|
"name": "flight-ops",
|
||||||
"version": "0.15.0",
|
"version": "0.15.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev -p 3000",
|
"dev": "next dev -p 3000",
|
||||||
|
|||||||
Reference in New Issue
Block a user