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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 23 additions & 16 deletions packages/ui/src/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { forwardRef } from "react";
import type { ButtonHTMLAttributes } from "react";
import { cn } from "../cn";

Expand Down Expand Up @@ -25,19 +26,25 @@ const SIZES: Record<ButtonSize, string> = {
* The primary action control. Amber ("lumen") by default — the one accent the
* design language reserves for the single most important action on a surface.
*/
export function Button({ variant = "primary", size = "md", className, ...props }: ButtonProps) {
return (
<button
className={cn(
"inline-flex items-center justify-center gap-2 rounded-m font-ui font-semibold",
"transition-colors duration-fast ease-standard",
"focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-lumen",
"disabled:cursor-not-allowed disabled:opacity-50",
VARIANTS[variant],
SIZES[size],
className,
)}
{...props}
/>
);
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ variant = "primary", size = "md", className, type = "button", ...props }, ref) => {
return (
<button
ref={ref}
type={type}
className={cn(
"inline-flex items-center justify-center gap-2 rounded-m font-ui font-semibold",
"transition-colors duration-fast ease-standard",
"focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-lumen",
"disabled:cursor-not-allowed disabled:opacity-50",
VARIANTS[variant],
SIZES[size],
className,
)}
{...props}
/>
);
},
);

Button.displayName = "Button";
20 changes: 15 additions & 5 deletions packages/ui/src/components/Card.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import { forwardRef } from "react";
import type { HTMLAttributes } from "react";
import { cn } from "../cn";

export type CardProps = HTMLAttributes<HTMLDivElement>;

/** A raised surface for grouping content. Ink-tinted for the dark-mode-first product. */
export function Card({ className, ...props }: CardProps) {
export const Card = forwardRef<HTMLDivElement, CardProps>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
className={cn(
"rounded-l border border-ink-700 bg-ink-900 p-6 text-paper shadow-m",
className,
)}
{...props}
/>
);
}
});

Card.displayName = "Card";

export type CardTitleProps = HTMLAttributes<HTMLHeadingElement>;

Expand All @@ -31,6 +35,12 @@ export function CardTitle({ className, ...props }: CardTitleProps) {
export type CardBodyProps = HTMLAttributes<HTMLParagraphElement>;

/** Muted supporting copy inside a {@link Card}. */
export function CardBody({ className, ...props }: CardBodyProps) {
return <p className={cn("mt-2 font-ui text-body text-ink-400", className)} {...props} />;
}
export const CardBody = forwardRef<HTMLParagraphElement, CardBodyProps>(
({ className, ...props }, ref) => {
return (
<p ref={ref} className={cn("mt-2 font-ui text-body text-ink-400", className)} {...props} />
);
},
);

CardBody.displayName = "CardBody";
38 changes: 22 additions & 16 deletions packages/ui/src/components/Input.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
import { forwardRef } from "react";
import type { InputHTMLAttributes } from "react";
import { cn } from "../cn";

export type InputProps = InputHTMLAttributes<HTMLInputElement>;

/** A single-line text field styled for ink surfaces. */
export function Input({ className, type = "text", ...props }: InputProps) {
return (
<input
type={type}
className={cn(
"h-10 w-full rounded-m border border-ink-600 bg-ink-800 px-3 font-ui text-body text-paper",
"placeholder:text-ink-400",
"transition-colors duration-fast ease-standard",
"focus-visible:border-lumen focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-lumen",
"disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
/>
);
}
export const Input = forwardRef<HTMLInputElement, InputProps>(
({ className, type = "text", ...props }, ref) => {
return (
<input
ref={ref}
type={type}
className={cn(
"h-10 w-full rounded-m border border-ink-600 bg-ink-800 px-3 font-ui text-body text-paper",
"placeholder:text-ink-400",
"transition-colors duration-fast ease-standard",
"focus-visible:border-lumen focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-lumen",
"disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
/>
);
},
);

Input.displayName = "Input";
21 changes: 21 additions & 0 deletions packages/ui/src/ui.test.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
// @vitest-environment jsdom
import { afterEach, describe, expect, it } from "vitest";
import { createRef } from "react";
import { cleanup, render } from "@testing-library/react";
import { Badge } from "./components/Badge";
import { Button } from "./components/Button";
import { Input } from "./components/Input";
import { cn } from "./cn";

// Not using vitest `globals`, so register Testing Library's DOM cleanup ourselves.
Expand All @@ -25,6 +27,25 @@ describe("Button", () => {
const { getByRole } = render(<Button>Go</Button>);
expect(getByRole("button").className).toContain("bg-lumen");
});

it("defaults to type='button' when no type is provided", () => {
const { getByRole } = render(<Button>Click me</Button>);
expect(getByRole("button").getAttribute("type")).toBe("button");
});

it("forwards a ref to the underlying <button> element", () => {
const ref = createRef<HTMLButtonElement>();
const { getByRole } = render(<Button ref={ref}>Ref test</Button>);
expect(ref.current).toBe(getByRole("button", { name: "Ref test" }));
});
});

describe("Input", () => {
it("forwards a ref to the underlying <input> element", () => {
const ref = createRef<HTMLInputElement>();
const { getByRole } = render(<Input ref={ref} aria-label="test input" />);
expect(ref.current).toBe(getByRole("textbox", { name: "test input" }));
});
});

describe("Badge", () => {
Expand Down
Loading