Skip to content
design
Components

NMEA offline notice

The red notice shown when Bosun receives no current instrument data from the NMEA network.

On this page

Navigation

Tanks

Two pages while the NMEA 2000 network goes quiet, one keeping its last values under the last-known notice and one with none under the plain notice.
Code
import { Button } from "@bosunkit/components/button";
import { Card } from "@bosunkit/components/card";
import { NmeaOfflineNotice } from "@bosunkit/components/nmea-offline-notice";
import { Reading } from "@bosunkit/components/reading";
import { Cable, Gauge, Navigation } from "lucide-react";
import { useEffect, useState, type CSSProperties, type Dispatch, type ReactElement, type SetStateAction } from "react";

export const description: string =
  "Two pages while the NMEA 2000 network goes quiet: the navigation page keeps its last values with their age, " +
  "and the tanks page, opened after the data stopped, has none to show.";

interface Network {
  readonly online: boolean;
  /** Seconds in the current state. */
  readonly seconds: number;
}

/** How long the example's network reports, and how long it stays quiet, before it changes by itself. */
const ONLINE_FOR_SECONDS: number = 6;
const QUIET_FOR_SECONDS: number = 16;

/** The card's content: the readings, and the notice under them while the network is quiet. */
const STACK: CSSProperties = { display: "flex", flexDirection: "column", gap: "var(--space-md)" };

function tick(network: Network): Network {
  const limit: number = network.online ? ONLINE_FOR_SECONDS : QUIET_FOR_SECONDS;
  return network.seconds + 1 >= limit
    ? { online: !network.online, seconds: 0 }
    : { ...network, seconds: network.seconds + 1 };
}

export default function Showcase(): ReactElement {
  const [network, setNetwork]: [Network, Dispatch<SetStateAction<Network>>] = useState<Network>({
    online: true,
    seconds: 0,
  });

  useEffect((): (() => void) => {
    const timer: ReturnType<typeof setInterval> = setInterval((): void => {
      setNetwork(tick);
    }, 1000);
    return (): void => {
      clearInterval(timer);
    };
  }, []);

  const quiet: boolean = !network.online;
  // Seconds since the last reading arrived: the navigation page shows how old its values are.
  const lastSeen: number | undefined = quiet ? network.seconds + 1 : undefined;
  const swell: number = network.online ? network.seconds : 0;

  return (
    <div style={{ display: "flex", flexDirection: "column", gap: "var(--space-lg)", width: "100%" }}>
      <div
        style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))", gap: "var(--space-md)" }}
      >
        <Card icon={<Navigation />} title="Navigation">
          <div style={STACK}>
            <div style={{ display: "flex", gap: "var(--space-lg)", flexWrap: "wrap" }}>
              <Reading
                value={6.8 + Math.sin(swell / 2) * 0.2}
                unit="kn"
                label="SOG"
                source="GPS"
                fractionDigits={1}
                integrity={quiet ? "invalid" : "fresh"}
                ageSeconds={lastSeen}
              />
              <Reading
                value={14.2 - swell * 0.1}
                unit="m"
                label="Depth"
                source="DST810"
                fractionDigits={1}
                maxDigits={2}
                integrity={quiet ? "invalid" : "fresh"}
                ageSeconds={lastSeen}
              />
              <Reading
                value={218}
                unit="°T"
                label="HDG"
                source="Compass"
                integrity={quiet ? "invalid" : "fresh"}
                ageSeconds={lastSeen}
              />
            </div>
            {quiet && <NmeaOfflineNotice hasLastKnownData quietSeconds={lastSeen} data-testid="nmea-offline-navigation" />}
          </div>
        </Card>
        <Card icon={<Gauge />} title="Tanks">
          <div style={STACK}>
            <div style={{ display: "flex", gap: "var(--space-lg)", flexWrap: "wrap" }}>
              <Reading value={quiet ? null : 212} unit="L" label="Diesel" source="Tank 1" />
              <Reading value={quiet ? null : 365} unit="L" label="Water" source="Tanks 2–3" />
              <Reading value={quiet ? null : 18} unit="%" label="Holding" source="Tank 4" />
            </div>
            {quiet && <NmeaOfflineNotice quietSeconds={lastSeen} data-testid="nmea-offline-tanks" />}
          </div>
        </Card>
      </div>
      <div>
        <Button
          size="sm"
          onClick={(): void => {
            setNetwork((current: Network): Network => ({ online: !current.online, seconds: 0 }));
          }}
        >
          <Cable size={14} />
          {network.online ? "Silence the network" : "Bring the network back"}
        </Button>
      </div>
    </div>
  );
}

