## data_table (`poetry_data_table`)

A table with sorting, row selection, and sticky headers.

Class: Poetry::Ui::DataTable::Component - BEM block `poetry-ui-data_table`.
Slot REQUIRED: with_column (at least one column) - a call without it raises.
- `caption:` (string) - The table's accessible purpose, rendered as its <caption>.
- `container_class:` (string) - Caps the scroll container's height (e.g. "max-h-96") - without a cap the sticky header has nothing to stick inside.
- `empty_text:` (string) - default "No results." - Shown in a full-width row when rows are empty.
- `filter:` (boolean) - default true - Renders the filter form; false drops the toolbar row.
- `filter_label:` (string) - default "Filter" - The filter input's accessible label.
- `filter_name:` (string) - default "q" - The query-param key the filter submits under.
- `filter_placeholder:` (string) - default "Filter…" - The filter input's placeholder text.
- `frame:` (string) - Wrap in a <turbo-frame data-turbo-action="advance"> so hosts with Turbo scope the round trip to the table while the URL still advances. The host response must render the same frame id.
- `scroll_label:` (string) - Accessible name for the sticky scroll region; falls back to caption:.
- `selectable:` () - Row selection: a lambda mapping each row to its id turns the feature ON - a leading checkbox column (select-all with a real indeterminate middle state, shift ranges, count announcements) whose checkboxes ARE the form value (selection_name[], plain checkboxes with no JS). Pair with the action-bar block for bulk actions.
- `selection_name:` (string) - default "selected_ids" - The checkbox field name; selected row ids post as selection_name[].
- `sticky_header:` (boolean) - default false - Forwarded to the inner Table: sticky_header pins the thead while the table's scroll container scrolls; container_class caps that container's height ("max-h-96") - without a cap nothing sticks. The sticky scroll region needs an accessible name (the ScrollArea rule); scroll_label: falls back to caption:.
Slots: columns (Columns are DECLARED here and rendered per row by the template. A sortable column's key must be in the state's whitelist - catching drift between the view's columns and the controller's sortable: list at render, not as a silently unsortable header.; many; with_column REQUIRES a content block (the cell renderer - { |row| ... })).
- PART `data-table` - Root surface - toolbar, table, and pagination footer stack here
- PART `data-table-toolbar` - The row above the table holding the filter form - renders unless filter: false
- PART `data-table-filter` - The GET filter form (role=search) - hidden fields carry the current sort; a new filter resets the page
- PART `table-container` - The composed Table's scroll container - Table renders it, this surface owns where it sits
- PART `data-table-footer` - The Pagination row - renders when total: is more than one page
In blocks: `action-bar` - for a screen, start from the block (MCP compose/describe_block, or `bin/rails g poetry:block`), not from scratch.
- WIRING root (if selectable?): `poetry--core--table-selection` registers; values label
- WIRING select_all: `poetry--core--table-selection` actions toggleAll on change; targets all
- WIRING row_checkbox: `poetry--core--table-selection` actions press on pointerdown/keydown, toggled on change
- RULE: Build State.from_params(params, sortable: [...]) in the controller - NEVER order by raw params; the whitelist is what makes state.order_clause injection-safe.
- RULE: Column cell blocks RETURN the cell content ({ |row| row.title }) - they must not write to the template buffer.
- RULE: Sort/filter/page are URL state over GET links and a GET form. Row mutations (inline edit, row actions) belong to poetry-reactive components rendered inside cells - never to this component.
- RULE: Give the table a caption: - it is the table's accessible purpose.
