style: apply Biome formatting and auto-fixes

- Format 78 files, fixed 42 files total
- Apply safe lint fixes (non-null assertions converted to optional chaining)
- Remaining lint warnings are non-blocking (test code, a11y suggestions)

🤖 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 18:59:49 +10:00
parent 65be671143
commit e904e8d319
34 changed files with 167 additions and 525 deletions

View file

@ -1,28 +1,31 @@
import reactConfig from '@tpmjs/eslint-config/react.js';
import reactConfig from "@tpmjs/eslint-config/react.js";
export default [
{
ignores: ['dist/**', 'node_modules/**', 'eslint.config.mjs']
},
...reactConfig,
{
rules: {
'import/no-internal-modules': ['error', {
allow: [
'@tpmjs/ui/*/[!index]*',
'@tpmjs/utils/*',
'@tpmjs/types/*',
'@tpmjs/eslint-config/*',
'@testing-library/**',
'react/*',
]
}],
}
},
{
files: ['**/*.test.ts', '**/*.test.tsx', '**/*.spec.ts', '**/*.spec.tsx'],
rules: {
'jsx-a11y/no-autofocus': 'off',
}
}
{
ignores: ["dist/**", "node_modules/**", "eslint.config.mjs"],
},
...reactConfig,
{
rules: {
"import/no-internal-modules": [
"error",
{
allow: [
"@tpmjs/ui/*/[!index]*",
"@tpmjs/utils/*",
"@tpmjs/types/*",
"@tpmjs/eslint-config/*",
"@testing-library/**",
"react/*",
],
},
],
},
},
{
files: ["**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts", "**/*.spec.tsx"],
rules: {
"jsx-a11y/no-autofocus": "off",
},
},
];

View file

@ -250,7 +250,7 @@ describe("Badge", () => {
</Badge>,
);
expect(ref).toBeInstanceOf(HTMLDivElement);
expect(ref!.tagName).toBe("DIV");
expect(ref?.tagName).toBe("DIV");
});
});
@ -297,11 +297,7 @@ 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>,
);
@ -355,12 +351,7 @@ 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>,
);
@ -372,12 +363,7 @@ 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,14 +8,7 @@ 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,33 +19,17 @@ 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,9 +119,7 @@ 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");
@ -241,19 +239,14 @@ 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,11 +65,9 @@ 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,13 +1,6 @@
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", () => {
@ -23,9 +16,7 @@ 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>
@ -236,9 +227,7 @@ describe("Card", () => {
render(
<Card>
<CardHeader>
<CardDescription data-testid="description">
Description text
</CardDescription>
<CardDescription data-testid="description">Description text</CardDescription>
</CardHeader>
</Card>,
);
@ -250,9 +239,7 @@ describe("Card", () => {
render(
<Card>
<CardHeader>
<CardDescription data-testid="description">
Description
</CardDescription>
<CardDescription data-testid="description">Description</CardDescription>
</CardHeader>
</Card>,
);
@ -417,7 +404,7 @@ describe("Card", () => {
</Card>,
);
expect(ref).toBeInstanceOf(HTMLDivElement);
expect(ref!.tagName).toBe("DIV");
expect(ref?.tagName).toBe("DIV");
});
it("forwards ref to CardHeader element", () => {
@ -452,7 +439,7 @@ describe("Card", () => {
</Card>,
);
expect(ref).toBeInstanceOf(HTMLHeadingElement);
expect(ref!.tagName).toBe("H3");
expect(ref?.tagName).toBe("H3");
});
it("forwards ref to CardDescription element", () => {
@ -574,9 +561,7 @@ describe("Card", () => {
render(
<Card>
<CardHeader>
<CardDescription data-testid="description">
Description
</CardDescription>
<CardDescription data-testid="description">Description</CardDescription>
</CardHeader>
</Card>,
);
@ -606,9 +591,7 @@ 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>,
);
@ -616,9 +599,7 @@ 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,13 +93,7 @@ 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} />;
},
);
@ -110,18 +104,11 @@ 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,10 +28,7 @@ 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",
@ -85,10 +82,7 @@ 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,13 +50,7 @@ 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");
@ -77,9 +71,7 @@ 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");
@ -223,12 +215,8 @@ 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" />);
@ -249,17 +237,13 @@ 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");
});
@ -267,13 +251,7 @@ 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");
@ -293,7 +271,7 @@ describe("CodeBlock", () => {
/>,
);
expect(ref).toBeInstanceOf(HTMLDivElement);
expect(ref!.querySelector("code")).toBeInTheDocument();
expect(ref?.querySelector("code")).toBeInTheDocument();
});
});

