Four raw <select> elements (web/src/fields/field-form.tsx:120,138,158, web/src/objects/object-form.tsx:148) are styled w-full rounded border px-2 py-1 text-sm, while the sibling ui/Input (components/ui/input.tsx:12) is h-8 rounded-lg border-input … focus-visible:ring-3 focus-visible:ring-ring/50. So in the same form a select and an Input differ in radius, border color, padding, height — and the select has no focus ring (keyboard users get no focus affordance). Separately, the field-form's vocabulary/authority pickers use a bare native <select> while the object form's equivalent reference pickers use the searchable OptionsCombobox (objects/field-input.tsx:8) — the same "pick a vocabulary/authority" action looks and behaves differently in two places.
Suggested fix
Add a ui/select.tsx (Base UI Select) that matches Input's tokens/height/radius/focus ring, and replace all four raw selects.
For the field-form vocabulary + authority-kind pickers, reuse the existing combobox so reference selection is uniform with object editing.
Source: frontend UX/design audit, 2026-06-06.
**Severity: High.** _From a frontend UX audit._
## Problem
Four raw `<select>` elements (`web/src/fields/field-form.tsx:120,138,158`, `web/src/objects/object-form.tsx:148`) are styled `w-full rounded border px-2 py-1 text-sm`, while the sibling `ui/Input` (`components/ui/input.tsx:12`) is `h-8 rounded-lg border-input … focus-visible:ring-3 focus-visible:ring-ring/50`. So in the **same form** a select and an Input differ in radius, border color, padding, height — and the select has **no focus ring** (keyboard users get no focus affordance). Separately, the field-form's vocabulary/authority pickers use a bare native `<select>` while the object form's equivalent reference pickers use the searchable `OptionsCombobox` (`objects/field-input.tsx:8`) — the same "pick a vocabulary/authority" action looks and behaves differently in two places.
## Suggested fix
- Add a `ui/select.tsx` (Base UI Select) that matches `Input`'s tokens/height/radius/focus ring, and replace all four raw selects.
- For the field-form vocabulary + authority-kind pickers, reuse the existing combobox so reference selection is uniform with object editing.
_Source: frontend UX/design audit, 2026-06-06._
Added web/src/components/ui/select.tsx (Base UI Select) styled to match ui/Input — same h-8/rounded-lg/border-input/padding and a real focus-visible:ring-3 ring-ring/50 focus ring, plus disabled + aria-invalid states. Story-validated by running.
Replaced all four raw <select> elements with it:
object-form visibility (draft/internal) — via a react-hook-form Controller (Base UI Select isn't a native input); default stays draft.
field-form data_type, vocabulary, authority-kind — value/onValueChange; disabled-on-edit preserved; the vocabulary picker shows a placeholder until chosen (the required check still blocks submit); the authority "Any" state shows "Any" (placeholder), with an "Any" item to switch back.
So in a given form, selects and Inputs now share radius/border/height/padding, and keyboard users get a focus ring on every control.
Implementation notes:
The wrapper derives item labels from its <SelectItem> children (incl. .map() arrays), so the trigger shows the chosen label with the plain <SelectItem value="x">Label</SelectItem> API.
Tests rewritten from native userEvent.selectOptions to open-trigger + click-option (Base UI Select isn't a <select>); all payload assertions unchanged.
Base UI Select code-split into its own ~12.6 KB gz chunk — the entry bundle stayed 214.4 KB gz (< 250 budget).
No new dependency (Base UI already present); no new i18n keys; en/sv parity; 204 tests green; typecheck/lint/build/check:size/check:colors clean; no codename.
Follow-ups (out of scope): a searchable vocabulary picker (generalize OptionsCombobox to {id,label}) if vocab lists grow; the remaining non-target <select> (objects-table page-size picker); a lint guard banning raw <select> outside components/ui/.
Done — merged to `main` (`f45f1d8`).
Added **`web/src/components/ui/select.tsx`** (Base UI Select) styled to match `ui/Input` — same `h-8`/`rounded-lg`/`border-input`/padding and a real `focus-visible:ring-3 ring-ring/50` focus ring, plus disabled + `aria-invalid` states. Story-validated by running.
Replaced all four raw `<select>` elements with it:
- **object-form** visibility (draft/internal) — via a react-hook-form `Controller` (Base UI Select isn't a native input); default stays draft.
- **field-form** data_type, vocabulary, authority-kind — `value`/`onValueChange`; disabled-on-edit preserved; the vocabulary picker shows a placeholder until chosen (the required check still blocks submit); the authority "Any" state shows "Any" (placeholder), with an "Any" item to switch back.
So in a given form, selects and Inputs now share radius/border/height/padding, and keyboard users get a focus ring on every control.
Implementation notes:
- The wrapper derives item labels from its `<SelectItem>` children (incl. `.map()` arrays), so the trigger shows the chosen label with the plain `<SelectItem value="x">Label</SelectItem>` API.
- Tests rewritten from native `userEvent.selectOptions` to open-trigger + click-option (Base UI Select isn't a `<select>`); all payload assertions unchanged.
- Base UI Select code-split into its own ~12.6 KB gz chunk — the entry bundle stayed 214.4 KB gz (< 250 budget).
No new dependency (Base UI already present); no new i18n keys; en/sv parity; 204 tests green; typecheck/lint/build/check:size/check:colors clean; no codename.
Follow-ups (out of scope): a **searchable** vocabulary picker (generalize `OptionsCombobox` to `{id,label}`) if vocab lists grow; the remaining non-target `<select>` (objects-table page-size picker); a lint guard banning raw `<select>` outside `components/ui/`.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Severity: High. From a frontend UX audit.
Problem
Four raw
<select>elements (web/src/fields/field-form.tsx:120,138,158,web/src/objects/object-form.tsx:148) are styledw-full rounded border px-2 py-1 text-sm, while the siblingui/Input(components/ui/input.tsx:12) ish-8 rounded-lg border-input … focus-visible:ring-3 focus-visible:ring-ring/50. So in the same form a select and an Input differ in radius, border color, padding, height — and the select has no focus ring (keyboard users get no focus affordance). Separately, the field-form's vocabulary/authority pickers use a bare native<select>while the object form's equivalent reference pickers use the searchableOptionsCombobox(objects/field-input.tsx:8) — the same "pick a vocabulary/authority" action looks and behaves differently in two places.Suggested fix
ui/select.tsx(Base UI Select) that matchesInput's tokens/height/radius/focus ring, and replace all four raw selects.Source: frontend UX/design audit, 2026-06-06.
Done — merged to
main(f45f1d8).Added
web/src/components/ui/select.tsx(Base UI Select) styled to matchui/Input— sameh-8/rounded-lg/border-input/padding and a realfocus-visible:ring-3 ring-ring/50focus ring, plus disabled +aria-invalidstates. Story-validated by running.Replaced all four raw
<select>elements with it:Controller(Base UI Select isn't a native input); default stays draft.value/onValueChange; disabled-on-edit preserved; the vocabulary picker shows a placeholder until chosen (the required check still blocks submit); the authority "Any" state shows "Any" (placeholder), with an "Any" item to switch back.So in a given form, selects and Inputs now share radius/border/height/padding, and keyboard users get a focus ring on every control.
Implementation notes:
<SelectItem>children (incl..map()arrays), so the trigger shows the chosen label with the plain<SelectItem value="x">Label</SelectItem>API.userEvent.selectOptionsto open-trigger + click-option (Base UI Select isn't a<select>); all payload assertions unchanged.No new dependency (Base UI already present); no new i18n keys; en/sv parity; 204 tests green; typecheck/lint/build/check:size/check:colors clean; no codename.
Follow-ups (out of scope): a searchable vocabulary picker (generalize
OptionsComboboxto{id,label}) if vocab lists grow; the remaining non-target<select>(objects-table page-size picker); a lint guard banning raw<select>outsidecomponents/ui/.