Platform detail
Vertical card showing the deep state of a single platform — the master-detail companion to platform roster. Composes naturally to the right of a roster: operator scans the list, selects a row, this panel surfaces everything else.
Live
Six fixtures across UXV domains (UAV / UGV / UUV / USV), then a lost-link state and a strike platform demonstrating the indicator alphabet. Each section renders only when its props are passed.
Anatomy
Header + three labelled sections (Comms, Telemetry, Mission), plus an optional last- contact footer. Each row is a label + value pair in mono. The header carries the Ember platform marker; rows stay neutral except for semantic-coded link / battery values.
Ember-dotted platform identifier, optional class tag, optional current-autonomy summary on the right. Always present.
LINK label (semantic colour), signal bars next to the count, battery gauge next to the percent. Surfaces only if at least one is passed.
POSITION, HEADING (with a small rotating dial alongside the bearing), SPEED, vertical (ALT/ELEV/DEPTH from domain). Each row optional.
Mission step progress (e.g. WPT-04 · 4/12) and active operator. Together they answer 'what is this platform doing and who's responsible.'
Domain-specific rows surfaced between Mission and Last contact — payload, sensor health, fuel, munitions. Consecutive entries with the same section group under one header.
PipCount (discrete inventory), CapacityBar (continuous percentage), StateDot (active / standby / off), StateText (binary safety-critical state) compose into extras[].value so payload, endurance, and sensor rows read as instruments rather than flat text. See the Indicators section below.
Pass fillHeight to opt into internal scrolling. The header and last-contact footer stay anchored; the body (Comms / Telemetry / Mission / extras) scrolls within the frame. Use when composing inside a height-constrained container so a card with many extras doesn't push the parent layout. Without it, the card stays intrinsic-height.
Time since last heartbeat. Anchors invariant 5 — when contact ages out, the operator sees it before drawing conclusions from stale data.
Designed to sit next to platform roster — roster has the active-platform state, detail receives the same id and renders the deep state.
Indicators
Four pack-internal primitives extend the indicator alphabet beyond the built-in signal / battery / heading. Pass them through extras[].value so payload, endurance, sensor, and safety-critical rows read as instruments rather than flat text. Imported from @/components/rc3/indicators.
Discrete inventory — munition rounds, smoke cartridges, comms-relay drops. Pips stay countable; a smooth gauge would round away the operationally-meaningful unit.
<PipCount filled={3} total={4} suffix="AGM-114" />Continuous percentage — fuel reserve, comms-relay buffer, mission completion, tank level. Pair with text suffix carrying the value or remaining-time. Tone is explicit since 'low = bad' is not universal.
<CapacityBar pct={62} suffix="62%" />Three-state operational status — active / standby / off. Colour-coded dot paired with the state text. Use for sensor channels, sub-system health, secondary modes.
<StateDot state="active">Tracking</StateDot>Binary safety-critical state — ARMED / SAFE, WEAPONS HOT / COLD. Colour-coded mono text, no border or pill. Matches the LINK / LOST family already in Comms.
<StateText tone="danger">ARMED</StateText>Pick the indicator that matches the data's shape — pips for discrete countable quantities, capacity bar for continuous percentages, dots for ternary state, semantic text for binary safety-critical state. Don't reach for chrome decoration (corner brackets, bordered pills, glow) to signal “tactical”; the instrument reads tactical because the encoding is honest.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| platform | string | — | Platform identifier — e.g. "UAV-01". Ember-dotted header. |
| klass | string | — | Class tag — UGV / UAV / USV / UUV. |
| domain | "aerial" | "ground" | "surface" | "underwater" | — | Drives the vertical concept label (ALT / ELEV / DEPTH). |
| link | "good" | "degraded" | "lost" | — | Link state. Surfaces in the Comms section as a semantic-coloured LINK label. |
| signal | 0 | 1 | 2 | 3 | 4 | — | Signal bars count. |
| battery | number | — | Battery percent (0–100). Colour-coded — success > 50, warning 20–50, danger < 20. |
| autonomy | string | — | Current autonomy rung label — e.g. "DELEGATED". Surfaces in the header. |
| position | string | — | Consumer-formatted coordinates — e.g. 01°20'58"N 103°49'13"E. |
| heading | number | — | Heading in degrees (0–359). Three-digit padded. |
| speed | number | — | Speed value. |
| speedUnit | "m/s" | "km/h" | "kn" | "m/s" | Speed unit. |
| vertical | number | — | Vertical position. Label resolves from domain. |
| verticalUnit | "m" | "ft" | "m" | Vertical unit. |
| verticalRef | string | — | Reference marker — e.g. "AGL", "MSL", "BLW". |
| mission | MissionStep | — | Mission step progress — { current, total, label? }. |
| operator | string | — | Active operator — who is currently driving / supervising. |
| lastContact | string | — | Time since last heartbeat — e.g. "0.3 s ago". Surfaces in the footer. |
| extras | PlatformDetailExtra[] | — | Domain-specific rows surfaced between Mission and Last contact. Consecutive entries with the same section group under one header; an entry without a section merges into the immediately-preceding section. |
| fillHeight | boolean | false | Opt into fit-and-scroll. Root takes parent's full height; header and last-contact footer stay anchored; the body section scrolls within the frame. Use inside height-constrained containers. |
| className | string | — | Forwarded to the root container. |
Types
MissionStepMission progress. The detail renders as `{label} · {current}/{total}` when label is provided, otherwise just the count.
interface MissionStep {
current: number; // current step, 1-indexed
total: number; // total steps
label?: string; // optional step label, e.g. "WPT-04"
}PlatformDetailExtraShape of one entry in `extras`. Omit `section` to merge a row into the preceding section.
interface PlatformDetailExtra {
section?: string; // section header (consecutive same-section entries group)
label: string; // row label
value: ReactNode; // row value
}Wiring
Read-only. Pass the active platform's deep state. Compose to the right of platform roster — roster passes the selected id up to the consumer's state machine, the consumer looks up the platform's full record and passes the fields to this organism. Pass `extras` for domain-specific rows (payload, sensors, fuel) that the base props don't cover.
import {
CapacityBar,
PipCount,
StateDot,
StateText,
} from "@/components/rc3/indicators";
const active = fleet.find((p) => p.id === activeId);
<div className="flex gap-4">
<PlatformRoster
platforms={fleet.map(toRosterEntry)}
activeId={activeId}
onSelect={setActiveId}
/>
{active && (
<PlatformDetail
platform={active.id}
klass={active.klass}
domain={active.domain}
link={active.link}
signal={active.signal}
battery={active.battery}
autonomy={active.autonomy}
position={active.coords}
heading={active.heading}
speed={active.speed}
speedUnit={active.speedUnit}
vertical={active.altitude}
verticalUnit={active.altitudeUnit}
verticalRef={active.altitudeRef}
mission={active.mission}
operator={active.operator}
lastContact={active.lastContact}
extras={[
{
section: "Payload",
label: "Munition",
value: <PipCount filled={4} total={4} suffix="AGM-114" />,
},
{
section: "Payload",
label: "Status",
value: <StateText tone="danger">ARMED</StateText>,
},
{
section: "Endurance",
label: "Fuel",
value: <CapacityBar pct={62} suffix="62%" />,
},
{
section: "Sensors",
label: "EO/IR",
value: <StateDot state="active">Tracking</StateDot>,
},
]}
fillHeight
/>
)}
</div>Signal bars, battery gauge, and heading dial appear automatically next to their text values when signal, battery, and heading are passed — no separate prop. Text stays as the primary read; the indicators are a glanceable secondary read.
Set fillHeight when composing inside a height-constrained container (e.g. a sidebar column with flex-1 min-h-0): the card grows to fill the available height and the body section scrolls within the frame, so a card with many extras doesn't push the parent layout. Without it the card stays intrinsic-height.
Behavioural rule
Active context unambiguous
The Ember-dotted header marks which platform this detail is for. When the operator selects a different row in the roster, this card swaps wholesale — never a partial update that could leave fields from the previous platform showing.
Accessibility
| Region role | The root carries `role="region"` with an `aria-label` of the form `Platform detail for {platform}`. |
|---|---|
| Decorative parts | The Ember dot in the header is `aria-hidden`. Row labels and values carry the meaning. |
| Section labels | Comms / Telemetry / Mission are visible mono section headers, not just heading roles — they read consistently across operator surfaces. |
| Colour and meaning | Link and battery state colour is paired with text. Operators with colour-vision differences still read state. |
| Last contact | Surfaces honestly when contact ages — pair with consumer logic that classifies the value (e.g. show in danger after a threshold). The organism doesn't classify the age; that's domain-specific. |
JavaFX
Ships in the PRIZM JavaFX library for thick-client C3 apps as Rc3PlatformDetail (extends VBox). Run the gallery to see it natively.
import design.prizm.fx.rc3.Rc3PlatformDetail;
Rc3PlatformDetail()
Rc3PlatformDetail(String platform)| Member | Type | Default | Description |
|---|---|---|---|
| LinkStatus / UxvDomain | enum | — | Comms status + vertical label (ALT / ELEV / DEPTH). |
| MissionStep | record(int current, int total, String label) | — | label optional. |
| Extra | record(String section, String label, Node value) | — | Domain-specific row; consecutive same-section entries group. value is any Node — pass an Rc3Indicators primitive (or a String for plain text). |
| setLink / setSignal / setBattery | → void | — | Comms section — signal bars + battery gauge. |
| setPosition / setHeading / setSpeed / setVertical | → void | — | Telemetry section — heading dial. |
| setMission / setOperator | → void | — | Mission section. |
| setExtras / setLastContact | → void | — | Extra rows + last-contact footer. |
| setFillHeight | (boolean) → void | false | Pins header + footer, scrolls the body. |
Master-detail companion to Rc3PlatformRoster; honours invariant 3. The Rc3Indicators alphabet (pipCount / capacityBar / stateDot / stateText) composes into extra values so payload / sensor state reads as instruments. Mirrors components/rc3/platform-detail.tsx.
Usage
Compose as the master-detail companion to platform roster. Pass only the fields the platform actually surfaces — the card stays useful even when most fields are absent (a lost-link platform shows the identifier, lastContact age, and whatever final state was captured before contact dropped). Don't carry stale values forward when contact is lost: omit fields whose freshness can't be guaranteed.