#70 Shipment View Redesign — cohesive dashboard, new widgets, motion pass feature/shipment-view-redesign · 5199f860
Overview
Files changed
19
9 new · 9 modified · 1 deleted
Lines added
+1631
−488 removed · net +1143
New modules
9
widgets + shared utilities
Unit tests
657
553 + 104 · all pass

What this PR does: Rebuilds the /shipment/:po detail page from a loosely stacked set of cards into a dense, scannable dashboard. The page now opens with a masthead containing a journey-progress ladder, a five-tile KPI strip (cost exposure, estimated delay, ports disrupted, next arrival, free time left), and a decision-countdown urgency band — all before the map. The map is promoted to a full-width hero. Below it, a 2:1 grid places port status + mitigation options on the left and voyage itinerary + cost exposure on the right, balancing column heights.

Signals page: The "Shipment Alerts" collapsible section gains an always-visible aggregate stats strip (tracked shipments, total exposure, ports disrupted) and redesigned alert cards with severity-colored left-accent bars, sentence-case disruption headlines, and a hover-lift micro-interaction.

Motion pass: A shimmer-sweep skeleton precisely mirrors the final layout (no content jump on load). Loaded content fades up via shipment-fade-up. The journey ladder bars grow left-to-right with staggered delays. KPI tiles stagger in. The urgent AlertTriangle icon pulses. Option cards and alert cards lift on hover. All animations are disabled under prefers-reduced-motion.

No server changes, no new npm dependencies. All data comes from the existing /api/shipment endpoint via useShipmentDetail. The data-testid attributes referenced by e2e tests are preserved.

