diff --git a/docs/components.md b/docs/components.md
index 0912a22..159713e 100644
--- a/docs/components.md
+++ b/docs/components.md
@@ -251,11 +251,16 @@ options as an ARIA button group.
| Prop | Type | Required | Notes |
| --- | --- | --- | --- |
| `variant` | `"ok" \| "warn" \| "down"` | yes | Maps to Operational, Degraded, or Down text. |
+| `label` | `ReactNode` | no | Overrides the default per-variant text. An omitted, `null`, or empty-string value falls back to the variant default, so a label is always present. |
-The color dot is decorative; the visible label carries the status meaning.
+The color dot is decorative (`aria-hidden`); the visible label carries the
+status meaning. Use `label` to reuse the same dot affordance for states outside
+the three defaults — for example `"Paused"` on a `warn` dot — without rendering a
+separate element.
```tsx
+
```
### `Spinner`
diff --git a/src/app/docs/page.tsx b/src/app/docs/page.tsx
index 6769a87..ab17d1d 100644
--- a/src/app/docs/page.tsx
+++ b/src/app/docs/page.tsx
@@ -1,5 +1,7 @@
import { PageShell } from "@/components/PageShell";
+import { CurlBlock } from "@/components/CurlBlock";
import { messages } from "@/lib/messages";
+import { resolveApiBase } from "@/lib/resolveApiBase";
export const metadata = { title: "Docs — AgentPay" };
diff --git a/src/components/StatusDot.tsx b/src/components/StatusDot.tsx
index 3ed07e2..606c1bb 100644
--- a/src/components/StatusDot.tsx
+++ b/src/components/StatusDot.tsx
@@ -1,3 +1,5 @@
+import { type ReactNode } from "react";
+
type Variant = "ok" | "warn" | "down";
const variants: Record = {
@@ -12,14 +14,41 @@ const labels: Record = {
down: "Down",
};
-export function StatusDot({ variant }: { variant: Variant }) {
+/**
+ * A colour-coded status indicator: a small decorative dot paired with a visible
+ * text label, so status is never conveyed by colour alone (WCAG 1.4.1).
+ *
+ * Variants and their default labels:
+ * - `ok` → emerald dot, "Operational"
+ * - `warn` → amber dot, "Degraded"
+ * - `down` → rose dot, "Down"
+ *
+ * Pass `label` to override the default text while keeping the same dot
+ * affordance — useful for states outside the three defaults (e.g. `"Paused"`
+ * on a `warn` dot). An omitted, `null`, or empty-string `label` falls back to
+ * the variant's default text, so the label is always present for screen
+ * readers. The decorative dot is always `aria-hidden`.
+ */
+export function StatusDot({
+ variant,
+ label,
+}: {
+ variant: Variant;
+ /** Optional text override; falls back to the variant default when empty. */
+ label?: ReactNode;
+}) {
+ const resolvedLabel =
+ label === undefined || label === null || label === ""
+ ? labels[variant]
+ : label;
+
return (
- {labels[variant]}
+ {resolvedLabel}
);
}
diff --git a/src/components/__tests__/Header.test.tsx b/src/components/__tests__/Header.test.tsx
index 54d7962..faee338 100644
--- a/src/components/__tests__/Header.test.tsx
+++ b/src/components/__tests__/Header.test.tsx
@@ -53,14 +53,13 @@ describe("Header", () => {
mockPathname.mockReturnValue("/");
render();
- // Exactly one link has aria-current="page"
+ // Exactly one link has aria-current="page". The mobile menu panel is only
+ // rendered while open, so at rest only the desktop "Home" link is active.
const activeLinks = screen.getAllByRole("link").filter(
(link) => link.getAttribute("aria-current") === "page"
);
- // Home link is active twice (desktop + mobile)
- expect(activeLinks.length).toBe(2);
+ expect(activeLinks.length).toBe(1);
expect(activeLinks[0]).toHaveTextContent("Home");
- expect(activeLinks[1]).toHaveTextContent("Home");
});
it("marks zero links as active for an unknown route", () => {
@@ -80,14 +79,15 @@ describe("Header", () => {
// Open the secondary menu to expose secondary links
fireEvent.click(screen.getByRole("button", { name: /more/i }));
- // The active links should strictly be the desktop "Services" and the mobile "Services"
+ // With the mobile panel closed, the only current link is the desktop
+ // "Services" primary link; the opened "More" menu holds secondary links,
+ // none of which match /services.
const activeLinks = screen.getAllByRole("link", { hidden: true }).filter(
(link) => link.getAttribute("aria-current") === "page"
);
-
- expect(activeLinks.length).toBe(2);
+
+ expect(activeLinks.length).toBe(1);
expect(activeLinks[0]).toHaveTextContent("Services");
- expect(activeLinks[1]).toHaveTextContent("Services");
});
it("shows More button that opens secondary menu", () => {
diff --git a/src/components/__tests__/StatusDot.test.tsx b/src/components/__tests__/StatusDot.test.tsx
index 1f5a02a..bc94e51 100644
--- a/src/components/__tests__/StatusDot.test.tsx
+++ b/src/components/__tests__/StatusDot.test.tsx
@@ -2,48 +2,96 @@ import { render, screen } from "@testing-library/react";
import { StatusDot } from "../StatusDot";
describe("StatusDot", () => {
- it("renders 'Operational' label for ok variant", () => {
- render();
- expect(screen.getByText("Operational")).toBeInTheDocument();
- });
+ describe("default labels per variant", () => {
+ it("renders 'Operational' for the ok variant", () => {
+ render();
+ expect(screen.getByText("Operational")).toBeInTheDocument();
+ });
- it("renders 'Degraded' label for warn variant", () => {
- render();
- expect(screen.getByText("Degraded")).toBeInTheDocument();
- });
+ it("renders 'Degraded' for the warn variant", () => {
+ render();
+ expect(screen.getByText("Degraded")).toBeInTheDocument();
+ });
- it("renders 'Down' label for down variant", () => {
- render();
- expect(screen.getByText("Down")).toBeInTheDocument();
+ it("renders 'Down' for the down variant", () => {
+ render();
+ expect(screen.getByText("Down")).toBeInTheDocument();
+ });
});
- it("colour dot is aria-hidden so the label carries the meaning", () => {
- const { container } = render();
- const dot = container.querySelector("[aria-hidden='true']");
- expect(dot).toBeInTheDocument();
- });
+ describe("variant colour dot", () => {
+ it("ok dot has the emerald colour class", () => {
+ const { container } = render();
+ const dot = container.querySelector('[aria-hidden="true"]');
+ expect(dot?.className).toMatch(/bg-emerald-500/);
+ });
+
+ it("warn dot has the amber colour class", () => {
+ const { container } = render();
+ const dot = container.querySelector('[aria-hidden="true"]');
+ expect(dot?.className).toMatch(/bg-amber-500/);
+ });
+
+ it("down dot has the rose colour class", () => {
+ const { container } = render();
+ const dot = container.querySelector('[aria-hidden="true"]');
+ expect(dot?.className).toMatch(/bg-rose-500/);
+ });
- it("ok dot has emerald colour class", () => {
- const { container } = render();
- const dot = container.querySelector("[aria-hidden='true']");
- expect(dot?.className).toMatch(/emerald/);
+ it("marks the colour dot as aria-hidden so colour is not the only cue", () => {
+ const { container } = render();
+ const dot = container.querySelector(".rounded-full");
+ expect(dot).toHaveAttribute("aria-hidden", "true");
+ });
+
+ it("does not mark the visible label as aria-hidden", () => {
+ render();
+ const label = screen.getByText("Operational");
+ expect(label).not.toHaveAttribute("aria-hidden");
+ });
});
- it("warn dot has amber colour class", () => {
- const { container } = render();
- const dot = container.querySelector("[aria-hidden='true']");
- expect(dot?.className).toMatch(/amber/);
+ describe("custom label override", () => {
+ it("renders a custom string label instead of the default", () => {
+ render();
+ expect(screen.getByText("Paused")).toBeInTheDocument();
+ expect(screen.queryByText("Degraded")).not.toBeInTheDocument();
+ });
+
+ it("accepts a ReactNode label", () => {
+ render(Live now} />);
+ const node = screen.getByText("Live now");
+ expect(node.tagName).toBe("STRONG");
+ expect(screen.queryByText("Operational")).not.toBeInTheDocument();
+ });
+
+ it("keeps the variant dot colour when the label is overridden", () => {
+ const { container } = render(
+ ,
+ );
+ const dot = container.querySelector('[aria-hidden="true"]');
+ expect(dot?.className).toMatch(/bg-rose-500/);
+ });
});
- it("down dot has rose colour class", () => {
- const { container } = render();
- const dot = container.querySelector("[aria-hidden='true']");
- expect(dot?.className).toMatch(/rose/);
+ describe("empty-string label fallback", () => {
+ it("falls back to the default label for an empty string", () => {
+ render();
+ expect(screen.getByText("Operational")).toBeInTheDocument();
+ });
+
+ it("falls back to the default label for an explicit undefined", () => {
+ render();
+ expect(screen.getByText("Down")).toBeInTheDocument();
+ });
});
- it("visible label is not aria-hidden", () => {
- render();
- const label = screen.getByText("Operational");
- expect(label).not.toHaveAttribute("aria-hidden");
+ it("always renders a visible text label for screen readers", () => {
+ const { container } = render();
+ // The dot is aria-hidden; the label span must hold non-empty, non-hidden text.
+ const labelSpan = Array.from(container.querySelectorAll("span")).find(
+ (s) => !s.hasAttribute("aria-hidden") && s.textContent,
+ );
+ expect(labelSpan?.textContent).toBe("Operational");
});
});