Action bar
Bulk actions over a selectable table: a selection-driven floating bar with the live count, the actions, and clear - shown while rows are selected, Escape clears, focus never steals.
Installation
Blocks are copied source, not library calls — the generator writes this
exact template into app/views/blocks/
and from there it's yours to edit. The preview below renders the same
source the copy-in produces.
bin/rails g poetry:block action-bar
Default
Open Action bar standalone
<%# ActionBar is a BLOCK, not a component: the bar
is plain markup wired to two small engines - the data_table's
selectable: engine dispatches selection-change, and the bar's
controller shows/hides on it, retains the last non-zero count while
animating out, announces "Actions available." once per appearance,
and Escape anywhere inside clears the selection. The wrapper div
PAIRS them positionally (shared parent) - no id contract. Swap the
sample actions for your bulk endpoints: the checkboxes are real form
values (selected_ids[]), so a wrapping form_with posts them as-is. %>
<% invoice = Struct.new(:id, :number, :customer, :amount) %>
<% rows = local_assigns[:rows] || [
invoice.new(1, "INV-001", "Acme", "$250.00"),
invoice.new(2, "INV-002", "Globex", "$1,150.00"),
invoice.new(3, "INV-003", "Initech", "$420.00")
] %>
<% state = Poetry::Ui::DataTable::State.from_params(params, sortable: %w[number customer]) %>
<div class="relative mx-auto max-w-3xl pb-16">
<%= poetry_data_table(rows: rows, state: state, path: ->(p) { "?#{p.to_query}" },
caption: "Recent invoices.",
selectable: ->(row) { row.id }) do |table| %>
<% table.with_column("Invoice", key: :number, sortable: true) { |row| row.number } %>
<% table.with_column("Customer", key: :customer, sortable: true) { |row| row.customer } %>
<% table.with_column("Amount", classes: "text-right") { |row| row.amount } %>
<% end %>
<div hidden
data-controller="poetry--core--action-bar"
data-poetry--core--action-bar-label-value="%{count} selected"
data-available-label="Actions available."
data-action="keydown->poetry--core--action-bar#keydown"
data-slot="action-bar"
class="absolute inset-x-8 bottom-2 z-10 mx-auto flex max-w-xl items-center gap-3 rounded-lg border bg-popover px-4 py-2 text-popover-foreground shadow-lg transition-[opacity,translate] data-open:translate-y-0 data-open:opacity-100 not-data-open:translate-y-2 not-data-open:opacity-0">
<span data-slot="action-bar-count" class="text-sm font-medium tabular-nums"
data-poetry--core--action-bar-target="count"></span>
<span class="ms-auto flex items-center gap-2" data-slot="action-bar-actions" aria-label="Actions">
<%= poetry_button(variant: :outline, size: :sm) { "Export" } %>
<%= poetry_button(variant: :destructive, size: :sm) { "Archive" } %>
</span>
<%= poetry_button(variant: :ghost, size: :"icon-sm", label: "Clear selection",
"data-slot": "action-bar-clear",
data: { action: "click->poetry--core--action-bar#clear" }) do %>
<%= poetry_icon(name: :x) %>
<% end %>
</div>
</div>
Styling
A block has no styling surface of its own — every element in the copied
template is a Poetry component carrying its usual
data-slot parts, and because you own
the source, utility classes can be edited directly in place. Per-part
contracts live on the composed components' pages: