## select (`poetry_select`)

A dropdown for choosing one option from a list.

Class: Poetry::Ui::Select::Component - BEM block `poetry-ui-select`.
Slot REQUIRED: with_item (at least one item) - a call without it raises.
- `align:` (symbol) - one of start|center|end, default "start" - Popup alignment along the chosen side's edge.
- `align_item_with_trigger:` (boolean) - default false - Opens the popup OVER the trigger with the selected item aligned on it (native-select feel); falls back to regular below-the-trigger positioning on touch, viewport-edge triggers, or squeezed heights.
- `align_offset:` (integer) - default 0 - Pixel shift along the alignment axis.
- `avoid_collisions:` (boolean) - default true - Flips/shifts the popup to keep it inside the viewport.
- `dir:` (symbol) - one of ltr|rtl - Text direction for the select and its popup.
- `disabled:` (boolean) - default false - Disables the trigger and the hidden native <select>.
- `id:` (string) - The trigger's DOM id - what a Field label's for: must point at; giving one also satisfies the accessible-name requirement.
- `loop:` (boolean) - default false - Arrow-key navigation wraps from the last option back to the first.
- `modal:` (boolean) - default true - While open, blocks pointer interaction outside the popup.
- `name:` (string) - The form field name, carried by the hidden native <select>.
- `open:` (boolean) - default false - Server-renders the popup already open.
- `placeholder:` (string) - Text shown in the trigger until an option is committed; also rendered as the blank native <option> (posts "" when untouched).
- `required:` (boolean) - default false - Marks the hidden native <select> required - native constraint validation blocks submission while unset.
- `side:` (symbol) - one of top|right|bottom|left, default "bottom" - Preferred popup side relative to the trigger.
- `side_offset:` (integer) - default 4 - Gap in pixels between trigger and popup.
- `size:` (symbol) - one of sm|default, default "default" - The trigger size axis.
- `trigger_class:` (string) - Extra classes merged onto the trigger button (e.g. w-full over the base w-fit); class: styles the root wrapper instead.
- `value:` (string) - The committed option value - the item whose value: matches renders as selected and its label fills the trigger.
Slots: trigger (Optional custom trigger content rendered BEFORE the value span (rare); the component owns role=combobox + the aria wiring + the chevron regardless, so composition cannot drop the contract.), items (The option UNION: item | group (label + items) | separator - one ordered collection (interleaving preserved; items and groups are part COMPONENTS so option registration follows render/DOM order). Scroll buttons, the viewport, and the native select are component-owned anatomy, never caller-placed.; many; types item|group|separator - one with_<type> setter each, options as keywords).
- PART `select` - Root wrapper carrying both controllers (select + popper) and the optional dir attribute
- PART `select-native` - The visually-hidden native <select> - the serialization truth (name/required/disabled + every option); plumbing, never styled or targeted
- PART `select-trigger` - The role=combobox button the field label reaches - the value display and chevron ride inside | states: data-size=sm|default (always - the resolved size variant); data-placeholder (no option is committed (bare; the controller toggles it on every commit)); data-popup-open (the popup is open (bare while open, absent while closed - the controller flips it with the open state))
- PART `select-value` - The value display span - the selected option's label, or the placeholder | states: data-placeholder (placeholder: is given - carries the placeholder text so a later clear can restore it)
- PART `select-content` - The popper-positioned popup shell (scroll buttons + viewport) - open/closed and the resolved placement ride here | states: data-open (popup is open (the controller flips the pair at runtime)); data-closed (popup is closed or animating out (the server-rendered state)); data-side=top|right|bottom|left (the placement side - server-rendered from side:, rewritten to the resolved side by popper on open); data-align=start|center|end (the placement alignment - server-rendered from align:, rewritten by popper on open) | vars: --transform-origin (popper - the animation origin matching the resolved placement); --available-width (popper - viewport space available to the popup post-flip); --available-height (popper - viewport space available to the popup post-flip); --anchor-width (popper - the trigger's measured width); --anchor-height (popper - the trigger's measured height); --radix-select-trigger-width (the select controller measures the trigger on open - the viewport's min-width binding); --radix-select-trigger-height (the select controller measures the trigger on open - the viewport's MINIMUM-height binding (a hard height collapses the popup to the trigger; the list grows past it))
- PART `select-scroll-up-button` - Hover-scroll affordance above the viewport - rendered always but hidden; the controller unhides it per scroll extremes (aria-hidden)
- PART `select-scroll-down-button` - Hover-scroll affordance below the viewport (the same contract as the up button)
- PART `select-viewport` - The role=listbox scroll container - the options' actual parent, labelled from the trigger
- PART `select-group` - role=group wrapper labelled by its select-label heading
- PART `select-label` - The group heading - styled, no ARIA role (the group points at it via aria-labelledby)
- PART `select-item` - One role=option div - selection, disablement, and the committable value ride here | states: data-value (always - the option's committable value (the native <option> twin)); data-selected (the option is committed (bare; absent while unselected - the controller twin-writes it with aria-selected)); data-disabled (disabled: is set (aria-disabled rides along))
- PART `select-item-indicator` - The check gutter - server-rendered always; the parent item's data-selected absence hides it
- PART `select-item-text` - The option's label span - the value display copies from it
- PART `select-separator` - Decorative divider between options (aria-hidden)
- WIRING root: `poetry--core--select` registers; values open, value, modal, loop, align_item_with_trigger | `poetry--core--popper` registers; values side, align, side_offset, align_offset, avoid_collisions
- WIRING trigger: `poetry--core--select` actions toggle on click, triggerKeydown on keydown | `poetry--core--popper` targets anchor
- WIRING content: `poetry--core--popper` targets content
- WIRING item: `poetry--core--select` actions commit on click
- WIRING native: `poetry--core--select` actions nativeChanged on change
- WIRING viewport: `poetry--core--select` actions syncScrollButtons on scroll
- WIRING scroll_button: `poetry--core--select` actions scrollHoldStart on pointerenter, scrollHoldStop on pointerleave
- RULE: Use poetry_select (f.poetry_select in forms) - never hand-roll role=listbox popups, and never fake a select with DropdownMenu radio items bound to a hidden field.
- RULE: Options are VALUES. If activating an option should DO something beyond setting a value, it's a DropdownMenu item.
- RULE: In forms, ALWAYS go through f.poetry_select - it wires name/id/value/errors/required; bare poetry_select in a form is a smell.
- RULE: Every Select MUST be named: a Field label (id: + label[for]) or aria-label. A bare unnamed select fails at render - do not suppress it.
- RULE: NEVER write aria-selected without its data-selected twin, and NEVER write the display text without writing the native select's value first - the controller does all three; agents patching DOM must too.
- RULE: Do not put interactive elements inside options (an option IS the interactive unit).
- RULE: Long/filterable/async lists or multi-select -> Combobox, not a 50-option Select; 2-4 options -> RadioGroup.
- RULE: The hidden native select is plumbing - never target it with styles, labels, or Capybara selectors (drive the combobox like a user).
- RULE: Positioning is popper-only: poetry Select drops below the trigger (the item-aligned overlay mode is not ported - a documented parity delta).
