Skip to content
design
Instruments

Readout list item

A dense row for a list or a table, with a label, a value and the parts around it.

On this page
Distance to go
14.2
nmi
Route
Next waypoint
Cap de Formentor
Route
Sea temp
‒
°C
Transducer
Anchor alarm off until Sóller

Autopilot: the crew is turning the knob toward 064° for the Sóller lighthouse; the setpoint shows its focus state.

Clickable rows with leading icons, a setpoint the crew is dialing, an advice value, hinted zeros, degrees and a degree spacer, low-integrity and invalid data, an off value, text and a label-only row, aligned by hand on a live feed.
Code
import {
  ObcTextboxFontWeight,
  ReadoutDataQuality,
  ReadoutListItem,
  ReadoutSize,
  ReadoutValueType,
} from "@bosunkit/instruments";
import { Anchor, Compass, Gauge, MapPin, Navigation, Target, Waves, Wind } from "lucide-react";
import { useEffect, useState } from "react";
import type { CSSProperties, Dispatch, ReactElement, SetStateAction } from "react";

export const description: string =
  "Althea's passage rows on a one-second feed, aligned by hand with space reservers: a clickable autopilot row whose setpoint the crew is dialing, advised speed, hinted zeros, degrees beside a degree spacer, low-integrity wind, an engine that is off, text rows and a label-only anchor row; tap a row to see what it is for.";

/** Shared widths so the standalone rows line up without a list. */
const VALUE_RESERVER: string = "000.0";
const UNIT_RESERVER: string = "nmi";
const SOURCE_RESERVER: string = "Echosounder";

const ROWS_STYLE: CSSProperties = {
  display: "grid",
  gap: 2,
  padding: 12,
  borderRadius: 10,
  background: "var(--container-section-color)",
  width: "100%",
  maxWidth: 560,
};
const NOTE_STYLE: CSSProperties = { margin: "8px 4px 0", fontSize: 12, color: "var(--element-neutral-color)" };

type RowId = "autopilot" | "sog" | "depth" | "wind" | "engine";

const ROW_NOTES: Record<RowId, string> = {
  autopilot:
    "Autopilot: the crew is turning the knob toward 064° for the Sóller lighthouse; the setpoint shows its focus state.",
  sog: "Speed over ground against the 6.5 kn advised for an 18:00 arrival in Port de Sóller.",
  depth: "Depth from the echosounder; the forward sonar is the second source.",
  wind: "True wind from the masthead unit, marked low integrity until it is recalibrated.",
  engine: "The engine is stopped under sail; its readings return when it starts.",
};