View file

@ -32,17 +32,7 @@ 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 () => {
@ -57,11 +47,7 @@ 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,8 +3,7 @@ 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,13 +281,7 @@ 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>,
);
@ -300,12 +294,7 @@ 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,11 +77,7 @@ 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();
});
});
@ -255,7 +251,7 @@ describe("Header", () => {
/>,
);
expect(ref).toBeInstanceOf(HTMLElement);
expect(ref!.tagName).toBe("HEADER");
expect(ref?.tagName).toBe("HEADER");
});
});
@ -275,15 +271,7 @@ 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,11 +1,7 @@
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
@ -31,18 +27,7 @@ import {
* ```
*/
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(
@ -66,10 +51,7 @@ 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,8 +3,7 @@ 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,10 +119,7 @@ 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");
});
});
@ -148,14 +145,7 @@ 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");
});
@ -179,7 +169,7 @@ describe("Icon", () => {
/>,
);
expect(ref).toBeInstanceOf(SVGSVGElement);
expect(ref!.tagName).toBe("svg");
expect(ref?.tagName).toBe("svg");
});
});

View file

@ -4,8 +4,7 @@ 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,9 +196,7 @@ 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");
});
@ -317,7 +315,7 @@ describe("Input", () => {
/>,
);
expect(ref).toBeInstanceOf(HTMLInputElement);
expect(ref!.tagName).toBe("INPUT");
expect(ref?.tagName).toBe("INPUT");
});
it("can focus input through ref", () => {
@ -330,7 +328,7 @@ describe("Input", () => {
data-testid="input"
/>,
);
ref!.focus();
ref?.focus();
expect(screen.getByTestId("input")).toHaveFocus();
});
});

View file

@ -3,8 +3,7 @@ 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,17 +18,7 @@ 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,36 +103,28 @@ 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");
@ -183,17 +175,13 @@ 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");
});
@ -201,13 +189,7 @@ 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");
@ -253,14 +235,7 @@ 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,17 +26,7 @@ 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,8 +3,7 @@ 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,13 +308,7 @@ 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,12 +14,7 @@ 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();
@ -29,12 +24,7 @@ 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");
@ -42,9 +32,7 @@ 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();
@ -52,9 +40,7 @@ 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();
@ -62,9 +48,7 @@ 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");
@ -74,32 +58,15 @@ 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");
@ -107,9 +74,7 @@ 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");
@ -119,18 +84,14 @@ 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);
@ -138,9 +99,7 @@ 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");
});
@ -149,18 +108,14 @@ 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();
@ -168,27 +123,21 @@ 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");
});
@ -196,13 +145,7 @@ 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();
});
});
@ -210,14 +153,7 @@ 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");
@ -226,14 +162,7 @@ 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");
@ -242,14 +171,7 @@ 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");
@ -258,9 +180,7 @@ 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");
@ -271,38 +191,23 @@ 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",
@ -384,12 +289,7 @@ 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");
@ -400,9 +300,7 @@ 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");
@ -416,14 +314,7 @@ 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");
@ -437,18 +328,14 @@ 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,11 +1,7 @@
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 };
@ -40,15 +36,7 @@ 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,8 +23,7 @@ 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,10 +57,9 @@ 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
@ -68,9 +67,9 @@ export type VariantProps<T extends Record<string, Record<string, string>>> =
* @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];
@ -83,11 +82,7 @@ export function createVariants<
// 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]);
}
}
@ -96,8 +91,7 @@ export function createVariants<
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);

View file

@ -1 +1 @@
import '@testing-library/jest-dom/vitest';
import "@testing-library/jest-dom/vitest";

View file

@ -1,11 +1,11 @@
{
"extends": "@tpmjs/tsconfig/react-library.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"incremental": false,
"composite": false
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
"extends": "@tpmjs/tsconfig/react-library.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"incremental": false,
"composite": false
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

View file

@ -1,9 +1,9 @@
import { defineConfig } from 'vitest/config';
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true,
environment: 'happy-dom',
setupFiles: ['./src/test-setup.ts'],
},
test: {
globals: true,
environment: "happy-dom",
setupFiles: ["./src/test-setup.ts"],
},
});