feat(ui): Select primitive; restyle ProfileSelector on tokens
- New Select primitive in @trails-cool/ui: token-styled native <select> (appearance-none + overlaid chevron for a consistent closed control), sm/md sizes, sibling of Input. Unit-tested + added to /dev/ui. - ProfileSelector uses Select (size sm) with token label; the profile label now hides on small screens. Completes the topbar's migration onto the design system. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
bf73feb75a
commit
a56445daf3
5 changed files with 122 additions and 4 deletions
52
packages/ui/src/Select.test.tsx
Normal file
52
packages/ui/src/Select.test.tsx
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
// @vitest-environment jsdom
|
||||
import { describe, it, expect, afterEach, vi } from "vitest";
|
||||
import { render, screen, cleanup, fireEvent } from "@testing-library/react";
|
||||
import { Select } from "./Select.tsx";
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
function options() {
|
||||
return (
|
||||
<>
|
||||
<option value="fastbike">Fast bike</option>
|
||||
<option value="trekking">Trekking</option>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
describe("Select", () => {
|
||||
it("renders options and reflects the value", () => {
|
||||
render(
|
||||
<Select value="trekking" onChange={() => {}} aria-label="Profile">
|
||||
{options()}
|
||||
</Select>,
|
||||
);
|
||||
const select = screen.getByRole("combobox", { name: "Profile" });
|
||||
expect((select as HTMLSelectElement).value).toBe("trekking");
|
||||
expect(select.className).toContain("border-border");
|
||||
});
|
||||
|
||||
it("fires onChange with the selected value", () => {
|
||||
const onChange = vi.fn();
|
||||
render(
|
||||
<Select value="fastbike" onChange={onChange} aria-label="Profile">
|
||||
{options()}
|
||||
</Select>,
|
||||
);
|
||||
fireEvent.change(screen.getByRole("combobox", { name: "Profile" }), {
|
||||
target: { value: "trekking" },
|
||||
});
|
||||
expect(onChange).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("applies the compact size classes", () => {
|
||||
render(
|
||||
<Select size="sm" aria-label="Profile">
|
||||
{options()}
|
||||
</Select>,
|
||||
);
|
||||
expect(screen.getByRole("combobox", { name: "Profile" }).className).toContain(
|
||||
"h-7",
|
||||
);
|
||||
});
|
||||
});
|
||||
49
packages/ui/src/Select.tsx
Normal file
49
packages/ui/src/Select.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import type { SelectHTMLAttributes } from "react";
|
||||
import { cn } from "./cn.ts";
|
||||
|
||||
export type SelectSize = "sm" | "md";
|
||||
|
||||
export interface SelectProps
|
||||
extends Omit<SelectHTMLAttributes<HTMLSelectElement>, "size"> {
|
||||
size?: SelectSize;
|
||||
}
|
||||
|
||||
const base =
|
||||
"appearance-none rounded-md border border-border bg-bg-raised text-text-hi " +
|
||||
"transition-colors focus-visible:border-accent focus-visible:outline-none " +
|
||||
"focus-visible:ring-2 focus-visible:ring-accent-border " +
|
||||
"disabled:cursor-not-allowed disabled:opacity-50";
|
||||
|
||||
const sizes: Record<SelectSize, string> = {
|
||||
sm: "h-7 pl-2.5 pr-7 text-xs",
|
||||
md: "h-9 pl-3 pr-8 text-sm",
|
||||
};
|
||||
|
||||
/**
|
||||
* Token-styled wrapper around a native <select>. Uses `appearance-none` plus an
|
||||
* overlaid chevron so the closed control looks consistent across browsers while
|
||||
* keeping native option-list behaviour and accessibility.
|
||||
*/
|
||||
export function Select({ size = "md", className, children, ...props }: SelectProps) {
|
||||
return (
|
||||
<span className="relative inline-flex">
|
||||
<select className={cn(base, sizes[size], className)} {...props}>
|
||||
{children}
|
||||
</select>
|
||||
<svg
|
||||
className="pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 text-text-md"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden
|
||||
>
|
||||
<polyline points="6 9 12 15 18 9" />
|
||||
</svg>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
@ -7,6 +7,8 @@ export { Card } from "./Card.tsx";
|
|||
export type { CardProps } from "./Card.tsx";
|
||||
export { Input } from "./Input.tsx";
|
||||
export type { InputProps } from "./Input.tsx";
|
||||
export { Select } from "./Select.tsx";
|
||||
export type { SelectProps, SelectSize } from "./Select.tsx";
|
||||
export { IconButton } from "./IconButton.tsx";
|
||||
export type {
|
||||
IconButtonProps,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue