Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@open-elements/ui",
"version": "0.8.0",
"version": "0.9.0",
"description": "Shared UI component library for Open Elements applications",
"license": "Apache-2.0",
"repository": {
Expand Down
82 changes: 82 additions & 0 deletions src/components/__tests__/capability-status.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { describe, it, expect, afterEach } from "vitest";
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import { CapabilityStatus } from "../capability-status.tsx";
import { TooltipProvider } from "../tooltip.tsx";

function renderWithTooltip(ui: React.ReactElement) {
return render(<TooltipProvider delayDuration={0}>{ui}</TooltipProvider>);
}

afterEach(() => {
cleanup();
});

describe("CapabilityStatus", () => {
it("renders a green indicator and the available text when available", () => {
const { container } = renderWithTooltip(
<CapabilityStatus
available
label="HEIC image decoding"
availableText="Available"
unavailableText="Not available"
/>,
);

expect(screen.getByText("HEIC image decoding")).toBeInTheDocument();
expect(screen.getByText("Available")).toBeInTheDocument();

const icon = container.querySelector("svg.lucide-circle-check");
expect(icon).toBeInTheDocument();
expect(icon?.getAttribute("class")).toContain("text-oe-green");
});

it("renders a red warning indicator and the unavailable text when not available", () => {
const { container } = renderWithTooltip(
<CapabilityStatus
available={false}
label="HEIC image decoding"
availableText="Available"
unavailableText="Not available"
hint="HEIC uploads will be rejected with 415 — check Dockerfile"
/>,
);

expect(screen.getByText("Not available")).toBeInTheDocument();

const icon = container.querySelector("svg.lucide-triangle-alert");
expect(icon).toBeInTheDocument();
expect(icon?.getAttribute("class")).toContain("text-oe-red");
});

it("exposes the hint as a tooltip when a hint is provided", async () => {
renderWithTooltip(
<CapabilityStatus
available={false}
label="HEIC image decoding"
availableText="Available"
unavailableText="Not available"
hint="HEIC uploads will be rejected with 415 — check Dockerfile"
/>,
);

const trigger = screen.getByRole("note");
fireEvent.focus(trigger);

const tooltip = await screen.findByRole("tooltip");
expect(tooltip).toHaveTextContent("HEIC uploads will be rejected with 415 — check Dockerfile");
});

it("renders without a tooltip trigger and does not error when no hint is provided", () => {
renderWithTooltip(
<CapabilityStatus
available
label="HEIC image decoding"
availableText="Available"
unavailableText="Not available"
/>,
);

expect(screen.queryByRole("note")).not.toBeInTheDocument();
expect(screen.getByText("HEIC image decoding")).toBeInTheDocument();
});
});
63 changes: 63 additions & 0 deletions src/components/capability-status.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"use client";

import { CircleCheck, TriangleAlert } from "lucide-react";
import { Card, CardContent } from "./card.tsx";
import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip.tsx";

interface CapabilityStatusProps {
readonly available: boolean;
readonly label: string;
readonly availableText: string;
readonly unavailableText: string;
readonly hint?: string;
}

export function CapabilityStatus({
available,
label,
availableText,
unavailableText,
hint,
}: CapabilityStatusProps) {
const statusText = available ? availableText : unavailableText;
const Icon = available ? CircleCheck : TriangleAlert;

const row = (
<div className="flex items-center gap-3">
<Icon
className={`h-5 w-5 shrink-0 ${available ? "text-oe-green" : "text-oe-red"}`}
aria-hidden="true"
/>
<div className="flex flex-col">
<span className="text-base font-medium text-oe-dark">{label}</span>
<span className="text-sm text-oe-gray-mid">{statusText}</span>
</div>
</div>
);

return (
<Card className="border-oe-gray-light">
<CardContent>
{hint ? (
<Tooltip>
<TooltipTrigger asChild>
<div
tabIndex={0}
role="note"
aria-label={`${label}: ${statusText}. ${hint}`}
className="cursor-help outline-none"
>
{row}
</div>
</TooltipTrigger>
<TooltipContent>{hint}</TooltipContent>
</Tooltip>
) : (
row
)}
</CardContent>
</Card>
);
}

export type { CapabilityStatusProps };
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ export type { DetailFieldProps, DetailFieldTranslations } from "./components/det
export { LanguageSwitch } from "./components/language-switch.tsx";
export { HealthStatus } from "./components/health-status.tsx";
export type { HealthStatusProps, HealthStatusTranslations } from "./components/health-status.tsx";
export { CapabilityStatus } from "./components/capability-status.tsx";
export type { CapabilityStatusProps } from "./components/capability-status.tsx";
export { TagChips } from "./components/tag-chips.tsx";
export type { TagChipsProps } from "./components/tag-chips.tsx";
export { TagForm } from "./components/tag-form.tsx";
Expand Down
Loading