npm run build — tsc clean · Vite 11.2s · exit 0
npm run test:unit — 553 pass · 0 fail
npm run test:unit:supp — 104 pass · 0 fail
No server changes
No new dependencies
Component Architecture & Data Flow
MongoDBliv.pe_inputs
getShipmentDetail()server/queries.ts · unchanged
/api/shipmentWorker endpoint
useShipmentDetail()use-risk-data.ts
ShipmentPage/shipment/:po
JourneyProgressstage ladder
ShipmentKpiStrip5 KPI tiles
DecisionCountdownurgency band
NationwideBannerall_ports_affected
ShipmentMapfull-width hero
Left col (2/3)
PortStatusPanelall-ports table
MitigationOptionsoption cards
Right col (1/3)
VoyageItineraryport-call timeline
CostTable+ ActionButtons footer
Shared modules
shipment-stages.tsSTAGE_ORDER · labels · variants
shipment-status.tsisPortDisrupted · chipStyle
shipment-money.tsformatUsd()
shipment-metric.tsxShipmentMetric tile
port-status-card.tsxreplaced
Renders component
Imports shared module
Data flow
Hover nodes to trace connections
File-by-File Changes
Page rebuilds 2 files
M
web/src/pages/shipment.tsx
Full layout rebuild. Masthead now contains JourneyProgress + ShipmentKpiStrip. Map promoted to full-width hero above the 2:1 grid. ShipmentSkeleton rebuilt to precisely mirror the final layout with staggered shimmer tiles and an animated SVG map placeholder. Loaded content wrapped in shipment-fade-up entrance class.
+230 −105
M
web/src/pages/alerts.tsx
Shipment Alerts section gains an always-visible aggregate stats strip (tracked shipments, total exposure, ports disrupted) computed via useMemo. Imports formatUsd from the new shared module.
+29
New widgets 6 files
A
web/src/components/shipment/journey-progress.tsx
Horizontal stage ladder (Booked → Departed → In Transit → Approaching → Berthed → Discharged). Each bar gets shipment-ladder-bar entrance class with 60ms per-segment delay. Active segment gets shipment-stage-active pulse. Unknown stages handled gracefully with a fallback message.
+63
A
web/src/components/shipment/shipment-kpi-strip.tsx
Five-tile KPI strip: cost exposure (danger tone), estimated delay (warning tone), ports disrupted ratio, next arrival date, free time remaining. Each tile gets shipment-kpi-tile CSS class for nth-child stagger. Derives next arrival from the first port call without an actual_arrival.
+134
A
web/src/components/shipment/port-status-panel.tsx
Replaces the old per-port PortStatusCard. Renders all ports in a sticky-header desktop table and mobile article cards, sorted disrupted-first by severity. Active disruptions get full narrative articles with started/resolution/confidence/hard-blocked/nationwide chips. Hover lift on mobile cards and disruption articles.
+265
A
web/src/components/shipment/mitigation-options.tsx
Extracted from CostTable. Renders a responsive 2-column grid of option cards. Recommended card gets emerald highlight + border intensification on hover. Each card shows cost, time saved, confidence, cost-component chips, and source attribution. Returns null when no non-watch actions exist.
+98
A
web/src/components/shipment/voyage-itinerary.tsx
Ordered port-call timeline with a vertical rail. Each stop shows current-leg marker, transshipment chip, disruption status dot (severity-colored with glow ring), ETA/ETD or actual arrival/departure. Vessel telemetry footer (position, speed, heading, last fix) when position data is present.
+176
D
web/src/components/shipment/port-status-card.tsx
Deleted. Was a per-port card rendered in a loop; replaced entirely by PortStatusPanel which shows all ports in a single cohesive table + disruption detail section. No other consumers existed.
−91
Shared modules 4 files
A
web/src/components/shipment/shipment-stages.ts
Single source of truth for the shipment lifecycle: STAGE_ORDER, STAGE_LABELS, STAGE_VARIANTS, stageIndex(), stageLabel(), stageVariant(). Previously duplicated across shipment.tsx and the old card. Consumed by JourneyProgress, ShipmentPage badge, and ShipmentAlertCard.
+57
A
web/src/components/shipment/shipment-status.ts
Port status display helpers: portStatusLabel(), portStatusVariant(), isPortDisrupted(), portSeverityDisplay(), severityChipStyle(). Shared across PortStatusPanel, VoyageItinerary, ShipmentKpiStrip, and ShipmentAlertCard.
+50
A
web/src/components/shipment/shipment-money.ts
formatUsd(value) — null-safe dollar formatter (guards null/undefined/NaN, rounds before formatting). Shared across KPI strip, CostTable, MitigationOptions, ShipmentAlertCard, and alerts.tsx. Eliminates five inline $${Math.round(...).toLocaleString()} patterns.
+6
A
web/src/components/shipment/shipment-metric.tsx
ShipmentMetric — reusable dt/dd tile with micro-label, value, optional note, and danger tone. Shared by CostTable and MitigationOptions. Eliminates duplicated inline metric patterns.
+25
Restyled components 6 files
M
web/src/components/alerts/shipment-alert-card.tsx
Redesigned to match alert-card.tsx density. Left-accent border colored by severity. Sentence-case disruption headline. 2-line clamp on mobile. Separator dots hidden on narrow viewports. Hover lift (hover:-translate-y-px hover:shadow-md). Arrow shifts on hover via group-hover:translate-x-0.5.
+80 −42
M
web/src/components/shipment/cost-table.tsx
Restyled to ShipmentMetric dl grid. Surfaces reroute cost delta, airfreight uplift, wait-vs-act threshold, and tariff source. Mitigation options extracted to MitigationOptions. ActionButtons merged into card footer.
+90 −83
M
web/src/components/shipment/decision-countdown.tsx
Restyled with AlertTriangle/Clock icons. Urgency tones (red/amber/neutral). Redundant exposure/free-time stats removed (now in KPI strip). shipment-urgent-icon pulse class on the triangle when isUrgent.
+80 −76
M
web/src/components/shipment/nationwide-banner.tsx
Bug fix: now filters to only ports with all_ports_affected: true before extracting country codes. Previously showed "All X ports affected" for every disrupted port. Added CA → Canada to country map. Added Ban icon. Returns null when no nationwide ports.
+55 −41
M
web/src/components/shipment/shipment-map.tsx
Title and legend moved into CardHeader (consistent card chrome). No-coordinates fallback restyled with port chips. WebGL fallback given matching header. Map height tuned for full-width hero position. DeckGL layer logic unchanged.
+60 −41
M
web/src/components/shipment/action-buttons.tsx
Card wrapper removed. Uses buttonVariants. Full-width button on mobile. Modal restyled with design tokens, Check/Send/X lucide icons, and blurred overlay.
+65 −76
Motion & CSS 1 file
M
web/src/index.css
140 lines added. 9 new keyframe groups scoped to .shipment-* classes: shimmer sweep (shipment-shimmer), fade-up entrance (shipment-fade-up), KPI tile stagger (shipment-kpi-tile nth-child delays), ladder bar grow (shipment-ladder-bar), active stage pulse (shipment-stage-active), urgent icon pulse (shipment-urgent-icon), SVG map route trace (shipment-map-route), port dot blink (shipment-port-dot). Single @media (prefers-reduced-motion: reduce) block disables all of them. No existing keyframes modified.
+140
Notable Behaviors & Fixes

