refactor(ui): render explicit CaretDown on Select; normalize affordance across platforms

Native <select> chrome varies: macOS renders a dual-arrow indicator,
Windows and Linux render a single caret, and on dark themes the
native caret can drop to near-invisible contrast. The agent Edit
modal (image 5 in the audit) had five side-by-side Selects where the
indicator read as a subtle decoration, making the field look closer
to a filled-in text input than a pick-from-list.

Switched Select to:
- `appearance-none` to strip native chrome
- Phosphor CaretDown rendered absolutely on the right, bold weight,
  muted-foreground so it reads against both card and background
- Padding-right increased to reserve space so long option labels
  do not overlap the caret
- Opacity drop when disabled so the affordance tracks the field state

Scope: only the shared Select primitive; every caller inherits the
fix automatically. No API change.

Covers J10.
This commit is contained in:
ZacharyZhang-NY committed 2026-04-17 19:58:42 -04:00
1 parent 36bed1b0f1
commit e87170f105
1 file changed
+34 -16
+34 -16
View File
@@ -1,3 +1,13 @@
import { CaretDown } from "./icons";
/**
* Native <select> + explicit caret icon. We render our own CaretDown so
* the affordance is consistent across platforms (macOS shows dual-arrow,
* Windows/Linux a single caret of varying contrast) and visible against
* dark themes. `appearance: none` collapses the native chrome so the
* caret we render is the only visual indicator; padding-right reserves
* space for it so long option labels don't overlap.
*/
export function Select({
value,
onChange,
@@ -16,22 +26,30 @@ export function Select({
disabled?: boolean;
}) {
const select = (
<select
className={`focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 w-full max-w-full border bg-card text-foreground ${
compact
? "rounded-lg border-border px-2.5 py-1.5 text-[13px] font-medium"
: "rounded-lg border-border px-3.5 py-2.5 text-[14px]"
} disabled:cursor-not-allowed disabled:opacity-60 truncate`}
value={value}
disabled={disabled}
onChange={(event) => onChange(event.target.value)}
>
{options.map((option) => (
<option key={option.value} value={option.value} className="bg-card text-foreground">
{option.label.length > 80 ? option.label.slice(0, 77) + "..." : option.label}
</option>
))}
</select>
<div className="relative">
<select
className={`focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 w-full max-w-full appearance-none border bg-card text-foreground ${
compact
? "rounded-lg border-border pl-2.5 pr-7 py-1.5 text-[13px] font-medium"
: "rounded-lg border-border pl-3.5 pr-9 py-2.5 text-[14px]"
} disabled:cursor-not-allowed disabled:opacity-60 truncate`}
value={value}
disabled={disabled}
onChange={(event) => onChange(event.target.value)}
>
{options.map((option) => (
<option key={option.value} value={option.value} className="bg-card text-foreground">
{option.label.length > 80 ? option.label.slice(0, 77) + "..." : option.label}
</option>
))}
</select>
<CaretDown
size={compact ? 12 : 14}
weight="bold"
className={`pointer-events-none absolute top-1/2 -translate-y-1/2 text-muted-foreground ${compact ? "right-2" : "right-3"} ${disabled ? "opacity-40" : ""}`}
aria-hidden="true"
/>
</div>
);
if (label) {
return (