export default function Showcase(): ReactElement {
  const [step, setStep]: [number, Dispatch<SetStateAction<number>>] = useState<number>(0);
  const [selected, setSelected]: [RowId, Dispatch<SetStateAction<RowId>>] = useState<RowId>("autopilot");

  useEffect((): (() => void) => {
    const timer: number = window.setInterval((): void => {
      setStep((previous: number): number => previous + 1);
    }, 1000);
    return (): void => {
      window.clearInterval(timer);
    };
  }, []);

  const phase: number = step % 20;
  const dialing: boolean = phase < 8;
  const headingSetpoint: number = dialing ? 54 + phase * 1.25 : 64;
  const heading: number = Math.min(headingSetpoint, 52 + phase * 0.9);
  const speed: number = 6.1 + 0.3 * Math.sin(step / 3);
  const distanceToGo: number = 14.2 - phase * 0.03;
  const depth: number = 42 + 6 * Math.sin(step / 5);
  const trueWind: number = 17 + 2 * Math.sin(step / 2);

  function select(id: RowId): () => void {
    return (): void => {
      setSelected(id);
    };
  }

  return (
    <div style={ROWS_STYLE}>
      <ReadoutListItem
        label="Autopilot"
        hasLeadingIcon
        leadingIcon={<Navigation size={16} aria-hidden />}
        value={heading}
        hasDegree
        maxDigits={3}
        hasSetpoint
        setpoint={headingSetpoint}
        setpointIcon={<Compass size={14} aria-hidden />}
        setpointOptions={{ touching: dialing, hintedZeros: true, spaceReserver: VALUE_RESERVER }}
        valueOptions={{ weight: ObcTextboxFontWeight.bold, hintedZeros: true, spaceReserver: VALUE_RESERVER }}
        unitOptions={{ spaceReserver: UNIT_RESERVER }}
        srcOptions={{ spaceReserver: SOURCE_RESERVER }}
        src="Fluxgate"
        size={ReadoutSize.medium}
        clickable
        onClick={select("autopilot")}
      />
      <ReadoutListItem
        label="SOG"
        hasLeadingIcon
        leadingIcon={<Gauge size={16} aria-hidden />}
        value={speed}
        fractionDigits={1}
        maxDigits={3}
        unit="kn"
        src="GPS"
        hasAdvice
        advice={6.5}
        adviceIcon={<Target size={14} aria-hidden />}
        adviceOptions={{ spaceReserver: VALUE_RESERVER }}
        valueOptions={{ spaceReserver: VALUE_RESERVER }}
        unitOptions={{ spaceReserver: UNIT_RESERVER }}
        srcOptions={{ spaceReserver: SOURCE_RESERVER }}
        hasDegreeSpacer
        clickable
        onClick={select("sog")}
      />
      <ReadoutListItem
        label="Distance to go"
        hasLeadingIcon
        leadingIcon={<MapPin size={16} aria-hidden />}
        value={distanceToGo}
        fractionDigits={1}
        maxDigits={3}
        unit="nmi"
        src="Route"
        valueOptions={{ weight: ObcTextboxFontWeight.semibold, spaceReserver: VALUE_RESERVER }}
        unitOptions={{ spaceReserver: UNIT_RESERVER }}
        srcOptions={{ spaceReserver: SOURCE_RESERVER }}
        hasDegreeSpacer
      />
      <ReadoutListItem
        label="Depth"
        hasLeadingIcon
        leadingIcon={<Waves size={16} aria-hidden />}
        value={depth}
        fractionDigits={1}
        maxDigits={3}
        unit="m"
        src="Echosounder"
        valueOptions={{ spaceReserver: VALUE_RESERVER, hintedZeros: true }}
        unitOptions={{ spaceReserver: UNIT_RESERVER }}
        srcOptions={{ spaceReserver: SOURCE_RESERVER }}
        hasDegreeSpacer
        clickable
        onClick={select("depth")}
      />
      <ReadoutListItem
        label="TWS"
        hasLeadingIcon
        leadingIcon={<Wind size={16} aria-hidden />}
        value={trueWind}
        fractionDigits={1}
        maxDigits={3}
        unit="kn"
        src="Masthead"
        dataQuality={ReadoutDataQuality.lowIntegrity}
        valueOptions={{ spaceReserver: VALUE_RESERVER }}
        unitOptions={{ spaceReserver: UNIT_RESERVER }}
        srcOptions={{ spaceReserver: SOURCE_RESERVER, dataQuality: ReadoutDataQuality.lowIntegrity }}
        hasDegreeSpacer
        clickable
        onClick={select("wind")}
      />
      <ReadoutListItem
        label="Engine"
        value={0}
        off
        offText="Stopped"
        unit="rpm"
        src="J1939"
        valueOptions={{ spaceReserver: VALUE_RESERVER }}
        unitOptions={{ spaceReserver: UNIT_RESERVER }}
        srcOptions={{ spaceReserver: SOURCE_RESERVER }}
        clickable
        onClick={select("engine")}
      />
      <ReadoutListItem
        label="Next waypoint"
        value="Cap de Formentor"
        valueType={ReadoutValueType.text}
        src="Route"
        srcOptions={{ spaceReserver: SOURCE_RESERVER }}
      />
      <ReadoutListItem
        label="Sea temp"
        value={null}
        unit="°C"
        src="Transducer"
        dataQuality={ReadoutDataQuality.invalid}
        valueOptions={{ spaceReserver: VALUE_RESERVER }}
        unitOptions={{ spaceReserver: UNIT_RESERVER }}
        srcOptions={{ spaceReserver: SOURCE_RESERVER }}
      />
      <ReadoutListItem
        label="Anchor alarm off until Sóller"
        hasLeadingIcon
        leadingIcon={<Anchor size={16} aria-hidden />}
        hasValue={false}
      />
      <p style={NOTE_STYLE}>{ROW_NOTES[selected]}</p>
    </div>
  );
}

A readout list item is one row of a list or a table. It shows a label at the left and a value at the right, with an optional unit, source, setpoint and advice value. stacking places the unit and the source: after the value, or beside the label. fractionDigits sets the decimals and maxDigits reserves the integer width. clickable makes the row a button that calls onClick.

Put rows in a readout list to line up their columns. Use the readout for a single value with more room.

Examples

With unit and source

Depth
23.5
m
Sonar
A row with a label, a value to one decimal, a unit and the source of the reading.
Code
import { ReadoutListItem } from "@bosunkit/instruments";
import type { ReactElement } from "react";

export const description: string = "A row with a label, a value to one decimal, a unit and the source of the reading.";

export default function WithUnitAndSource(): ReactElement {
  return (
    <ReadoutListItem label="Depth" value={23.456} fractionDigits={1} unit="m" src="Sonar" />
  );
}

With setpoint

30
20
A row with a value of 20 and a setpoint of 30 beside it.
Code
import { ReadoutListItem } from "@bosunkit/instruments";
import type { ReactElement } from "react";

export const description: string = "A row with a value of 20 and a setpoint of 30 beside it.";

export default function WithSetpoint(): ReactElement {
  return (
    <ReadoutListItem value={20} hasSetpoint setpoint={30} />
  );
}

Rows and actions

Keep value, unit and source in stable columns through ReadoutList. Reserve widths for the largest expected value and test the longest translated label. Use stacking to fit the relationship, not to conceal missing units or distinguish two otherwise ambiguous readings.

Make a row clickable only when it opens details or performs a clear action. Keep its label meaningful as a button name and avoid embedding competing controls inside a clickable row. Read-only report rows should remain read-only, with source and quality information available in the surrounding text.

