diff --git a/frontend/packages/ui/AGENTS.md b/frontend/packages/ui/AGENTS.md index 2675489420..9f4f012b64 100644 --- a/frontend/packages/ui/AGENTS.md +++ b/frontend/packages/ui/AGENTS.md @@ -65,6 +65,7 @@ Components are organised to mirror the CLJS source tree | `ds/product/cta.cljs` | `src/lib/product/Cta.tsx` | | `ds/product/loader.cljs` | `src/lib/product/Loader.tsx` | | `ds/product/avatar.cljs` | `src/lib/product/Avatar.tsx` | +| `ds/product/panel_title.cljs` | `src/lib/product/PanelTitle.tsx` | | `ds/buttons/button.cljs` | `src/lib/buttons/Button.tsx` | | `ds/buttons/icon_button.cljs` | `src/lib/buttons/IconButton.tsx` | | `ds/utilities/swatch.cljs` | `src/lib/utilities/Swatch.tsx` | diff --git a/frontend/packages/ui/src/index.ts b/frontend/packages/ui/src/index.ts index ea2dcb6b2f..5043d4c638 100644 --- a/frontend/packages/ui/src/index.ts +++ b/frontend/packages/ui/src/index.ts @@ -19,6 +19,8 @@ export type { AvatarProfile, AvatarVariant, } from "./lib/product/Avatar"; +export { PanelTitle } from "./lib/product/PanelTitle"; +export type { PanelTitleProps } from "./lib/product/PanelTitle"; export { Icon, iconIds } from "./lib/foundations/assets/Icon"; export type { IconId, IconProps } from "./lib/foundations/assets/Icon"; export { RawSvg, rawSvgIds } from "./lib/foundations/assets/RawSvg"; diff --git a/frontend/packages/ui/src/lib/product/PanelTitle.module.scss b/frontend/packages/ui/src/lib/product/PanelTitle.module.scss new file mode 100644 index 0000000000..82cb6568d0 --- /dev/null +++ b/frontend/packages/ui/src/lib/product/PanelTitle.module.scss @@ -0,0 +1,26 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "../_ds/_sizes.scss" as *; +@use "../_ds/_borders.scss" as *; +@use "../_ds/typography.scss" as t; + +.panel-title { + display: flex; + align-items: center; + justify-content: center; + block-size: $sz-32; + border-radius: $br-8; + background-color: var(--color-background-secondary); +} + +.panel-title-text { + @include t.use-typography("headline-small"); + + flex-grow: 1; + text-align: center; + color: var(--color-foreground-primary); +} diff --git a/frontend/packages/ui/src/lib/product/PanelTitle.spec.tsx b/frontend/packages/ui/src/lib/product/PanelTitle.spec.tsx new file mode 100644 index 0000000000..379ea9c2eb --- /dev/null +++ b/frontend/packages/ui/src/lib/product/PanelTitle.spec.tsx @@ -0,0 +1,76 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +import { render } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { PanelTitle } from "./PanelTitle"; + +describe("PanelTitle", () => { + it("should render successfully", () => { + const { baseElement } = render(); + expect(baseElement).toBeTruthy(); + }); + + it("should render as a div", () => { + const { container } = render(); + expect(container.firstElementChild?.tagName.toLowerCase()).toBe("div"); + }); + + it("should apply panel-title class to root element", () => { + const { container } = render(); + expect(container.firstElementChild?.getAttribute("class")).toContain( + "panel-title", + ); + }); + + it("should render the text prop in a span", () => { + const { getByText } = render(); + const span = getByText("My Panel"); + expect(span.tagName.toLowerCase()).toBe("span"); + }); + + it("should not render a close button when onClose is not provided", () => { + const { container } = render(); + const button = container.querySelector("button"); + expect(button).toBeNull(); + }); + + it("should render a close button when onClose is provided", () => { + const { container } = render( + {}} />, + ); + const button = container.querySelector("button"); + expect(button).toBeTruthy(); + }); + + it("should call onClose when close button is clicked", () => { + const onClose = vi.fn(); + const { container } = render( + , + ); + const button = container.querySelector("button") as HTMLButtonElement; + button.click(); + expect(onClose).toHaveBeenCalledTimes(1); + }); + + it("should forward className to the root div", () => { + const { container } = render( + , + ); + expect(container.firstElementChild?.getAttribute("class")).toContain( + "custom-cls", + ); + }); + + it("should spread extra props onto the root div", () => { + const { container } = render( + , + ); + expect(container.firstElementChild?.getAttribute("data-testid")).toBe( + "panel-header", + ); + }); +}); diff --git a/frontend/packages/ui/src/lib/product/PanelTitle.stories.tsx b/frontend/packages/ui/src/lib/product/PanelTitle.stories.tsx new file mode 100644 index 0000000000..228432e15f --- /dev/null +++ b/frontend/packages/ui/src/lib/product/PanelTitle.stories.tsx @@ -0,0 +1,32 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { PanelTitle } from "./PanelTitle"; + +const meta = { + title: "Product/PanelTitle", + component: PanelTitle, + args: { + text: "Lorem ipsum", + }, + argTypes: { + text: { control: "text" }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const WithCloseButton: Story = { + args: { + onClose: () => { + console.warn("close"); + }, + }, +}; diff --git a/frontend/packages/ui/src/lib/product/PanelTitle.tsx b/frontend/packages/ui/src/lib/product/PanelTitle.tsx new file mode 100644 index 0000000000..c3fa9910af --- /dev/null +++ b/frontend/packages/ui/src/lib/product/PanelTitle.tsx @@ -0,0 +1,40 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +import { type ComponentPropsWithRef, memo } from "react"; +import clsx from "clsx"; +import { IconButton } from "../buttons/IconButton"; +import styles from "./PanelTitle.module.scss"; + +export interface PanelTitleProps extends ComponentPropsWithRef<"div"> { + /** Title text displayed in the panel header */ + text: string; + /** When provided, renders a close (×) button that calls this handler */ + onClose?: () => void; +} + +function PanelTitleInner({ + text, + onClose, + className, + ...rest +}: PanelTitleProps) { + return ( +
+ {text} + {onClose != null && ( + + )} +
+ ); +} + +export const PanelTitle = memo(PanelTitleInner);