## questionnaire (`poetry_questionnaire`)

A one-question-at-a-time survey flow with choices, free answers, and validation.

Class: Poetry::Ui::Questionnaire::Component - BEM block `poetry-ui-questionnaire`.
- `default_item:` (string) - The initially active item by name; default is the first item.
- `http_method:` (symbol) - default "post" - The form's HTTP verb. Named http_method (not method:) - an option named `method` would shadow Object#method.
- `id:` (string) - The root form's DOM id; item element ids derive from it.
- `next_label:` (string) - default "Next" - The forward-navigation button's text.
- `previous_label:` (string) - default "Previous" - The back-navigation button's text.
- `shortcuts:` (symbol) - one of letters|numbers - nil (off), :letters (A, B, C...) or :numbers (1-9): server- rendered key labels + one-keystroke answering.
- `skip_label:` (string) - default "Skip" - The skip button's text (shown only while the active item is optional).
- `submit_label:` (string) - default "Submit" - The final submit button's text (replaces Next on the last item).
- `url:` (string) - required - The form's submit URL - answers post here as ordinary params.
Slots: progress (with_progress (bare) renders the auto "Question X of Y" text; with_progress { custom } replaces it (marked data-custom so the controller leaves it alone). class: merges onto the progress element (e.g. w-full for a full-width segment bar over the base w-fit).).
- PART `questionnaire` - The root <form> the controller drives - navigation, validation gating, and the keyboard map all ride here
- PART `questionnaire-progress` - The polite progressbar ('Question X of Y'; block content replaces the text). Always carries live data-current/data-total, and a [data-progress-count] child gets the live 'X of Y' text - custom segment bars ride data-[current=N] variants | states: data-custom (block content supplied - the controller leaves the text alone); data-current (always - the active question number (live)); data-total (always - the enabled question count (live))
- PART `questionnaire-item` - One question <fieldset> - exactly one is active | states: data-name (always - the item's param name); data-active (the visible question (inactive items are hidden + inert)); data-status=unanswered|answered|skipped (always - the answer state); data-required (required: true - Skip hides and Next validates); data-multiple (multiple: true - checkbox choices named <name>[]); data-invalid (validation attempted and unanswered - the error shows); data-validated (a navigation has demanded this answer at least once); data-skipped (explicitly skipped (cleared by any interaction))
- PART `questionnaire-title` - The question heading - a real <legend>
- PART `questionnaire-description` - Muted copy under the title; its id rides the fieldset's aria-describedby
- PART `questionnaire-choices` - The answer grid for one item
- PART `questionnaire-choice` - One answer <label> wrapping its native input | states: data-type=radio|checkbox (always - the input kind); data-checked (the choice is selected); data-unchecked (the choice is not selected); data-shortcut (shortcuts: on - the key label (A, B... or 1-9)); data-disabled (choice or item disabled)
- PART `questionnaire-choice-input` - THE native radio/checkbox - stretched invisibly over the row (paste of the input-otp posture) | states: data-checked (selected); data-unchecked (not selected)
- PART `questionnaire-choice-indicator` - The box/circle glyph (aria-hidden) - dot for radio, check for checkbox
- PART `questionnaire-choice-indicator-dot` - The radio dot (shown while checked)
- PART `questionnaire-choice-indicator-check` - The checkbox check icon (shown while checked)
- PART `questionnaire-choice-label` - The label column - answer text over the optional description
- PART `questionnaire-choice-description` - Muted copy under the answer text
- PART `questionnaire-choice-shortcut` - The key hint chip (aria-hidden; hidden unless the choice carries data-shortcut)
- PART `questionnaire-input-wrapper` - The free-text answer's positioning wrapper
- PART `questionnaire-input` - The free-text answer - a real text input named after the item | states: data-filled (has a value (counts as answered)); data-empty (blank)
- PART `questionnaire-error` - The validation message (hidden until a navigation demands the answer; role=alert while shown)
- PART `questionnaire-actions` - The navigation row - Previous / Skip / Next / Submit (the buttons ride composed Buttons, so those elements belong to Button's anatomy, not this contract; each carries data-visible/data-hidden + hidden/inert)
- WIRING root: `poetry--core--questionnaire` registers; values shortcuts (if); actions keydown on keydown, submit on submit, reset on reset, change on change, input on input
- WIRING progress: `poetry--core--questionnaire` targets progress
- WIRING previous: `poetry--core--questionnaire` actions previous; targets previous
- WIRING skip: `poetry--core--questionnaire` actions skip; targets skip
- WIRING next_button: `poetry--core--questionnaire` actions next; targets next
- WIRING submit_button: `poetry--core--questionnaire` targets submit
- RULE: The root is a REAL form (url:/method:) - answers submit as ordinary params; validate server-side and re-render invalid items with error:.
- RULE: One with_item per question (name: is the param key); choices via item.with_choice, an optional free-text answer via item.with_input.
- RULE: multiple: true renders checkboxes named <name>[] (Rails array params) - a recorded divergence from the ported source's repeated bare names.
- RULE: required: true gates Next/submit client-side; the server stays the truth on submit.
- RULE: shortcuts: :letters or :numbers labels each choice with a key (server-rendered) and enables one-keystroke answering.
- RULE: Skip renders only while the active item is optional - never force-hide it.
- RULE: with_progress { custom } replaces the readout; data-current/data-total on the progress element and a [data-progress-count] child stay live for segment bars and counters.
