Alert card grid
Alert cards in two columns, or one on a narrow screen, with section dividers across both.
On this page
Code
import { AlertCard } from "@bosunkit/components/alert-card";
import { Badge } from "@bosunkit/components/badge";
import { Button } from "@bosunkit/components/button";
import { AlertCardGrid } from "@bosunkit/components/alert-card-grid";
import { SectionDivider } from "@bosunkit/components/section-divider";
import { useEffect, useState, type Dispatch, type ReactElement, type SetStateAction } from "react";
export const description: string =
"A full alert list at anchor: immediate alarms side by side, a warning across both columns, the cards being " +
"monitored, and a cleared one, with live timers and acknowledge buttons.";
/** The anchor drag alarm has been raised this many seconds before the example mounts. */
const DRAG_STARTED_SECONDS: number = 192;
/** The bilge alarm has been raised this many seconds before the example mounts. */
const BILGE_STARTED_SECONDS: number = 47;
function elapsed(seconds: number): string {
const minutes: number = Math.floor(seconds / 60);
const rest: number = seconds % 60;
return `${String(minutes)}m ${String(rest).padStart(2, "0")}s`;
}
export default function Showcase(): ReactElement {
const [ticks, setTicks]: [number, Dispatch<SetStateAction<number>>] = useState<number>(0);
const [dragAcknowledged, setDragAcknowledged]: [boolean, Dispatch<SetStateAction<boolean>>] =
useState<boolean>(false);
const [bilgeAcknowledged, setBilgeAcknowledged]: [boolean, Dispatch<SetStateAction<boolean>>] =
useState<boolean>(false);
useEffect((): (() => void) => {
const timer: number = window.setInterval((): void => {
setTicks((current: number): number => current + 1);
}, 1000);
return (): void => {
window.clearInterval(timer);
};
}, []);
return (
<div style={{ width: "100%" }}>
<AlertCardGrid>
<SectionDivider label="Immediate" variant="immediate" />
<AlertCard
priority="alarm"
title="Anchor drag"
time="02:11"
timeReference="UTC"
immediate
acknowledged={dragAcknowledged}
message={
"SV Althea has moved 42 m from the drop position, outside the 35 m swing radius. " +
"Wind southwest 24 kn, gusting 31 kn."
}
badges={<Badge variant="red">Swing radius</Badge>}
deliveryChannels={[
{ channel: "voice", status: "sent" },
{ channel: "telegram", status: "read" },
{ channel: "whatsapp", status: "failed" },
]}
advisory={[
"Start the engine and take the strain off the rode.",
"Wake the skipper.",
"Re-set the anchor in 6 m on sand, or move to the buoys off Camaret.",
]}
actions={
<Button
variant="primary"
disabled={dragAcknowledged}
onClick={(): void => {
setDragAcknowledged(true);
}}
>
Acknowledge
</Button>
}
figures={[
{ label: "From drop", value: "42 m" },
{ label: "Bearing", value: "047°" },
]}
elapsed={elapsed(DRAG_STARTED_SECONDS + ticks)}
/>
<AlertCard
priority="alarm"
title="Bilge water high"
time="02:13"
timeReference="UTC"
immediate
acknowledged={bilgeAcknowledged}
message="The aft bilge pump is running without stopping. Check the stern gland and the cockpit drains."
deliveryChannels={[
{ channel: "voice", status: "sent" },
{ channel: "telegram", status: "pending" },
]}
actions={
<Button
variant="primary"
disabled={bilgeAcknowledged}
onClick={(): void => {
setBilgeAcknowledged(true);
}}
>
Acknowledge
</Button>
}
figures={[{ label: "Pump cycles", value: "9 / h" }]}
elapsed={elapsed(BILGE_STARTED_SECONDS + ticks)}
/>
<SectionDivider label="Monitoring" variant="monitoring" />
<AlertCard
className="alarm--full"
priority="warning"
title="Wind rising above the anchoring limit"
time="01:40"
acknowledged
message={
"True wind has risen from 14 to 24 kn over the last hour, and the forecast holds southwest 25 to 30 kn " +
"until 06:00. The anchorage at Morgat is open to the southwest; plan a move before first light."
}
badges={<Badge variant="amber">Forecast 30 kn</Badge>}
figures={[
{ label: "True wind", value: "24 kn" },
{ label: "Gusts", value: "31 kn" },
{ label: "Barometer", value: "1004 hPa" },
]}
/>
<AlertCard
priority="caution"
title="House bank at 48%"
time="00:52"
acknowledged
message="The house bank will reach 40% at about 05:30 at the present 6.2 A draw."
figures={[{ label: "Voltage", value: "12.4 V" }]}
/>
<AlertCard
priority="none"
title="Tidal gate opens"
time="00:30"
acknowledged
message="The Raz de Sein turns fair at 14:20; leave Morgat by 12:45 to carry it south."
/>
<SectionDivider label="Cleared" variant="cleared" />
<AlertCard
priority="caution"
title="Shallow water"
time="23:18"
acknowledged
cleared
message="Depth fell to 3.1 m on the approach; it has been above 5.0 m since 23:26."
/>
</AlertCardGrid>
</div>
);
}
A card grid lays out the alert cards of a list. It has two columns, and one on a narrow screen or a touch screen. A
section divider placed in the grid spans both columns, so the cards below it
read as a group. A card that carries alarm--full also takes both columns.
Examples
Cards under a divider
Code
import { AlertCardGrid } from "@bosunkit/components/alert-card-grid";
import { SectionDivider } from "@bosunkit/components/section-divider";
import type { ReactElement } from "react";
export const description: string = "A divider that spans both columns, then two cards side by side.";
export default function CardsUnderADivider(): ReactElement {
return (
<div style={{ width: "100%" }}>
<AlertCardGrid>
<SectionDivider label="Immediate" variant="immediate" />
<div>Anchor drag</div>
<div>Shallow water</div>
</AlertCardGrid>
</div>
);
}
On a narrow screen
The grid takes one column on a phone or a tablet, a screen up to the 68.75em breakpoint (1,100 px at 16 px), with
16 px between the cards. The width is the screen's the grid sits in, not the window's: a page, an app's main area or a
phone frame declares itself the screen with container: bk-screen / inline-size, and outside one the grid keeps two
columns. A touch screen wider than a tablet keeps the two columns: a coarse pointer sizes targets, not the layout.
Code
import { AlertCard } from "@bosunkit/components/alert-card";
import { AlertCardGrid } from "@bosunkit/components/alert-card-grid";
import { SectionDivider } from "@bosunkit/components/section-divider";
import type { ReactElement } from "react";
export const description: string = "A 360 px screen: the same alert list in one column, the cards 16 px apart.";
export default function OnANarrowScreen(): ReactElement {
return (
<div style={{ container: "bk-screen / inline-size", width: "100%", maxWidth: 360 }}>
<AlertCardGrid>
<SectionDivider key="immediate" label="Immediate" variant="immediate" />
<AlertCard key="drag" priority="alarm" title="Anchor drag" message="41 m from the drop, radius 35 m." />
<AlertCard key="wind" priority="warning" title="True wind over 25 kn" message="Gusts to 31 kn from 225°." />
</AlertCardGrid>
</div>
);
}
Motion
An alert list moves by default, as the owner decided for alert lists. A new alert's card rises in over at most the
medium motion duration, with its text in the page and announced from the first frame; a
cleared card fades out with the fast exit and takes no press while it leaves; and the cards slide to a new order.
While a card has focus, or the pointer is pressed on one, the new order applies at once, so nothing moves under a
finger. Under reduced motion cards only fade, and a new order always applies at once. Cards on screen when the list
first renders do not move. Give each card a key. motion={false} turns all of it off.
Code
import { AlertCard } from "@bosunkit/components/alert-card";
import { Button } from "@bosunkit/components/button";
import { AlertCardGrid } from "@bosunkit/components/alert-card-grid";
import type { Priority } from "@bosunkit/components/priority";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";
export const description: string =
"An alert list that moves: a raised alarm rises in at the top, a cleared alert fades out, and a warning that " +
"turns into an alarm slides up to its new place. While a card has focus, the new order applies at once.";
interface BoardAlert {
readonly id: string;
readonly priority: Priority;
readonly title: string;
readonly message: string;
}
const RANK: Readonly<Record<Priority, number>> = { emergency: 0, alarm: 1, warning: 2, caution: 3, none: 4 };
const START: readonly BoardAlert[] = [
{ id: "wind", priority: "warning", title: "Wind above reef threshold", message: "22 kn gusts, reef at 20" },
{ id: "depth", priority: "warning", title: "Shallow water", message: "2.4 m under the keel and shoaling" },
{ id: "battery", priority: "caution", title: "House battery low", message: "12.1 V, 48 % charge" },
];
function sorted(alerts: readonly BoardAlert[]): BoardAlert[] {
return [...alerts].sort((a: BoardAlert, b: BoardAlert): number => RANK[a.priority] - RANK[b.priority]);
}
export default function Motion(): ReactElement {
const [alerts, setAlerts]: [readonly BoardAlert[], Dispatch<SetStateAction<readonly BoardAlert[]>>] =
useState<readonly BoardAlert[]>(START);
const has: (id: string) => boolean = (id: string): boolean =>
alerts.some((alert: BoardAlert): boolean => alert.id === id);
return (
<div style={{ display: "flex", flexDirection: "column", gap: "var(--space-md)", width: "100%" }}>
<div style={{ display: "flex", flexWrap: "wrap", gap: "var(--space-sm)" }}>
<Button
size="sm"
disabled={has("bilge")}
onClick={(): void => {
setAlerts((current: readonly BoardAlert[]): BoardAlert[] =>
sorted([
...current,
{ id: "bilge", priority: "alarm", title: "Bilge pump running", message: "6 min without stopping" },
]),
);
}}
>
Raise a bilge alarm
</Button>
<Button
size="sm"
onClick={(): void => {
setAlerts((current: readonly BoardAlert[]): BoardAlert[] =>
sorted(
current.map(
(alert: BoardAlert): BoardAlert =>
alert.id === "depth"
? { ...alert, priority: "alarm", message: "1.2 m under the keel and shoaling" }
: alert,
),
),
);
}}
>
The water shoals
</Button>
<Button
size="sm"
onClick={(): void => {
setAlerts((current: readonly BoardAlert[]): BoardAlert[] =>
current.filter((alert: BoardAlert): boolean => alert.id !== "wind"),
);
}}
>
The wind eases
</Button>
<Button
size="sm"
variant="ghost"
onClick={(): void => {
setAlerts(START);
}}
>
Start again
</Button>
</div>
<AlertCardGrid>
{alerts.map(
(alert: BoardAlert): ReactElement => (
<AlertCard key={alert.id} priority={alert.priority} title={alert.title} message={alert.message} />
),
)}
</AlertCardGrid>
</div>
);
}
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| childrenrequired | ReactNode | – | The cards, and the section dividers between them. Give each a key, so the right one leaves when it clears. |
| className | string | undefined | – | Classes added to the grid's own. |
| motion | boolean | undefined | true | Whether the list moves: on by default. A new alert card rises in, a cleared one fades out, and the cards slide to
a new order, except while the crew is using the list (a card has focus, or the pointer is pressed on it), when the
new order applies at once. Cards on screen when the grid first renders do not move. false turns it off. |
Related
- Alert card: the card a grid holds.
- Section divider: the named rule between groups of cards.
- Alarms and severity: the order the groups follow.