Props

PropTypeDefaultDescription
labelstring | undefined–Name of the quantity, shown at the left of the row, for example "Depth".
unitstring | undefined–Unit of the value, for example "m". Its place follows stacking. Nothing is shown when it is empty.
srcstring | undefined–Name of the source of the reading, for example "Sonar". Its place follows stacking. A blank string shows no source.
hasValueboolean | undefinedtrueLayout switch: false renders a deliberately value-less (label-only) row that hugs its remaining parts. For a temporarily missing value keep hasValue and set value to null instead — the dash keeps the value block at full size, so the row does not shift when data arrives.
valuenumber | string | null | undefinednullThe value; null renders a dash. A number by default, or text when {@link ReadoutListItemProps.valueType} is text.
valueTypeReadoutValueType | undefinedReadoutValueType.numberHow {@link ReadoutListItemProps.value} is interpreted. number (default) formats it via fractionDigits; text renders it verbatim and ignores the numeric format options.
offboolean | undefinedfalseRender the value as offText (e.g. equipment powered down). Affects the value only.
offTextstring | undefined"OFF"Text shown in place of the value when off is true. Available when off==true.
hasSetpointboolean | undefinedfalseShows the setpoint block beside the value.
setpointnumber | undefined–Available when hasSetpoint==true.
hasAdviceboolean | undefinedfalseShows the advice block beside the value.
advicenumber | undefined–Available when hasAdvice==true.
sizeReadoutListItemSize | undefined–Density of the row: small (default), medium or large. Larger sizes use larger digits.
priorityReadoutListItemPriority | undefined–Color emphasis of the value: regular (default) or enhanced, the in-command accent color.
stackingReadoutListItemStacking | undefined–Where the unit and source sit. trailing-unit (default) puts the unit after the value and the source after a divider. leading-unit puts the unit beside the label. leading-src puts the source beside the label.
clickableboolean | ReadoutListItemClickable | undefinedfalseRenders the row as a button. true gives a squared surface; an object picks the corner style.
hasLeadingIconboolean | undefinedfalseShows leadingIcon before the label.
hasDegreeboolean | undefinedfalseAppends a degree sign (°) after each number, for headings and angles.
hasDegreeSpacerboolean | undefinedfalseOn a row without a degree sign, reserves the width of one, so its digits line up with the rows that have it.
hasLeadingIconSpacerboolean | undefinedfalseOn a row without a leading icon, reserves the icon's width (house deviation), so its label starts where the labels of the rows with icons start. ReadoutList sets it when any of its rows has one.
fractionDigitsnumber | undefined0Also formats the numeric setpoint / advice blocks, which stay numeric even when the value is text. Available when valueType==number || hasSetpoint || hasAdvice.
maxDigitsnumber | undefined0Same availability as {@link ReadoutListItemProps.fractionDigits}.
dataQualityReadoutListItemDataQuality | undefined–Marks the value as low-integrity or invalid with an outline.
alertboolean | AlertFrameConfig | undefinedfalseRow-level alert frame. true normalizes to a default frame; an {@link AlertFrameConfig} object configures it; false renders none.
valueOptionsReadoutValueOptions | undefined–Options for the value block: hinted leading zeros, font weight, icon, alert frame, data quality and reserved width.
setpointOptionsReadoutSetpointOptions | undefined–Options for the setpoint block, including how it behaves as the value reaches it. Applies when hasSetpoint is set.
adviceOptionsReadoutAdviceOptions | undefined–Options for the advice block: hinted leading zeros, alert frame, data quality and reserved width. Applies when hasAdvice is set.
unitOptionsReadoutReserverOptions | undefined–Options for the unit: the longest unit to reserve width for, so the unit columns of several rows line up.
srcOptionsReadoutSrcOptions | undefined–Options for the source: alert frame, data quality and the longest source to reserve width for.
showDebugOverlayboolean | undefinedfalseDevelopment aid: outline the readout building blocks (red), the degree columns (blue) and the degree spacer (green) so reserver widths / alignment are visible. Off by default.
listColumnsReadoutListColumns | undefined–Every unit and every source of the rows in this row's list (house deviation). ReadoutList sets it, so the unit and source columns take the width of the widest entry as drawn, not of the one with the most characters ("Echosounder" is wider than "Bilge switch"). When set, it reserves those columns in place of the spaceReserver of unitOptions and srcOptions.
leadingIconReactNode | undefined–Icon before the label (the upstream leading-icon slot). Available when hasLeadingIcon==true.
valueIconReactNode | undefined–Icon before the value (the upstream value-icon slot). Available when valueOptions.hasIcon==true.
setpointIconReactNode | undefined–Overrides the default setpoint icon (the upstream setpoint-icon slot).
adviceIconReactNode | undefined–Overrides the default advice icon (the upstream advice-icon slot).
onClickMouseEventHandler<HTMLButtonElement> | undefined–Click handler for the clickable row (the upstream native button click event).
classNamestring | undefined–Class name added to the root element.