fix: correct Biome formatting and restore non-null assertions in tests

- Remove root biome.json (conflicted with packages/config/biome.json)
- Format all files with correct config (spaces, not tabs)
- Restore non-null assertions (ref!) in test files where refs are guaranteed
- Biome's optional chaining conversion broke TypeScript inference in tests

Fixes type-check and format-check CI failures.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Ajax Davis 2025-11-26 19:04:34 +10:00
parent e904e8d319
commit 990ba6f050
31 changed files with 484 additions and 152 deletions

View file

@ -1,29 +0,0 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"ignoreUnknown": false,
"ignore": [".next", "dist", ".turbo", "node_modules", "storybook-static"]
},
"formatter": {
"enabled": true,
"formatWithErrors": false,
"indentStyle": "tab",
"indentWidth": 2,
"lineWidth": 100,
"lineEnding": "lf"
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"suspicious": {
"noExplicitAny": "warn"
}
}
}
}

View file

@ -250,7 +250,7 @@ describe("Badge", () => {
</Badge>,
);
expect(ref).toBeInstanceOf(HTMLDivElement);
expect(ref?.tagName).toBe("DIV");
expect(ref!.tagName).toBe("DIV");
});
});
@ -297,7 +297,11 @@ describe("Badge", () => {
it("works correctly with outline variant and custom className", () => {
render(
<Badge variant="outline" className="hover:bg-accent" data-testid="badge">
<Badge
variant="outline"
className="hover:bg-accent"
data-testid="badge"
>
Outlined
</Badge>,
);
@ -351,7 +355,12 @@ describe("Badge", () => {
describe("Semantic Usage", () => {
it("can be used as status indicator with role", () => {
render(
<Badge variant="success" role="status" aria-label="Online status" data-testid="badge">
<Badge
variant="success"
role="status"
aria-label="Online status"
data-testid="badge"
>
Online
</Badge>,
);
@ -363,7 +372,12 @@ describe("Badge", () => {
it("can be used as notification count", () => {
render(
<Badge variant="error" size="sm" aria-label="3 unread messages" data-testid="badge">
<Badge
variant="error"
size="sm"
aria-label="3 unread messages"
data-testid="badge"
>
3
</Badge>,
);

View file

@ -8,7 +8,14 @@ export interface BadgeProps extends HTMLAttributes<HTMLDivElement> {
* Visual variant of the badge
* @default 'default'
*/
variant?: "default" | "secondary" | "outline" | "success" | "error" | "warning" | "info";
variant?:
| "default"
| "secondary"
| "outline"
| "success"
| "error"
| "warning"
| "info";
/**
* Size of the badge

View file

@ -19,17 +19,33 @@ export const badgeVariants = createVariants({
variants: {
variant: {
default: ["bg-primary text-primary-foreground", "border border-primary"].join(" "),
default: [
"bg-primary text-primary-foreground",
"border border-primary",
].join(" "),
secondary: ["bg-secondary text-secondary-foreground", "border border-secondary"].join(" "),
secondary: [
"bg-secondary text-secondary-foreground",
"border border-secondary",
].join(" "),
outline: ["bg-transparent text-foreground", "border border-border"].join(" "),
outline: ["bg-transparent text-foreground", "border border-border"].join(
" ",
),
success: ["bg-success text-success-foreground", "border border-success"].join(" "),
success: [
"bg-success text-success-foreground",
"border border-success",
].join(" "),
error: ["bg-error text-error-foreground", "border border-error"].join(" "),
error: ["bg-error text-error-foreground", "border border-error"].join(
" ",
),
warning: ["bg-warning text-warning-foreground", "border border-warning"].join(" "),
warning: [
"bg-warning text-warning-foreground",
"border border-warning",
].join(" "),
info: ["bg-info text-info-foreground", "border border-info"].join(" "),
},

View file

@ -119,7 +119,9 @@ describe("Button", () => {
});
it("applies icon size classes", () => {
render(<Button size="icon" data-testid="button" aria-label="Icon button" />);
render(
<Button size="icon" data-testid="button" aria-label="Icon button" />,
);
const button = screen.getByTestId("button");
expect(button.className).toContain("h-10");
expect(button.className).toContain("w-10");
@ -239,14 +241,19 @@ describe("Button", () => {
</Button>,
);
expect(ref).toBeInstanceOf(HTMLButtonElement);
expect(ref?.tagName).toBe("BUTTON");
expect(ref!.tagName).toBe("BUTTON");
});
});
describe("HTML Attributes", () => {
it("passes through HTML button attributes", () => {
render(
<Button type="submit" name="test-button" value="test-value" data-testid="button">
<Button
type="submit"
name="test-button"
value="test-value"
data-testid="button"
>
Submit
</Button>,
);

View file

@ -65,9 +65,11 @@ export const buttonVariants = createVariants({
"active:bg-accent/80",
].join(" "),
link: ["text-primary underline-offset-4", "hover:underline", "active:text-primary/80"].join(
" ",
),
link: [
"text-primary underline-offset-4",
"hover:underline",
"active:text-primary/80",
].join(" "),
},
size: {

View file

@ -1,6 +1,13 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./Card";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "./Card";
describe("Card", () => {
describe("Rendering", () => {
@ -16,7 +23,9 @@ describe("Card", () => {
<Card data-testid="card">
<CardHeader data-testid="header">
<CardTitle data-testid="title">Title</CardTitle>
<CardDescription data-testid="description">Description</CardDescription>
<CardDescription data-testid="description">
Description
</CardDescription>
</CardHeader>
<CardContent data-testid="content">Content</CardContent>
<CardFooter data-testid="footer">Footer</CardFooter>
@ -227,7 +236,9 @@ describe("Card", () => {
render(
<Card>
<CardHeader>
<CardDescription data-testid="description">Description text</CardDescription>
<CardDescription data-testid="description">
Description text
</CardDescription>
</CardHeader>
</Card>,
);
@ -239,7 +250,9 @@ describe("Card", () => {
render(
<Card>
<CardHeader>
<CardDescription data-testid="description">Description</CardDescription>
<CardDescription data-testid="description">
Description
</CardDescription>
</CardHeader>
</Card>,
);
@ -404,7 +417,7 @@ describe("Card", () => {
</Card>,
);
expect(ref).toBeInstanceOf(HTMLDivElement);
expect(ref?.tagName).toBe("DIV");
expect(ref!.tagName).toBe("DIV");
});
it("forwards ref to CardHeader element", () => {
@ -439,7 +452,7 @@ describe("Card", () => {
</Card>,
);
expect(ref).toBeInstanceOf(HTMLHeadingElement);
expect(ref?.tagName).toBe("H3");
expect(ref!.tagName).toBe("H3");
});
it("forwards ref to CardDescription element", () => {
@ -561,7 +574,9 @@ describe("Card", () => {
render(
<Card>
<CardHeader>
<CardDescription data-testid="description">Description</CardDescription>
<CardDescription data-testid="description">
Description
</CardDescription>
</CardHeader>
</Card>,
);
@ -591,7 +606,9 @@ describe("Card", () => {
<CardTitle>Card Title</CardTitle>
<CardDescription>This is a card description</CardDescription>
</CardHeader>
<CardContent data-testid="content">Card content goes here</CardContent>
<CardContent data-testid="content">
Card content goes here
</CardContent>
<CardFooter data-testid="footer">Footer actions</CardFooter>
</Card>,
);
@ -599,7 +616,9 @@ describe("Card", () => {
const card = screen.getByTestId("card");
expect(card).toBeInTheDocument();
expect(screen.getByText("Card Title")).toBeInTheDocument();
expect(screen.getByText("This is a card description")).toBeInTheDocument();
expect(
screen.getByText("This is a card description"),
).toBeInTheDocument();
expect(screen.getByText("Card content goes here")).toBeInTheDocument();
expect(screen.getByText("Footer actions")).toBeInTheDocument();
});

View file

@ -93,7 +93,13 @@ CardHeader.displayName = "CardHeader";
export const CardTitle = forwardRef<HTMLHeadingElement, CardTitleProps>(
({ className, as = "h3", ...props }, ref) => {
const Component = as;
return <Component ref={ref} className={cn(cardTitleVariants(), className)} {...props} />;
return (
<Component
ref={ref}
className={cn(cardTitleVariants(), className)}
{...props}
/>
);
},
);
@ -104,11 +110,18 @@ CardTitle.displayName = "CardTitle";
*
* Description text for a card, typically placed below the title.
*/
export const CardDescription = forwardRef<HTMLParagraphElement, CardDescriptionProps>(
({ className, ...props }, ref) => {
return <p ref={ref} className={cn(cardDescriptionVariants(), className)} {...props} />;
},
);
export const CardDescription = forwardRef<
HTMLParagraphElement,
CardDescriptionProps
>(({ className, ...props }, ref) => {
return (
<p
ref={ref}
className={cn(cardDescriptionVariants(), className)}
{...props}
/>
);
});
CardDescription.displayName = "CardDescription";

View file

@ -28,7 +28,10 @@ export const cardVariants = createVariants({
"shadow-md",
].join(" "),
outline: ["border-2 border-dotted border-border", "bg-transparent text-foreground"].join(" "),
outline: [
"border-2 border-dotted border-border",
"bg-transparent text-foreground",
].join(" "),
blueprint: [
"border border-dotted border-border",
@ -82,7 +85,10 @@ export const cardHeaderVariants = createVariants({
* CardTitle variant definitions
*/
export const cardTitleVariants = createVariants({
base: ["text-2xl font-semibold leading-none tracking-tight", "text-foreground"].join(" "),
base: [
"text-2xl font-semibold leading-none tracking-tight",
"text-foreground",
].join(" "),
variants: {},

View file

@ -50,7 +50,13 @@ describe("CodeBlock", () => {
describe("Language", () => {
it("sets data-language attribute", () => {
render(<CodeBlock code="const x = 5" language="javascript" data-testid="codeblock" />);
render(
<CodeBlock
code="const x = 5"
language="javascript"
data-testid="codeblock"
/>,
);
const codeblock = screen.getByTestId("codeblock");
const code = codeblock.querySelector("code");
expect(code).toHaveAttribute("data-language", "javascript");
@ -71,7 +77,9 @@ describe("CodeBlock", () => {
let code = codeblock.querySelector("code");
expect(code).toHaveAttribute("data-language", "python");
rerender(<CodeBlock code="code" language="bash" data-testid="codeblock" />);
rerender(
<CodeBlock code="code" language="bash" data-testid="codeblock" />,
);
codeblock = screen.getByTestId("codeblock");
code = codeblock.querySelector("code");
expect(code).toHaveAttribute("data-language", "bash");
@ -215,8 +223,12 @@ describe("CodeBlock", () => {
});
it("handles clipboard API errors gracefully", async () => {
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
mockClipboard.writeText.mockRejectedValueOnce(new Error("Clipboard not available"));
const consoleErrorSpy = vi
.spyOn(console, "error")
.mockImplementation(() => {});
mockClipboard.writeText.mockRejectedValueOnce(
new Error("Clipboard not available"),
);
try {
render(<CodeBlock code="test code" />);
@ -237,13 +249,17 @@ describe("CodeBlock", () => {
describe("HTML Attributes", () => {
it("passes through id attribute", () => {
render(<CodeBlock code="code" id="codeblock-id" data-testid="codeblock" />);
render(
<CodeBlock code="code" id="codeblock-id" data-testid="codeblock" />,
);
const codeblock = screen.getByTestId("codeblock");
expect(codeblock).toHaveAttribute("id", "codeblock-id");
});
it("passes through data attributes", () => {
render(<CodeBlock code="code" data-custom="test" data-testid="codeblock" />);
render(
<CodeBlock code="code" data-custom="test" data-testid="codeblock" />,
);
const codeblock = screen.getByTestId("codeblock");
expect(codeblock).toHaveAttribute("data-custom", "test");
});
@ -251,7 +267,13 @@ describe("CodeBlock", () => {
describe("Custom className", () => {
it("merges custom className with variant classes", () => {
render(<CodeBlock code="code" className="custom-class" data-testid="codeblock" />);
render(
<CodeBlock
code="code"
className="custom-class"
data-testid="codeblock"
/>,
);
const codeblock = screen.getByTestId("codeblock");
expect(codeblock.className).toContain("custom-class");
expect(codeblock.className).toContain("bg-background");
@ -271,7 +293,7 @@ describe("CodeBlock", () => {
/>,
);
expect(ref).toBeInstanceOf(HTMLDivElement);
expect(ref?.querySelector("code")).toBeInTheDocument();
expect(ref!.querySelector("code")).toBeInTheDocument();
});
});

View file

@ -32,7 +32,17 @@ import {
* ```
*/
export const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(
({ className, code, language = "text", size = "md", showCopy = true, ...props }, ref) => {
(
{
className,
code,
language = "text",
size = "md",
showCopy = true,
...props
},
ref,
) => {
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
@ -47,7 +57,11 @@ export const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(
};
return (
<div ref={ref} className={cn(codeBlockContainerVariants(), className)} {...props}>
<div
ref={ref}
className={cn(codeBlockContainerVariants(), className)}
{...props}
>
<code
className={codeBlockCodeVariants({
size,

View file

@ -3,7 +3,8 @@ import type { HTMLAttributes } from "react";
/**
* CodeBlock component props
*/
export interface CodeBlockProps extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
export interface CodeBlockProps
extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
/**
* Code content to display
*/

View file

@ -223,7 +223,7 @@ describe("Container", () => {
</Container>,
);
expect(ref).toBeInstanceOf(HTMLDivElement);
expect(ref?.tagName).toBe("DIV");
expect(ref!.tagName).toBe("DIV");
});
});

View file

@ -281,7 +281,13 @@ describe("GridContainer", () => {
describe("Compound Scenarios", () => {
it("works with multiple variants combined", () => {
render(
<GridContainer columns={4} gap="lg" align="center" justify="center" data-testid="grid">
<GridContainer
columns={4}
gap="lg"
align="center"
justify="center"
data-testid="grid"
>
Content
</GridContainer>,
);
@ -294,7 +300,12 @@ describe("GridContainer", () => {
it("works with fixed columns and custom className", () => {
render(
<GridContainer columns={3} responsive="fixed" className="my-custom-grid" data-testid="grid">
<GridContainer
columns={3}
responsive="fixed"
className="my-custom-grid"
data-testid="grid"
>
Content
</GridContainer>,
);

View file

@ -77,7 +77,11 @@ describe("Header", () => {
});
it("renders ReactNode as actions", () => {
render(<Header actions={<div data-testid="custom-actions">Custom Actions</div>} />);
render(
<Header
actions={<div data-testid="custom-actions">Custom Actions</div>}
/>,
);
expect(screen.getByTestId("custom-actions")).toBeInTheDocument();
});
});
@ -251,7 +255,7 @@ describe("Header", () => {
/>,
);
expect(ref).toBeInstanceOf(HTMLElement);
expect(ref?.tagName).toBe("HEADER");
expect(ref!.tagName).toBe("HEADER");
});
});
@ -271,7 +275,15 @@ describe("Header", () => {
describe("Compound Scenarios", () => {
it("works correctly with large size and sticky", () => {
render(<Header title="App" actions="Sign In" size="lg" sticky={true} data-testid="header" />);
render(
<Header
title="App"
actions="Sign In"
size="lg"
sticky={true}
data-testid="header"
/>,
);
const header = screen.getByTestId("header");
expect(header.className).toContain("h-20");
expect(header.className).toContain("px-8");

View file

@ -1,7 +1,11 @@
import { cn } from "@tpmjs/utils/cn";
import { forwardRef } from "react";
import type { HeaderProps } from "./types";
import { headerActionsVariants, headerTitleVariants, headerVariants } from "./variants";
import {
headerActionsVariants,
headerTitleVariants,
headerVariants,
} from "./variants";
/**
* Header component
@ -27,7 +31,18 @@ import { headerActionsVariants, headerTitleVariants, headerVariants } from "./va
* ```
*/
export const Header = forwardRef<HTMLElement, HeaderProps>(
({ className, title, actions, size = "md", sticky = false, children, ...props }, ref) => {
(
{
className,
title,
actions,
size = "md",
sticky = false,
children,
...props
},
ref,
) => {
return (
<header
className={cn(
@ -51,7 +66,10 @@ export const Header = forwardRef<HTMLElement, HeaderProps>(
</div>
)}
{children && (
<div className="flex-1 flex items-center justify-center" data-testid="header-children">
<div
className="flex-1 flex items-center justify-center"
data-testid="header-children"
>
{children}
</div>
)}

View file

@ -3,7 +3,8 @@ import type { HTMLAttributes, ReactNode } from "react";
/**
* Header component props
*/
export interface HeaderProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
export interface HeaderProps
extends Omit<HTMLAttributes<HTMLElement>, "title"> {
/**
* Title/logo content for the left side
*/

View file

@ -119,7 +119,10 @@ describe("Icon", () => {
render(<Icon icon="chevronDown" data-testid="icon" />);
const icon = screen.getByTestId("icon");
const path = icon.querySelector("path");
expect(path).toHaveAttribute("d", "M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z");
expect(path).toHaveAttribute(
"d",
"M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z",
);
});
});
@ -145,7 +148,14 @@ describe("Icon", () => {
});
it("passes through aria-label", () => {
render(<Icon icon="check" aria-label="Success" aria-hidden={false} data-testid="icon" />);
render(
<Icon
icon="check"
aria-label="Success"
aria-hidden={false}
data-testid="icon"
/>,
);
const icon = screen.getByTestId("icon");
expect(icon).toHaveAttribute("aria-label", "Success");
});
@ -169,7 +179,7 @@ describe("Icon", () => {
/>,
);
expect(ref).toBeInstanceOf(SVGSVGElement);
expect(ref?.tagName).toBe("svg");
expect(ref!.tagName).toBe("svg");
});
});

View file

@ -4,7 +4,8 @@ import type { IconName } from "./icons";
/**
* Icon component props
*/
export interface IconProps extends Omit<SVGAttributes<SVGSVGElement>, "children"> {
export interface IconProps
extends Omit<SVGAttributes<SVGSVGElement>, "children"> {
/**
* Icon to display
*/

View file

@ -196,7 +196,9 @@ describe("Input", () => {
describe("Value and onChange", () => {
it("renders with initial value", () => {
render(<Input value="test value" onChange={() => {}} data-testid="input" />);
render(
<Input value="test value" onChange={() => {}} data-testid="input" />,
);
const input = screen.getByTestId("input") as HTMLInputElement;
expect(input.value).toBe("test value");
});
@ -315,7 +317,7 @@ describe("Input", () => {
/>,
);
expect(ref).toBeInstanceOf(HTMLInputElement);
expect(ref?.tagName).toBe("INPUT");
expect(ref!.tagName).toBe("INPUT");
});
it("can focus input through ref", () => {
@ -328,7 +330,7 @@ describe("Input", () => {
data-testid="input"
/>,
);
ref?.focus();
ref!.focus();
expect(screen.getByTestId("input")).toHaveFocus();
});
});

View file

@ -3,7 +3,8 @@ import type { InputHTMLAttributes } from "react";
/**
* Input component props
*/
export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "size"> {
export interface InputProps
extends Omit<InputHTMLAttributes<HTMLInputElement>, "size"> {
/**
* Visual state of the input
* @default 'default'

View file

@ -208,7 +208,7 @@ describe("Label", () => {
</Label>,
);
expect(ref).toBeInstanceOf(HTMLLabelElement);
expect(ref?.tagName).toBe("LABEL");
expect(ref!.tagName).toBe("LABEL");
});
});

View file

@ -18,7 +18,17 @@ import { labelVariants } from "./variants";
* ```
*/
export const Label = forwardRef<HTMLLabelElement, LabelProps>(
({ className, size = "md", required = false, disabled = false, children, ...props }, ref) => {
(
{
className,
size = "md",
required = false,
disabled = false,
children,
...props
},
ref,
) => {
return (
// biome-ignore lint/a11y/noLabelWithoutControl: This is a generic label component that can be used with htmlFor or wrap inputs
<label

View file

@ -103,28 +103,36 @@ describe("ProgressBar", () => {
describe("Color Variants", () => {
it("applies primary variant", () => {
render(<ProgressBar value={50} variant="primary" data-testid="progress" />);
render(
<ProgressBar value={50} variant="primary" data-testid="progress" />,
);
const progress = screen.getByTestId("progress");
const fill = progress.querySelector("div");
expect(fill?.className).toContain("bg-primary");
});
it("applies success variant", () => {
render(<ProgressBar value={50} variant="success" data-testid="progress" />);
render(
<ProgressBar value={50} variant="success" data-testid="progress" />,
);
const progress = screen.getByTestId("progress");
const fill = progress.querySelector("div");
expect(fill?.className).toContain("bg-success");
});
it("applies warning variant", () => {
render(<ProgressBar value={50} variant="warning" data-testid="progress" />);
render(
<ProgressBar value={50} variant="warning" data-testid="progress" />,
);
const progress = screen.getByTestId("progress");
const fill = progress.querySelector("div");
expect(fill?.className).toContain("bg-warning");
});
it("applies danger variant", () => {
render(<ProgressBar value={50} variant="danger" data-testid="progress" />);
render(
<ProgressBar value={50} variant="danger" data-testid="progress" />,
);
const progress = screen.getByTestId("progress");
const fill = progress.querySelector("div");
expect(fill?.className).toContain("bg-error");
@ -175,13 +183,17 @@ describe("ProgressBar", () => {
describe("HTML Attributes", () => {
it("passes through id attribute", () => {
render(<ProgressBar value={50} id="progress-id" data-testid="progress" />);
render(
<ProgressBar value={50} id="progress-id" data-testid="progress" />,
);
const progress = screen.getByTestId("progress");
expect(progress).toHaveAttribute("id", "progress-id");
});
it("passes through data attributes", () => {
render(<ProgressBar value={50} data-custom="test" data-testid="progress" />);
render(
<ProgressBar value={50} data-custom="test" data-testid="progress" />,
);
const progress = screen.getByTestId("progress");
expect(progress).toHaveAttribute("data-custom", "test");
});
@ -189,7 +201,13 @@ describe("ProgressBar", () => {
describe("Custom className", () => {
it("merges custom className with variant classes", () => {
render(<ProgressBar value={50} className="custom-class" data-testid="progress" />);
render(
<ProgressBar
value={50}
className="custom-class"
data-testid="progress"
/>,
);
const progress = screen.getByTestId("progress");
expect(progress.className).toContain("custom-class");
expect(progress.className).toContain("bg-surface");
@ -235,7 +253,14 @@ describe("ProgressBar", () => {
describe("Compound Scenarios", () => {
it("works correctly with large size and success variant", () => {
render(<ProgressBar value={80} size="lg" variant="success" data-testid="progress" />);
render(
<ProgressBar
value={80}
size="lg"
variant="success"
data-testid="progress"
/>,
);
const progress = screen.getByTestId("progress");
expect(progress.className).toContain("h-3");
const fill = progress.querySelector("div");

View file

@ -26,7 +26,17 @@ import { progressBarFillVariants, progressBarTrackVariants } from "./variants";
* ```
*/
export const ProgressBar = forwardRef<HTMLDivElement, ProgressBarProps>(
({ className, value, size = "md", variant = "primary", showLabel = false, ...props }, ref) => {
(
{
className,
value,
size = "md",
variant = "primary",
showLabel = false,
...props
},
ref,
) => {
// Clamp value between 0 and 100
const clampedValue = Math.min(Math.max(value, 0), 100);

View file

@ -3,7 +3,8 @@ import type { HTMLAttributes } from "react";
/**
* ProgressBar component props
*/
export interface ProgressBarProps extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
export interface ProgressBarProps
extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
/**
* Progress value (0-100)
*/

View file

@ -292,7 +292,7 @@ describe("Section", () => {
</Section>,
);
expect(ref).toBeInstanceOf(HTMLElement);
expect(ref?.tagName).toBe("SECTION");
expect(ref!.tagName).toBe("SECTION");
});
});
@ -308,7 +308,13 @@ describe("Section", () => {
describe("Compound Scenarios", () => {
it("works with multiple variants combined", () => {
render(
<Section spacing="lg" background="blueprint" container="xl" centered data-testid="section">
<Section
spacing="lg"
background="blueprint"
container="xl"
centered
data-testid="section"
>
Content
</Section>,
);

View file

@ -14,7 +14,12 @@ describe("Tabs", () => {
it("renders tabs container", () => {
const handleChange = vi.fn();
render(
<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} data-testid="tabs" />,
<Tabs
tabs={mockTabs}
activeTab="all"
onTabChange={handleChange}
data-testid="tabs"
/>,
);
const tabs = screen.getByTestId("tabs");
expect(tabs).toBeInTheDocument();
@ -24,7 +29,12 @@ describe("Tabs", () => {
it("has tablist role", () => {
const handleChange = vi.fn();
render(
<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} data-testid="tabs" />,
<Tabs
tabs={mockTabs}
activeTab="all"
onTabChange={handleChange}
data-testid="tabs"
/>,
);
const tabs = screen.getByTestId("tabs");
expect(tabs).toHaveAttribute("role", "tablist");
@ -32,7 +42,9 @@ describe("Tabs", () => {
it("renders all tabs", () => {
const handleChange = vi.fn();
render(<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />);
render(
<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />,
);
expect(screen.getByTestId("tab-all")).toBeInTheDocument();
expect(screen.getByTestId("tab-featured")).toBeInTheDocument();
expect(screen.getByTestId("tab-recent")).toBeInTheDocument();
@ -40,7 +52,9 @@ describe("Tabs", () => {
it("renders tab labels", () => {
const handleChange = vi.fn();
render(<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />);
render(
<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />,
);
expect(screen.getByText("All Tools")).toBeInTheDocument();
expect(screen.getByText("Featured")).toBeInTheDocument();
expect(screen.getByText("Recent")).toBeInTheDocument();
@ -48,7 +62,9 @@ describe("Tabs", () => {
it("renders tabs as buttons", () => {
const handleChange = vi.fn();
render(<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />);
render(
<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />,
);
const tab = screen.getByTestId("tab-all");
expect(tab.tagName).toBe("BUTTON");
expect(tab).toHaveAttribute("type", "button");
@ -58,15 +74,32 @@ describe("Tabs", () => {
describe("Active State", () => {
it("marks active tab with aria-selected", () => {
const handleChange = vi.fn();
render(<Tabs tabs={mockTabs} activeTab="featured" onTabChange={handleChange} />);
expect(screen.getByTestId("tab-featured")).toHaveAttribute("aria-selected", "true");
expect(screen.getByTestId("tab-all")).toHaveAttribute("aria-selected", "false");
expect(screen.getByTestId("tab-recent")).toHaveAttribute("aria-selected", "false");
render(
<Tabs
tabs={mockTabs}
activeTab="featured"
onTabChange={handleChange}
/>,
);
expect(screen.getByTestId("tab-featured")).toHaveAttribute(
"aria-selected",
"true",
);
expect(screen.getByTestId("tab-all")).toHaveAttribute(
"aria-selected",
"false",
);
expect(screen.getByTestId("tab-recent")).toHaveAttribute(
"aria-selected",
"false",
);
});
it("applies active styling to active tab", () => {
const handleChange = vi.fn();
render(<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />);
render(
<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />,
);
const activeTab = screen.getByTestId("tab-all");
expect(activeTab.className).toContain("text-foreground");
expect(activeTab.className).toContain("border-primary");
@ -74,7 +107,9 @@ describe("Tabs", () => {
it("applies inactive styling to inactive tabs", () => {
const handleChange = vi.fn();
render(<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />);
render(
<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />,
);
const inactiveTab = screen.getByTestId("tab-featured");
expect(inactiveTab.className).toContain("text-foreground-secondary");
expect(inactiveTab.className).toContain("border-transparent");
@ -84,14 +119,18 @@ describe("Tabs", () => {
describe("Click Handling", () => {
it("calls onTabChange when tab is clicked", () => {
const handleChange = vi.fn();
render(<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />);
render(
<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />,
);
screen.getByTestId("tab-featured").click();
expect(handleChange).toHaveBeenCalledWith("featured");
});
it("calls onTabChange with correct tab ID", () => {
const handleChange = vi.fn();
render(<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />);
render(
<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />,
);
screen.getByTestId("tab-recent").click();
expect(handleChange).toHaveBeenCalledWith("recent");
expect(handleChange).toHaveBeenCalledTimes(1);
@ -99,7 +138,9 @@ describe("Tabs", () => {
it("allows clicking already active tab", () => {
const handleChange = vi.fn();
render(<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />);
render(
<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />,
);
screen.getByTestId("tab-all").click();
expect(handleChange).toHaveBeenCalledWith("all");
});
@ -108,14 +149,18 @@ describe("Tabs", () => {
describe("Count Badges", () => {
it("renders count badge when count is provided", () => {
const handleChange = vi.fn();
render(<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />);
render(
<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />,
);
expect(screen.getByText("1234")).toBeInTheDocument();
expect(screen.getByText("42")).toBeInTheDocument();
});
it("does not render count badge when count is undefined", () => {
const handleChange = vi.fn();
render(<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />);
render(
<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />,
);
const recentTab = screen.getByTestId("tab-recent");
const badge = recentTab.querySelector("span[aria-label]");
expect(badge).not.toBeInTheDocument();
@ -123,21 +168,27 @@ describe("Tabs", () => {
it("count badge has aria-label", () => {
const handleChange = vi.fn();
render(<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />);
render(
<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />,
);
const badge = screen.getByText("1234");
expect(badge).toHaveAttribute("aria-label", "1234 items");
});
it("applies active styling to count badge on active tab", () => {
const handleChange = vi.fn();
render(<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />);
render(
<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />,
);
const activeBadge = screen.getByText("1234");
expect(activeBadge.className).toContain("text-foreground");
});
it("applies inactive styling to count badge on inactive tab", () => {
const handleChange = vi.fn();
render(<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />);
render(
<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />,
);
const inactiveBadge = screen.getByText("42");
expect(inactiveBadge.className).toContain("text-foreground-tertiary");
});
@ -145,7 +196,13 @@ describe("Tabs", () => {
it("handles zero count", () => {
const handleChange = vi.fn();
const tabsWithZero: Tab[] = [{ id: "empty", label: "Empty", count: 0 }];
render(<Tabs tabs={tabsWithZero} activeTab="empty" onTabChange={handleChange} />);
render(
<Tabs
tabs={tabsWithZero}
activeTab="empty"
onTabChange={handleChange}
/>,
);
expect(screen.getByText("0")).toBeInTheDocument();
});
});
@ -153,7 +210,14 @@ describe("Tabs", () => {
describe("Size Variants", () => {
it("applies small size", () => {
const handleChange = vi.fn();
render(<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} size="sm" />);
render(
<Tabs
tabs={mockTabs}
activeTab="all"
onTabChange={handleChange}
size="sm"
/>,
);
const tab = screen.getByTestId("tab-all");
expect(tab.className).toContain("text-sm");
expect(tab.className).toContain("px-3");
@ -162,7 +226,14 @@ describe("Tabs", () => {
it("applies medium size", () => {
const handleChange = vi.fn();
render(<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} size="md" />);
render(
<Tabs
tabs={mockTabs}
activeTab="all"
onTabChange={handleChange}
size="md"
/>,
);
const tab = screen.getByTestId("tab-all");
expect(tab.className).toContain("text-base");
expect(tab.className).toContain("px-4");
@ -171,7 +242,14 @@ describe("Tabs", () => {
it("applies large size", () => {
const handleChange = vi.fn();
render(<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} size="lg" />);
render(
<Tabs
tabs={mockTabs}
activeTab="all"
onTabChange={handleChange}
size="lg"
/>,
);
const tab = screen.getByTestId("tab-all");
expect(tab.className).toContain("text-lg");
expect(tab.className).toContain("px-6");
@ -180,7 +258,9 @@ describe("Tabs", () => {
it("uses md size by default", () => {
const handleChange = vi.fn();
render(<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />);
render(
<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />,
);
const tab = screen.getByTestId("tab-all");
expect(tab.className).toContain("text-base");
expect(tab.className).toContain("px-4");
@ -191,23 +271,38 @@ describe("Tabs", () => {
describe("Accessibility", () => {
it('tabs have role="tab"', () => {
const handleChange = vi.fn();
render(<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />);
render(
<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />,
);
const tab = screen.getByTestId("tab-all");
expect(tab).toHaveAttribute("role", "tab");
});
it("tabs have unique IDs", () => {
const handleChange = vi.fn();
render(<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />);
render(
<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />,
);
expect(screen.getByTestId("tab-all")).toHaveAttribute("id", "tab-all");
expect(screen.getByTestId("tab-featured")).toHaveAttribute("id", "tab-featured");
expect(screen.getByTestId("tab-recent")).toHaveAttribute("id", "tab-recent");
expect(screen.getByTestId("tab-featured")).toHaveAttribute(
"id",
"tab-featured",
);
expect(screen.getByTestId("tab-recent")).toHaveAttribute(
"id",
"tab-recent",
);
});
it("tabs have aria-controls attribute", () => {
const handleChange = vi.fn();
render(<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />);
expect(screen.getByTestId("tab-all")).toHaveAttribute("aria-controls", "tabpanel-all");
render(
<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />,
);
expect(screen.getByTestId("tab-all")).toHaveAttribute(
"aria-controls",
"tabpanel-all",
);
expect(screen.getByTestId("tab-featured")).toHaveAttribute(
"aria-controls",
"tabpanel-featured",
@ -289,7 +384,12 @@ describe("Tabs", () => {
it("container includes base classes", () => {
const handleChange = vi.fn();
render(
<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} data-testid="tabs" />,
<Tabs
tabs={mockTabs}
activeTab="all"
onTabChange={handleChange}
data-testid="tabs"
/>,
);
const tabs = screen.getByTestId("tabs");
expect(tabs.className).toContain("flex");
@ -300,7 +400,9 @@ describe("Tabs", () => {
it("tab buttons include base classes", () => {
const handleChange = vi.fn();
render(<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />);
render(
<Tabs tabs={mockTabs} activeTab="all" onTabChange={handleChange} />,
);
const tab = screen.getByTestId("tab-all");
expect(tab.className).toContain("inline-flex");
expect(tab.className).toContain("items-center");
@ -314,7 +416,14 @@ describe("Tabs", () => {
describe("Compound Scenarios", () => {
it("works correctly with large size and active tab with count", () => {
const handleChange = vi.fn();
render(<Tabs tabs={mockTabs} activeTab="featured" onTabChange={handleChange} size="lg" />);
render(
<Tabs
tabs={mockTabs}
activeTab="featured"
onTabChange={handleChange}
size="lg"
/>,
);
const tab = screen.getByTestId("tab-featured");
expect(tab.className).toContain("text-lg");
expect(tab.className).toContain("px-6");
@ -328,14 +437,18 @@ describe("Tabs", () => {
it("handles single tab", () => {
const handleChange = vi.fn();
const singleTab: Tab[] = [{ id: "only", label: "Only Tab" }];
render(<Tabs tabs={singleTab} activeTab="only" onTabChange={handleChange} />);
render(
<Tabs tabs={singleTab} activeTab="only" onTabChange={handleChange} />,
);
expect(screen.getByTestId("tab-only")).toBeInTheDocument();
expect(screen.getByText("Only Tab")).toBeInTheDocument();
});
it("handles empty tabs array", () => {
const handleChange = vi.fn();
const { container } = render(<Tabs tabs={[]} activeTab="" onTabChange={handleChange} />);
const { container } = render(
<Tabs tabs={[]} activeTab="" onTabChange={handleChange} />,
);
const buttons = container.querySelectorAll("button");
expect(buttons.length).toBe(0);
});

View file

@ -1,7 +1,11 @@
import { cn } from "@tpmjs/utils/cn";
import { forwardRef } from "react";
import type { Tab, TabsProps } from "./types";
import { tabButtonVariants, tabCountVariants, tabsContainerVariants } from "./variants";
import {
tabButtonVariants,
tabCountVariants,
tabsContainerVariants,
} from "./variants";
// Re-export types for consumers
export type { Tab, TabsProps };
@ -36,7 +40,15 @@ export type { Tab, TabsProps };
*/
export const Tabs = forwardRef<HTMLDivElement, TabsProps>(
(
{ className, tabs, activeTab, onTabChange, size = "md", variant = "default", ...props },
{
className,
tabs,
activeTab,
onTabChange,
size = "md",
variant = "default",
...props
},
ref,
) => {
return (

View file

@ -23,7 +23,8 @@ export interface Tab {
/**
* Tabs component props
*/
export interface TabsProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
export interface TabsProps
extends Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
/**
* Array of tabs to display
*/

View file

@ -57,9 +57,10 @@ export type VariantConfig<T extends Record<string, Record<string, string>>> = {
/**
* Props type for consuming variant functions
*/
export type VariantProps<T extends Record<string, Record<string, string>>> = Partial<{
[K in keyof T]: keyof T[K];
}>;
export type VariantProps<T extends Record<string, Record<string, string>>> =
Partial<{
[K in keyof T]: keyof T[K];
}>;
/**
* Creates a type-safe variant composition function
@ -67,9 +68,9 @@ export type VariantProps<T extends Record<string, Record<string, string>>> = Par
* @param config - Variant configuration with base, variants, compound variants, and defaults
* @returns Function that accepts variant props and returns composed className string
*/
export function createVariants<T extends Record<string, Record<string, string>>>(
config: VariantConfig<T>,
) {
export function createVariants<
T extends Record<string, Record<string, string>>,
>(config: VariantConfig<T>) {
return function getVariantClasses(props?: VariantProps<T>): string {
const classes: string[] = [config.base];
@ -82,7 +83,11 @@ export function createVariants<T extends Record<string, Record<string, string>>>
// Apply variant classes
for (const [key, value] of Object.entries(mergedProps)) {
const valueStr = value as string;
if (value !== undefined && value !== null && config.variants[key]?.[valueStr]) {
if (
value !== undefined &&
value !== null &&
config.variants[key]?.[valueStr]
) {
classes.push(config.variants[key][valueStr]);
}
}
@ -91,7 +96,8 @@ export function createVariants<T extends Record<string, Record<string, string>>>
if (config.compoundVariants) {
for (const { conditions, className } of config.compoundVariants) {
const matches = Object.entries(conditions).every(
([key, value]) => mergedProps[key as keyof typeof mergedProps] === value,
([key, value]) =>
mergedProps[key as keyof typeof mergedProps] === value,
);
if (matches) {
classes.push(className);