#70 Redesign shipments tab and expanded detail view feature/shipment-view-redesign · 79b9a205
Overview
Files changed
7
7 modified · 0 added · 0 deleted
Lines added
+363
−95 removed
Net change
+268
1 commit on top of 5b066335
Motion utilities
5
all behind prefers-reduced-motion

What this PR does: Reworks the presentation of the shipments platform that landed in #64 — the list page at /shipments and the detail panel that expands inside a row. It adds a four-tile KPI strip to the page header, converts the route section of the expanded row from a wrap of flat chips into a vertical timeline rail with a marked current leg, gives table rows and mobile cards a status-derived left accent, and introduces five motion utilities in index.css that the two skeletons and the entrance transitions consume.

Presentation only. All seven changed files live under web/src. No server file, no worker route, no query, no migration, and no package.json entry is touched. Nothing is added or deleted — every file in the diff is a modification, and no data contract, hook signature, or component prop changes shape.

Data stays where it was. The list still reads /api/shipments through useShipments(); the expanded row still reads /api/shipments/:id/context through useShipmentContext(), still gated on the row actually being expanded. The KPI strip is derived client-side from the list already in memory — buildKpiTiles() is four filter().length passes over the same array — so the redesign adds no request.

Motion is opt-out by construction. The five new classes are all prefixed shipment- and are switched off together by one @media (prefers-reduced-motion: reduce) block at the bottom of the new CSS. No existing keyframe or rule in index.css is modified — the 95 lines are appended after the last existing block.

tsc — client project clean
vite build — passes
web/src only — 0 server files
0 new dependencies
main baseline red — 16 tsc errors · 11 failing suites, pre-existing
How This Branch Got Here
Originally
The PR was written against the legacy /shipment/:po route

The first version of this branch redesigned a dedicated per-shipment page — web/src/pages/shipment.tsx plus a web/src/components/shipment/ folder holding the map, cost table, decision countdown, nationwide banner, action buttons, and per-port status card. That work introduced new widgets and shared helpers on top of those files.

Then, on main
PR #64 deleted every surface the branch was built on

