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