/** * The Pause/Resume control, at the foot of the sidebar. A pause stops filtering * for the whole resolver, so it belongs to the shell rather than to any page, * and it has no second placement. * * It reads `Health.protection` rather than `/api/pause` so it cannot contradict * the Diagnostics health strip, and it renders nothing at all while protection * is unavailable or unknown — pausing a resolver that has no filter snapshot * would change nothing an operator could observe, and a state health has not * confirmed does not name an action either. * * A pause says so, wherever the control is. "Resume" alone names an action * without stating the state it would end, and with the header indicator and the * Overview status row both gone the sidebar is the only place most pages can * carry that fact at all: a paused resolver would otherwise leave no trace * outside the Diagnostics page. The line and the health strip cannot disagree — * one `protection` reading, one clock format, and the expiry refetch in * `useProtection` retires both at the same moment. An active resolver gets no * line: the button says Pause, which is the whole message. */ import { useEffect } from "react"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import * as stylex from "@stylexjs/stylex"; import { Button, Menu, MenuItem, MenuTrigger, Popover } from "react-aria-components"; import { formatClock } from "@/lib/format"; import { pauseMutation } from "@/lib/queries"; import InlineError from "@/lib/InlineError"; import { styles as shared } from "@/ui/styles"; import { colors } from "@/ui/tokens.stylex"; import { useProtection } from "./protection"; const DURATIONS = [ { label: "60 seconds", seconds: 60 }, { label: "5 minutes", seconds: 300 }, { label: "30 minutes", seconds: 1800 }, { label: "Indefinitely", seconds: null }, ] as const; const styles = stylex.create({ /** * Disabled text darkens in light scheme and lightens in dark, the opposite * direction from `textMuted`, so the token cannot express it. */ trigger: { color: { default: null, ":disabled": "oklch(70.5% 0.015 286.067)", "@media (prefers-color-scheme: dark)": { default: null, ":disabled": "oklch(44.2% 0.017 285.786)" }, }, /** The sidebar foot is the only placement, and there Log out sets the width. */ width: "100%", }, row: { display: "flex", flexDirection: "column", alignItems: "flex-start", gap: "0.25rem", }, /** Text, never a colour or an icon alone: this is the state, spelled out. */ state: { fontSize: "0.75rem", lineHeight: "1rem", color: colors.textSecondary, }, /** `--trigger-width` is RAC's: the menu is as wide as the button that opened it. */ popover: { width: "var(--trigger-width)", borderRadius: "0.25rem", borderWidth: 1, borderStyle: "solid", borderColor: colors.border, backgroundColor: colors.surfaceRaised, color: colors.text, boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)", }, menu: { outlineStyle: "none", paddingBlock: "0.25rem", }, menuItem: { cursor: "pointer", paddingInline: "0.75rem", paddingBlock: "0.375rem", fontSize: "0.875rem", lineHeight: "1.25rem", }, /** * RAC focuses the item's own node, so the shared ring applies; it is inset * because an item flush against the popover edge clips an outset one, and * recoloured because the focus token is the blue this row just painted. */ menuItemFocused: { backgroundColor: colors.primary, color: colors.primaryText, outlineColor: { default: null, ":focus-visible": colors.primaryText }, }, }); export default function PauseControl() { const queryClient = useQueryClient(); const protection = useProtection(); const mutation = useMutation(pauseMutation(queryClient)); const paused = protection.state === "paused"; const { reset } = mutation; useEffect(() => reset(), [paused, reset]); // Nothing to offer, by the same rule in both cases: unavailable has no action // worth taking, and unknown has no way to tell which of the two it would be. // A disabled "Pause" beside a reading that says "Paused until 14:05" names // the wrong action, which is the contradiction this control exists to end. const actionable = protection.state === "active" || protection.state === "paused"; if (!actionable) return null; if (paused) { const until = protection.state === "paused" ? protection.until : null; return (

{until === null ? "Paused" : `Paused until ${formatClock(until)}`}

); } return (
stylex.props(styles.popover).className ?? ""}> {DURATIONS.map(({ label, seconds }) => ( mutation.mutate( seconds === null ? { paused: true } : { paused: true, duration_seconds: seconds }, ) } className={({ isFocused }) => stylex.props( styles.menuItem, shared.insetFocusRing, isFocused && styles.menuItemFocused, ).className ?? "" } > {label} ))}
); }