f26c517d — feat(shipments): build owner-scoped shipments platform (#64) replaced the single-shipment route with an owner-scoped platform: a D1-backed store, an import pipeline, an enrichment job, and a list page. In doing so it removed pages/shipment.tsx and all six files under components/shipment/ — 998 deleted lines.

Every file the original branch modified ceased to exist, and the history on that line was rewritten. Rebasing was not an option: there was no longer a common file to merge into.

This diff
Branch recreated from current main, redesign ported to the new platform

The branch was re-cut from 5b066335 and the design intent was re-applied to the surfaces that exist now: pages/shipments.tsx (the list) and components/shipments/ (the expandable detail row). One commit, 79b9a205, sits on top of main.

The shape of the work changed with the target. There is no longer a dedicated route to lay out, so the KPI strip moved from a page masthead into the list header, and the port-call timeline moved from a standalone widget into the expanded row. Nothing from the old components/shipment/ folder was resurrected — the diff adds no files at all.

Change Map
/api/shipmentslist endpoint
useShipments()use-shipments.ts
/api/shipments/:id/contextcontext endpoint
useShipmentContext()enabled on expand
ShipmentsPagepages/shipments.tsx · +118 −41
List surfaces
ShipmentKpiStripbuildKpiTiles() · 4 tiles
ShipmentListSkeletonmirrors loaded layout
ShipmentTabledesktop · +11 −1
ShipmentCardListmobile · +11 −2
ShipmentDetailPanelexpanded row · +29 −7
Detail sections
ShipmentPortCallsroute-timeline.tsx
ShipmentEventTimelineroute-timeline.tsx
ShipmentTrackingSectionField primitive · +5 −3
Motion utilitiesweb/src/index.css · +95 −0
.shipment-shimmer1.6s sweep
.shipment-fade-up0.35s rise
.shipment-kpi-tile0/50/100/150ms
.shipment-ladder-barscaleX grow
.shipment-stage-active2s pulse
Renders
Data flow (unchanged)
Consumes motion class
Hover a node to trace its connections
File-by-File Changes
List page 1 file
M
web/src/pages/shipments.tsx
Why: the page header carried a title, a bare count, and two buttons — nothing that answered "what is the state of my fleet right now". The new ShipmentKpiStrip widget, built here alongside buildKpiTiles(), answers it in four tiles: tracked total, active, delivered, and needs‑attention (errors plus untracked or stopped). The header becomes a space-y-3 stack so the strip sits under the title row rather than competing with it.
The standalone "N tracked" count was dropped, because the Tracked tile now states it; the count line survives only in its filtered form, "N of M", which the strip does not cover. The strip renders only when the list has loaded with a non-zero count, so it never appears empty.
ShipmentListSkeleton was five identical h-16 blocks; it now mirrors what it is standing in for — a four-tile KPI grid with label, value and sub bars, a search-field block, then five row bars — each carrying shipment-shimmer with staggered animationDelay. The loaded list container gains shipment-fade-up.
+118 −41
Expanded detail row 3 files
M
web/src/components/shipments/shipment-route-timeline.tsx
Why: the biggest single-file change, and the one with a real reading problem behind it. Port calls rendered as a flex-wrap row of bordered chips, so voyage order was implied by reading direction and wrapped unpredictably at narrow widths — and only the current call was distinguishable, by fill colour.
ShipmentPortCalls becomes a vertical rail: a marker column per stop with a dot and a connecting line, and a content column beside it. The dot now encodes three states rather than two — current (teal, ring-2, pulsing via shipment-stage-active), past (solid muted, from the new isPast derivation), and future (hollow, card background). Each dot enters with shipment-ladder-bar on a 60ms per-index delay, so the route draws itself in voyage order. The current stop also gets a text "Current" chip, so the state is not carried by colour alone; aria-current="step" and the visually-hidden "(current port)" label are unchanged.
ShipmentEventTimeline drops its per-item left border for a dot bullet aligned to the first text line, and promotes actual‑vs‑expected from a plain word in a run of metadata to a tinted chip — teal for Actual, inset surface for Expected. Location detail is now muted and rendered only when event.location.name is present.
+94 −41
M
web/src/components/shipments/shipment-detail-panel.tsx
Why: the panel read as a flat wall of same-weight text. Metric terms are regrouped into a proper micro-label ramp — 11px, semibold, uppercase, 0.08em tracking — against the unchanged value size, so the grid scans as label/value pairs. The container moves to rounded-xl with p-4 and picks up shipment-fade-up, so an expanding row resolves rather than snaps.
The context skeleton was three identical h-20 blocks for tracking, news and forecasts. It is now three shaped groups that match those sections — a heading plus a four-column metric grid, a heading plus three chip blocks, a heading plus a wide block — each staggered, so the expanded row does not resize when the context request lands.
+29 −7
M
web/src/components/shipments/shipment-tracking-section.tsx
Why: the smallest change, applied at the one place that propagates. The shared Field primitive adopts the same 11px uppercase micro-label as the detail grid and adds font-medium to the value, so tracking metrics stop looking like a different component from the panel around them. It also picks up shipment-kpi-tile, so tracking fields stagger in on the same curve as the header strip.
+5 −3
List surfaces 2 files
M
web/src/components/shipments/shipment-table.tsx
Why: tracking status was only legible by reading the badge in its column. Rows gain a border-l-4 whose colour comes from a new TRACKING_ACCENT map, so status is scannable down the left gutter of the table. Errored rows read red, active teal, delivered indigo; stopped and untracked fall back to var(--border) and stay quiet. The row also gains transition-colors so the existing hover no longer snaps.
+11 −1
M
web/src/components/shipments/shipment-card-list.tsx
Why: the mobile equivalent of the row change, using the same TRACKING_ACCENT values so the two surfaces agree. Cards additionally take shadow-sm lifting to shadow-md on hover through transition-shadow, giving the tap target the depth cue a table row gets from its hover fill. The shadow is tinted with the theme's --shadow-color rather than a raw black.
+11 −2
Motion utilities 1 file
M
web/src/index.css
Why: the motion above needed a home that is not a component. 95 lines appended after the last existing block — nothing already in the file is edited. Five classes, each with one job: shipment-shimmer (a ::after gradient sweep over a skeleton, 1.6s, driving both rebuilt skeletons), shipment-fade-up (0.35s opacity plus a 6px rise), shipment-kpi-tile (the same curve at 0.3s with nth-child delays of 0/50/100/150ms), shipment-ladder-bar (scaleX grow from left center), and shipment-stage-active (a 2s opacity pulse).
The sweep is built from the existing --skeleton-line and --skeleton-line-subtle theme tokens rather than a hardcoded colour, so it tracks light and dark. The file closes with one @media (prefers-reduced-motion: reduce) block that names all five selectors and neutralises animation, transform and opacity together.
+95
Notable Behaviors

Two loading states in this diff had the same defect: they were generic blocks of the wrong shape, so the page visibly reflowed when data arrived.

ShipmentListSkeleton was five h-16 bars. It now renders the same three-part structure as the loaded page — a four-tile KPI grid, each tile with its own label, value and sub bars in the matching sizes; a h-9 max-w-sm search-field block; and five h-14 row bars.

ShipmentDetailPanel's context skeleton was three identical h-20 blocks for tracking, news and forecasts. It now renders a heading plus a four-column metric grid, a heading plus three h-14 w-28 chip blocks, and a heading plus a wide block — the shapes those three sections actually take.

Every block carries shipment-shimmer with an explicit animationDelay, so the placeholder resolves in reading order instead of flashing as one plane. aria-busy, the sr-only live regions, and both loading testids are untouched.

All five new classes share the shipment- prefix, and the CSS closes by naming all five in a single query:

@media (prefers-reduced-motion: reduce) { .shipment-shimmer::after, .shipment-fade-up, .shipment-kpi-tile, .shipment-ladder-bar, .shipment-stage-active { animation: none !important; transform: none !important; opacity: 1 !important; } }

Neutralising transform and opacity alongside animation matters here, because three of the five animate with both fill mode — cancelling the animation alone would strand elements at their from state, invisible or scaled to zero. Setting all three restores the resting layout.

The prefix also bounds the blast radius: no rule in this block can reach a component outside the shipments surface, and no existing keyframe in index.css was modified to make room for these.

Both list surfaces declare the accent as a fully-keyed record rather than a lookup with a default:

const TRACKING_ACCENT: Record<TrackingStatus, string> = { active: "#14b8a6", delivered: "#6366f1", stopped: "var(--border)", error: "#ef4444", untracked: "var(--border)", };

Because the key type is the TrackingStatus union imported from server/shipments/store-types.js, adding a sixth status upstream makes both files fail to compile rather than silently rendering an undefined border colour. The indexing is total, so no fallback branch is needed at the call site.

The two colour-carrying values are applied as an inline borderLeftColor because the accent is data-derived; the two quiet states resolve to var(--border), so stopped and untracked rows keep the default gutter and only the states worth noticing draw the eye.

The old header rendered "N tracked" whenever the list was non-empty, and "N of M" when a search narrowed it. The new Tracked tile states the total more prominently, so keeping both would have printed the same number twice in adjacent rows.

The condition tightened to totalCount > 0 && isFiltered: the count line now appears only in the filtered case, which is the one thing the strip cannot express — the strip always describes the whole tracked set, not the current search result.

The strip itself is gated on !error && !isPending && totalCount > 0 && shipments, so it is absent during load (where the skeleton draws its shape instead), absent on error, and absent for a brand-new account that would otherwise see four zeroes above an empty state.

Every data-testid the diff touches is carried through on the restyled element rather than moved or renamed: shipments-loading, shipments-list, shipment-row, shipment-card, shipment-context-loading, shipment-port-calls, and shipment-event-timeline. One testid is added — shipments-kpi-strip, on the new widget.

Accessibility semantics are likewise preserved through the rewrite that changed the most markup. The port-call list stays an ol, keeps aria-current="step" on the current stop, and keeps the visually-hidden "(current port)" text; the new "Current" chip is an addition, so the state now reads through shape, colour and text rather than colour alone. Both loading states keep aria-busy="true", their aria-live announcements, and aria-hidden on the decorative bars.

A full repository check does not come back clean on this branch, and it does not come back clean on 5b066335 either. The baseline carries 16 TypeScript errors and 11 failing unit suites, every one of them server-side.

The set is identical before and after this diff. That is verifiable from the change itself as much as from the run: all seven changed files are client-side under web/src, no server module, test file, or type shared with the server is touched, and the only cross-boundary reference added is a type-only import of ShipmentRecord and TrackingStatus.

The gates that this diff can move are green: the client project type-checks clean and the Vite build passes. The baseline failures are pre-existing and belong to separate work — they are recorded here so the red is not mistaken for this branch's, and not so it is waved through.

Verdict
Presentation-only, ready for review
Seven modified files, all client-side, no additions or deletions. No endpoint, hook signature, prop shape, testid, or ARIA contract changes. The client project type-checks clean and the Vite build passes. Every animation introduced is switched off by one prefers-reduced-motion block, and every new class is prefixed so it cannot reach another surface.
tsc clean (client) vite build passes 7 files · web/src only 0 new deps testids preserved reduced-motion honoured
Caveat carried forward: a full repository check is red on main at 5b066335 — 16 tsc errors and 11 failing unit suites, all server-side and identical before and after this diff. This branch neither causes nor fixes them, and no gate here should be read as the repository being green.