diff --git a/apps/planner/app/components/ProfileSelector.tsx b/apps/planner/app/components/ProfileSelector.tsx index 3ee079a..0131486 100644 --- a/apps/planner/app/components/ProfileSelector.tsx +++ b/apps/planner/app/components/ProfileSelector.tsx @@ -1,5 +1,6 @@ import { useEffect, useState, useCallback } from "react"; import { useTranslation } from "react-i18next"; +import { Select } from "@trails-cool/ui"; import type { YjsState } from "~/lib/use-yjs"; import { DEFAULT_PROFILE, getProfile, setProfile as setYjsProfile } from "~/lib/route-data"; @@ -34,21 +35,21 @@ export function ProfileSelector({ yjs }: ProfileSelectorProps) { return (
-
); } diff --git a/apps/planner/app/routes/dev.ui.tsx b/apps/planner/app/routes/dev.ui.tsx index 22dc4ed..02fdeee 100644 --- a/apps/planner/app/routes/dev.ui.tsx +++ b/apps/planner/app/routes/dev.ui.tsx @@ -6,6 +6,7 @@ import { Card, IconButton, Input, + Select, SegmentedControl, } from "@trails-cool/ui"; import { Topbar } from "~/components/Topbar"; @@ -123,6 +124,19 @@ export default function DevUi() { +
+ + +
+
diff --git a/packages/ui/src/Select.test.tsx b/packages/ui/src/Select.test.tsx new file mode 100644 index 0000000..d8111f5 --- /dev/null +++ b/packages/ui/src/Select.test.tsx @@ -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 ( + <> + + + + ); +} + +describe("Select", () => { + it("renders options and reflects the value", () => { + render( + , + ); + 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( + , + ); + fireEvent.change(screen.getByRole("combobox", { name: "Profile" }), { + target: { value: "trekking" }, + }); + expect(onChange).toHaveBeenCalledTimes(1); + }); + + it("applies the compact size classes", () => { + render( + , + ); + expect(screen.getByRole("combobox", { name: "Profile" }).className).toContain( + "h-7", + ); + }); +}); diff --git a/packages/ui/src/Select.tsx b/packages/ui/src/Select.tsx new file mode 100644 index 0000000..674aad2 --- /dev/null +++ b/packages/ui/src/Select.tsx @@ -0,0 +1,49 @@ +import type { SelectHTMLAttributes } from "react"; +import { cn } from "./cn.ts"; + +export type SelectSize = "sm" | "md"; + +export interface SelectProps + extends Omit, "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 = { + 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 + {children} + + + + + + ); +} diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 82bbc79..93eb65d 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -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,