From 8a6f01a4a00a9e6cb01e6bff3df8e95039c338b1 Mon Sep 17 00:00:00 2001 From: Hendrik Ebbers Date: Sat, 11 Jul 2026 12:54:18 +0200 Subject: [PATCH] feat: add CapabilityStatus component (0.9.0) Add a generic presentational status row for reporting runtime capability availability on admin status pages. Renders a green check when available and a red warning when not, mirroring the look of HealthStatus and reusing the Card + Tooltip primitives. An optional `hint` is surfaced as a tooltip (intended for the unavailable case, e.g. a remediation pointer). This is the ui half of the cross-repo HEIC-support-status feature; the nextjs-app-layer status page will render one row per configured capability. Part of OpenElementsLabs/nextjs-app-layer#6 Co-Authored-By: Claude Opus 4.8 (1M context) --- package.json | 2 +- .../__tests__/capability-status.test.tsx | 82 +++++++++++++++++++ src/components/capability-status.tsx | 63 ++++++++++++++ src/index.ts | 2 + 4 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 src/components/__tests__/capability-status.test.tsx create mode 100644 src/components/capability-status.tsx diff --git a/package.json b/package.json index a0a7fae..daf84cb 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/components/__tests__/capability-status.test.tsx b/src/components/__tests__/capability-status.test.tsx new file mode 100644 index 0000000..420fee0 --- /dev/null +++ b/src/components/__tests__/capability-status.test.tsx @@ -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({ui}); +} + +afterEach(() => { + cleanup(); +}); + +describe("CapabilityStatus", () => { + it("renders a green indicator and the available text when available", () => { + const { container } = renderWithTooltip( + , + ); + + 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( + , + ); + + 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( + , + ); + + 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( + , + ); + + expect(screen.queryByRole("note")).not.toBeInTheDocument(); + expect(screen.getByText("HEIC image decoding")).toBeInTheDocument(); + }); +}); diff --git a/src/components/capability-status.tsx b/src/components/capability-status.tsx new file mode 100644 index 0000000..5e4d4a6 --- /dev/null +++ b/src/components/capability-status.tsx @@ -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 = ( +
+
+ ); + + return ( + + + {hint ? ( + + +
+ {row} +
+
+ {hint} +
+ ) : ( + row + )} +
+
+ ); +} + +export type { CapabilityStatusProps }; diff --git a/src/index.ts b/src/index.ts index 0baea07..c204aac 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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";