The NMEA offline notice tells the crew that the instruments are not reporting, in the order the crew need it: what stopped and for how long, which readings on the page are from before, and what to check. It draws OpenBridge's signal-failure mark and the red that data integrity gives a lost reading.

  • quietSeconds is the time since the last reading arrived, the youngest of bosund's instrument_age_seconds. The notice writes it floored, in seconds for the first minute, minutes for the first hour, then hours: "for 2 min", never longer than it has been. Without it the notice says only that nothing is arriving.
  • With hasLastKnownData it says the readings below are the last ones received, each marked with its age, and not to navigate by them; without it, that readings appear again when data returns.

The notice sets no margin of its own; the page spaces it. It takes the props of a div.

Examples

With last known data

No instrument data

Nothing has arrived from the NMEA network for 2 min. The readings below are the last ones received, each marked with its age. Don't navigate by them until data returns.

Check that the instruments are switched on, then the NMEA gateway's power and cabling.

Two and a half minutes after the last reading, on a page that still shows the last values it received.
Code
import { NmeaOfflineNotice } from "@bosunkit/components/nmea-offline-notice";
import type { ReactElement } from "react";

export const description: string =
  "Two and a half minutes after the last reading, on a page that still shows the last values it received.";

export default function WithLastKnownData(): ReactElement {
  return <NmeaOfflineNotice hasLastKnownData quietSeconds={150} />;
}

No data

No instrument data

Nothing is arriving from the NMEA network. Readings appear again when data returns.

Check that the instruments are switched on, then the NMEA gateway's power and cabling.

The notice on a page with no earlier reading to show, and no time since the last one: it says only that nothing is arriving.
Code
import { NmeaOfflineNotice } from "@bosunkit/components/nmea-offline-notice";
import type { ReactElement } from "react";

export const description: string =
  "The notice on a page with no earlier reading to show, and no time since the last one: it says only that nothing is arriving.";

export default function NoData(): ReactElement {
  return <NmeaOfflineNotice />;
}

Motion

By default the notice rises in when the caller shows it inside a MotionPresence as the data stops, and fades out when it returns; a notice there on the first render does not move. motion takes the kit's motion prop: true does the same outside a presence, and false turns it off. Its words are announced from the first frame, and under reduced motion it only fades.

The notice rises in when Bosun stops receiving instrument data, announced from its first frame, and fades out when it comes back.
Code
import { Button } from "@bosunkit/components/button";
import { MotionPresence } from "@bosunkit/components/motion";
import { NmeaOfflineNotice } from "@bosunkit/components/nmea-offline-notice";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";

export const description: string =
  "The notice rises in when Bosun stops receiving instrument data, announced from its first frame, and fades out when it comes back.";

export default function Motion(): ReactElement {
  const [lost, setLost]: [boolean, Dispatch<SetStateAction<boolean>>] = useState<boolean>(false);
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: "var(--space-md)", width: "100%" }}>
      <Button
        size="sm"
        onClick={(): void => {
          setLost((current: boolean): boolean => !current);
        }}
      >
        {lost ? "The data returns" : "The instruments go quiet"}
      </Button>
      <div style={{ minHeight: 56 }}>
        <MotionPresence>{lost && <NmeaOfflineNotice key="notice" />}</MotionPresence>
      </div>
    </div>
  );
}

Props

PropTypeDefaultDescription
hasLastKnownDataboolean | undefinedfalseWhether the page below still shows readings from before the data stopped.
quietSecondsnumber | undefined–Seconds since the last instrument reading arrived, the youngest of bosund's instrument_age_seconds: the notice says how long nothing has arrived, floored so it never says longer. Left out, or not a number, it says only that nothing is arriving.
motionMotionProp | undefined–How the notice moves. By default, inside a MotionPresence where the caller shows and hides it, it rises in when the loss is noticed and fades out when it is over; a notice there on the first render does not move. true does the same outside a presence too; a preset (fade, rise, scale) or { enter, exit } chooses; false turns it off. Its words are announced from the first frame.
refRef<HTMLDivElement> | undefined–The notice's element.

Also takes every attribute of HTMLAttributes<HTMLDivElement>.

  • Data integrity: the four states a reading takes when its feed stops.
  • Reading: the value that keeps its place, with its age, when the feed is lost.