Skip to content
design
Components

Live data connection notice

The red notice shown when the browser has lost its live connection to Bosun.

On this page
Four passage readings that update every second until the stream from Bosun drops, when the notice appears and each reading turns stale with its age.
Code
import { Button } from "@bosunkit/components/button";
import { LiveDataConnectionNotice } from "@bosunkit/components/live-data-connection-notice";
import { Reading } from "@bosunkit/components/reading";
import { RefreshCw, Unplug } from "lucide-react";
import { useEffect, useState, type Dispatch, type ReactElement, type SetStateAction } from "react";

export const description: string =
  "A passage page whose readings update every second until the browser loses its stream from Bosun: the notice " +
  "appears above them and every reading turns stale with its age, until the stream returns.";

interface Feed {
  readonly connected: boolean;
  /** Seconds in the current state. */
  readonly seconds: number;
  readonly sog: number;
  readonly depth: number;
  readonly aws: number;
}

const START: Feed = { connected: true, seconds: 0, sog: 6.4, depth: 23.6, aws: 16.2 };

/** How long the example's stream stays up, and how long it stays lost, before it changes by itself. */
const LIVE_FOR_SECONDS: number = 8;
const LOST_FOR_SECONDS: number = 14;

function tick(feed: Feed): Feed {
  if (!feed.connected) {
    return feed.seconds + 1 >= LOST_FOR_SECONDS
      ? { ...feed, connected: true, seconds: 0 }
      : { ...feed, seconds: feed.seconds + 1 };
  }
  if (feed.seconds + 1 >= LIVE_FOR_SECONDS) {
    return { ...feed, connected: false, seconds: 0 };
  }
  // A gentle swell on the readings, so a frozen value is easy to tell from a live one.
  const phase: number = feed.seconds + 1;
  return {
    ...feed,
    seconds: phase,
    sog: 6.4 + Math.sin(phase / 2) * 0.3,
    depth: 23.6 + Math.sin(phase / 4) * 1.2,
    aws: 16.2 + Math.cos(phase / 3) * 1.4,
  };
}

export default function Showcase(): ReactElement {
  const [feed, setFeed]: [Feed, Dispatch<SetStateAction<Feed>>] = useState<Feed>(START);

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

  const integrity: "fresh" | "stale" = feed.connected ? "fresh" : "stale";
  const age: number | undefined = feed.connected ? undefined : feed.seconds + 1;

  return (
    <div style={{ display: "flex", flexDirection: "column", width: "100%" }}>
      {!feed.connected && <LiveDataConnectionNotice data-testid="live-data-lost" />}
      <div style={{ display: "flex", gap: "var(--space-xl)", flexWrap: "wrap", alignItems: "flex-start" }}>
        <Reading
          value={feed.sog}
          unit="kn"
          label="SOG"
          source="GPS"
          fractionDigits={1}
          integrity={integrity}
          ageSeconds={age}
        />
        <Reading
          value={feed.depth}
          unit="m"
          label="Depth"
          source="DST810"
          fractionDigits={1}
          maxDigits={2}
          integrity={integrity}
          ageSeconds={age}
        />
        <Reading
          value={feed.aws}
          unit="kn"
          label="AWS"
          source="WS320"
          fractionDigits={1}
          integrity={integrity}
          ageSeconds={age}
        />
        <Reading value={41} unit="NM" label="To A Coruña" source="Route" integrity={integrity} ageSeconds={age} />
      </div>
      <div style={{ marginTop: "var(--space-lg)" }}>
        <Button
          size="sm"
          onClick={(): void => {
            setFeed((current: Feed): Feed => ({ ...current, connected: !current.connected, seconds: 0 }));
          }}
        >
          {feed.connected ? <Unplug size={14} /> : <RefreshCw size={14} />}
          {feed.connected ? "Drop the stream" : "Restore the stream"}
        </Button>
      </div>
    </div>
  );
}

The live data connection notice tells the crew that the page they are reading has stopped updating. It carries a red badge and one line of text, and it is announced as an alert so a screen reader speaks it when it appears. It takes the props of a div.

Examples

Lost

The notice shown when the browser has lost its live connection to Bosun.
Code
import { LiveDataConnectionNotice } from "@bosunkit/components/live-data-connection-notice";
import type { ReactElement } from "react";

export const description: string = "The notice shown when the browser has lost its live connection to Bosun.";

export default function Lost(): ReactElement {
  return <LiveDataConnectionNotice />;
}

Motion

By default the notice rises in when the caller shows it inside a MotionPresence as the connection is lost, 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 the browser loses its live connection to Bosun, 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 { LiveDataConnectionNotice } from "@bosunkit/components/live-data-connection-notice";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";

export const description: string =
  "The notice rises in when the browser loses its live connection to Bosun, 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 connection returns" : "The connection drops"}
      </Button>
      <div style={{ minHeight: 56 }}>
        <MotionPresence>{lost && <LiveDataConnectionNotice key="notice" />}</MotionPresence>
      </div>
    </div>
  );
}

Props

PropTypeDefaultDescription
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: how a reading that stops arriving is shown.
  • Badge: the red badge the notice carries.