The original NationwideBanner extracted country codes from all disrupted ports and displayed "All X ports affected — a domestic reroute does not avoid this disruption." This was factually wrong: a port being impaired does not mean every port in that country is affected.

Fix: filter disruptedPorts to only entries where status.all_ports_affected === true before extracting country codes. The component now returns null when no ports carry that flag, so the banner only appears when the data actually warrants it. Also added CA → Canada to the country name map (the app tracks Montreal and Halifax routes).

The fix is in nationwide-banner.tsx: const nationwidePorts = disruptedPorts.filter(([, s]) => s.all_ports_affected).

The previous skeleton was three generic Skeleton blocks that bore no resemblance to the final layout. When data arrived, the page would reflow significantly.

The new ShipmentSkeleton renders: back-link skeleton, title + badge row, 6-segment journey ladder bars (matching STAGE_ORDER.length), 5-column KPI strip tiles, a countdown block, an animated SVG map placeholder (two shipment-map-route path traces + four shipment-port-dot circles), and the 2:1 grid with correctly-sized column blocks.

Every skeleton block carries a shipment-shimmer class (a 1.6s shimmer sweep via ::after pseudo-element) with staggered animationDelay values so tiles light up sequentially. The SVG map placeholder reuses the existing trace-draw and map-port-blink keyframes already in index.css.

All nine shipment-scoped animation classes are disabled by a single @media (prefers-reduced-motion: reduce) block at the end of the new CSS section in index.css:

.shipment-shimmer::after, .shipment-fade-up, .shipment-kpi-tile, .shipment-ladder-bar, .shipment-stage-active, .shipment-urgent-icon, .shipment-map-route, .shipment-map-route-b, .shipment-port-dot { animation: none !important; transform: none !important; opacity: 1 !important; }

The existing global animate-pulse on Skeleton is unaffected — Tailwind's built-in motion-safe variant already handles it. No other pages are affected because all new classes are prefixed shipment-.

The e2e smoke suite references data-testid="signals-shipment-alerts" on the collapsible Card in alerts.tsx. This attribute is preserved exactly as found.

New data-testid attributes added for the new widgets: shipment-detail (page wrapper), shipment-journey-progress, shipment-kpi-strip, shipment-decision-countdown, shipment-nationwide-banner, shipment-route-map, shipment-port-status, shipment-port-row, shipment-voyage-itinerary, shipment-cost-exposure, shipment-mitigation-options, shipment-option-card, shipment-alert-card. These are additive and do not conflict with existing selectors.

The original layout placed the map in the left column of a 2-col grid. When the right column (itinerary + cost) ran longer than the left (map + port cards), a large dead zone appeared below the map.

The fix: promote the map to full-width above the grid. Below it, a 2:1 grid places PortStatusPanel + MitigationOptions on the left (wide) and VoyageItinerary + CostTable on the right (narrow). This balances the two columns to within ~130px of each other in the typical data scenario.

On mobile, everything stacks: map → port status → mitigation options → voyage itinerary → cost exposure. The KPI strip collapses from 5 columns to 2 columns (sm: 3, lg: 5).

Verdict
Ready to merge
All three quality gates pass. No server changes, no new npm dependencies, no stubs or mocks left in source. The data-testid contract for e2e is preserved. The nationwide banner bug is fixed. The skeleton mirrors the final layout. All animations respect prefers-reduced-motion.
build ✓ test:unit 553 ✓ test:unit:supp 104 ✓ no server changes no new deps e2e testids preserved