Skip to content

Commit c06ceb1

Browse files
Merge pull request #4 from OpenElementsLabs/feat/capability-status
feat: add CapabilityStatus component (0.9.0)
2 parents 9c78c2a + 8a6f01a commit c06ceb1

4 files changed

Lines changed: 148 additions & 1 deletion

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@open-elements/ui",
3-
"version": "0.8.0",
3+
"version": "0.9.0",
44
"description": "Shared UI component library for Open Elements applications",
55
"license": "Apache-2.0",
66
"repository": {
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { describe, it, expect, afterEach } from "vitest";
2+
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
3+
import { CapabilityStatus } from "../capability-status.tsx";
4+
import { TooltipProvider } from "../tooltip.tsx";
5+
6+
function renderWithTooltip(ui: React.ReactElement) {
7+
return render(<TooltipProvider delayDuration={0}>{ui}</TooltipProvider>);
8+
}
9+
10+
afterEach(() => {
11+
cleanup();
12+
});
13+
14+
describe("CapabilityStatus", () => {
15+
it("renders a green indicator and the available text when available", () => {
16+
const { container } = renderWithTooltip(
17+
<CapabilityStatus
18+
available
19+
label="HEIC image decoding"
20+
availableText="Available"
21+
unavailableText="Not available"
22+
/>,
23+
);
24+
25+
expect(screen.getByText("HEIC image decoding")).toBeInTheDocument();
26+
expect(screen.getByText("Available")).toBeInTheDocument();
27+
28+
const icon = container.querySelector("svg.lucide-circle-check");
29+
expect(icon).toBeInTheDocument();
30+
expect(icon?.getAttribute("class")).toContain("text-oe-green");
31+
});
32+
33+
it("renders a red warning indicator and the unavailable text when not available", () => {
34+
const { container } = renderWithTooltip(
35+
<CapabilityStatus
36+
available={false}
37+
label="HEIC image decoding"
38+
availableText="Available"
39+
unavailableText="Not available"
40+
hint="HEIC uploads will be rejected with 415 — check Dockerfile"
41+
/>,
42+
);
43+
44+
expect(screen.getByText("Not available")).toBeInTheDocument();
45+
46+
const icon = container.querySelector("svg.lucide-triangle-alert");
47+
expect(icon).toBeInTheDocument();
48+
expect(icon?.getAttribute("class")).toContain("text-oe-red");
49+
});
50+
51+
it("exposes the hint as a tooltip when a hint is provided", async () => {
52+
renderWithTooltip(
53+
<CapabilityStatus
54+
available={false}
55+
label="HEIC image decoding"
56+
availableText="Available"
57+
unavailableText="Not available"
58+
hint="HEIC uploads will be rejected with 415 — check Dockerfile"
59+
/>,
60+
);
61+
62+
const trigger = screen.getByRole("note");
63+
fireEvent.focus(trigger);
64+
65+
const tooltip = await screen.findByRole("tooltip");
66+
expect(tooltip).toHaveTextContent("HEIC uploads will be rejected with 415 — check Dockerfile");
67+
});
68+
69+
it("renders without a tooltip trigger and does not error when no hint is provided", () => {
70+
renderWithTooltip(
71+
<CapabilityStatus
72+
available
73+
label="HEIC image decoding"
74+
availableText="Available"
75+
unavailableText="Not available"
76+
/>,
77+
);
78+
79+
expect(screen.queryByRole("note")).not.toBeInTheDocument();
80+
expect(screen.getByText("HEIC image decoding")).toBeInTheDocument();
81+
});
82+
});
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"use client";
2+
3+
import { CircleCheck, TriangleAlert } from "lucide-react";
4+
import { Card, CardContent } from "./card.tsx";
5+
import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip.tsx";
6+
7+
interface CapabilityStatusProps {
8+
readonly available: boolean;
9+
readonly label: string;
10+
readonly availableText: string;
11+
readonly unavailableText: string;
12+
readonly hint?: string;
13+
}
14+
15+
export function CapabilityStatus({
16+
available,
17+
label,
18+
availableText,
19+
unavailableText,
20+
hint,
21+
}: CapabilityStatusProps) {
22+
const statusText = available ? availableText : unavailableText;
23+
const Icon = available ? CircleCheck : TriangleAlert;
24+
25+
const row = (
26+
<div className="flex items-center gap-3">
27+
<Icon
28+
className={`h-5 w-5 shrink-0 ${available ? "text-oe-green" : "text-oe-red"}`}
29+
aria-hidden="true"
30+
/>
31+
<div className="flex flex-col">
32+
<span className="text-base font-medium text-oe-dark">{label}</span>
33+
<span className="text-sm text-oe-gray-mid">{statusText}</span>
34+
</div>
35+
</div>
36+
);
37+
38+
return (
39+
<Card className="border-oe-gray-light">
40+
<CardContent>
41+
{hint ? (
42+
<Tooltip>
43+
<TooltipTrigger asChild>
44+
<div
45+
tabIndex={0}
46+
role="note"
47+
aria-label={`${label}: ${statusText}. ${hint}`}
48+
className="cursor-help outline-none"
49+
>
50+
{row}
51+
</div>
52+
</TooltipTrigger>
53+
<TooltipContent>{hint}</TooltipContent>
54+
</Tooltip>
55+
) : (
56+
row
57+
)}
58+
</CardContent>
59+
</Card>
60+
);
61+
}
62+
63+
export type { CapabilityStatusProps };

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ export type { DetailFieldProps, DetailFieldTranslations } from "./components/det
139139
export { LanguageSwitch } from "./components/language-switch.tsx";
140140
export { HealthStatus } from "./components/health-status.tsx";
141141
export type { HealthStatusProps, HealthStatusTranslations } from "./components/health-status.tsx";
142+
export { CapabilityStatus } from "./components/capability-status.tsx";
143+
export type { CapabilityStatusProps } from "./components/capability-status.tsx";
142144
export { TagChips } from "./components/tag-chips.tsx";
143145
export type { TagChipsProps } from "./components/tag-chips.tsx";
144146
export { TagForm } from "./components/tag-form.tsx";

0 commit comments

Comments
 (0)