feat(ui): add a shared native select and a segmented control
Two patterns the app had been rebuilding per call site. NativeSelect: eight `<select>` elements had each been hand-styled, arriving at four different answers — `h-8 rounded border px-1 text-xs`, `h-9 rounded border px-2 text-sm`, `h-9 flex-1 rounded ...`, and one correct `h-9 rounded-md border-input px-2`. Measured, that is two radii (4px against the system's 6px), two border colours (the row hairline against the 3:1 field border) and three text indents against Input's 12px. In the purchase dialog a select sat directly beside the Total input in a visibly different control language. It stays a native select on purpose: the platform picker on a phone and type-to-select on a keyboard are worth more here than a prettier menu, and entry speed is a stated product principle. The chevron is a background image from a new `--budget-select-chevron` token rather than a positioned icon, because that needs no wrapper element — so `className` lands on the select itself and every call site's existing `w-full`, `flex-1`, `h-8` and `w-40` keeps behaving as it did. The token is declared per theme since a background-image stroke cannot be `currentColor`. Segmented: four screens had built the same view switch from two loose buttons, the selected one filled and the other outlined, separated by a gap. That reads as one primary action beside one unrelated button rather than as two states of a single control. One track, no gap, `aria-pressed` per segment. Neither is wired up yet; the call sites follow. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
da8431a57f
commit
394c5f74df
@@ -0,0 +1,45 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
* A native `<select>`, styled to Input's exact metrics.
|
||||
*
|
||||
* Native on purpose. This app is a fast-entry tool, and a real `<select>` gets
|
||||
* the platform picker on a phone, type-to-select on a keyboard and the OS's own
|
||||
* accessibility affordances for free — a Radix listbox would trade all of that
|
||||
* for a nicer-looking menu. `src/components/ui/select.tsx` is still there for
|
||||
* anywhere that genuinely needs a rich menu; nothing does yet.
|
||||
*
|
||||
* It exists because eight call sites had each hand-rolled the same control and
|
||||
* arrived at four different answers: `h-8 rounded border px-1 text-xs`,
|
||||
* `h-9 rounded border px-2 text-sm`, `h-9 flex-1 rounded ...` and one correct
|
||||
* `h-9 rounded-md border-input px-2`. So a select and the Input beside it in
|
||||
* the same row disagreed on radius (4px vs 6px), on border colour (the row
|
||||
* hairline vs the 3:1 field border) and on text indent (4 or 8px vs 12px).
|
||||
*
|
||||
* `className` lands on the `<select>` itself and there is no wrapper element,
|
||||
* so every layout class a call site already used — `w-full`, `flex-1`, `h-8`,
|
||||
* `w-40` — keeps behaving exactly as it did. That is also why the chevron is a
|
||||
* background image from `--budget-select-chevron` rather than an absolutely
|
||||
* positioned icon: no wrapper to position it against.
|
||||
*/
|
||||
function NativeSelect({ className, ...props }: React.ComponentProps<"select">) {
|
||||
return (
|
||||
<select
|
||||
data-slot="native-select"
|
||||
className={cn(
|
||||
// Matches Input line for line, except for the right padding that keeps
|
||||
// the longest option clear of the chevron.
|
||||
"h-9 w-full min-w-0 appearance-none rounded-md border border-input bg-transparent bg-no-repeat py-1 pr-8 pl-3 text-base shadow-xs transition-[color,box-shadow] outline-none md:text-sm dark:bg-input/30",
|
||||
"bg-[image:var(--budget-select-chevron)] bg-[length:0.875rem] bg-[position:right_0.625rem_center]",
|
||||
"disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"aria-invalid:border-destructive",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { NativeSelect };
|
||||
Reference in New Issue
Block a user