📋 v0.19.0 — Data Display
What's New
TableComponent
A structured data table covering the most common patterns out of the box. Define columns with a block-based DSL, optionally sort them, select rows via checkboxes, pin a sticky header, and display empty or loading states — all without writing custom HTML.
<%= midwest_table(collection: @users, selectable: true, sticky: true) do |t| %> <% t.with_column('Name', key: :name, sortable: true, sorted: table_sorted?(:name), sort_direction: table_sort_direction, sort_url: midwest_sort_url(:name)) %> <% t.with_column('Email', key: :email) %> <% t.with_pagination do %> <%= midwest_pagination(current_page: table_page, total_pages: @users.total_pages, page_url: ->(p) { midwest_page_url(p) }) %> <% end %><% end %>Midwest::TurboTable concern
Include Midwest::TurboTable in any controller and call turbo_table to wire up sort, pagination, per-page, and filter state from query params. Helper methods like table_order, table_sorted?, midwest_sort_url, and midwest_filter_form_tag keep view code clean and prevent SQL column injection via an explicit allowed_sorts: allowlist.
<% # Controller %><% class UsersController < ApplicationController %> <% include Midwest::TurboTable %> <% turbo_table :users, default_sort: :name, allowed_sorts: %i[name email created_at] %> <% def index %> <% @users = User.order(table_order).page(table_page) %> <% end %><% end %>Midwest::TablePresenter
A plain Ruby object for keeping column definitions out of controllers and views. Subclass it, declare columns in define_columns, and call table_presenter from the controller. Auto-discovery is convention-based — UsersController looks for YourApp::Tables::UsersTable automatically.
HorizontalScrollComponent
An overflow-x scrolling container that adds consistent cross-browser styling and optional fade-out shadow masks at both edges to signal hidden content. Eliminates per-project overflow-x: auto wrapper boilerplate for tag lists, image strips, action rows, and narrow-viewport tables.
<%= midwest_horizontal_scroll do %> <% # ... wide content ... %><% end %>Pass shadows: false to suppress the gradient masks.
SplitButtonComponent
A primary action button joined to a dropdown trigger in a single visual unit. Accepts label:, path:, and all standard ButtonComponent props (variant:, size:, state:, pill:, disabled:). Dropdown items are added with with_item.
<%= midwest_split_button('Save', save_path, variant: :primary) do |b| %> <% b.with_item(label: 'Save and close', path: save_and_close_path) %> <% b.with_item(label: 'Save as draft', path: draft_path) %><% end %>What Changed
ButtonComponent — notification badge slot
Add a count badge anchored to the button corner for notification counts and cart quantities. Pass a label and optional state: to with_badge.
<%= midwest_button('Messages') do |b| %> <% b.with_badge('4', state: :error) %><% end %>CarouselComponent — fade: transition variant
Pass fade: true for a cross-dissolve transition instead of the default slide. Suited for image galleries and testimonial rotators where a cut-or-dissolve reads better than a horizontal pan.
<%= midwest_carousel(fade: true) do |c| %> <% c.with_slide { image_tag photo.url } %> <% c.with_slide { image_tag photo2.url } %><% end %>TabsComponent — icon-only variant
Tabs now support an icon-only display with a visually hidden but accessible label. Pass icon_only: true on a tab and include the label for screen readers via label:.
Form::InputComponent — match: validation
Pass a CSS selector to match: pointing to another field whose value must match before the input passes HTML constraint validation. The primary use case is password-confirmation fields.
<%= midwest_input(name: "password", type: :password, label: "Password") %><%= midwest_input(name: "password_confirmation", type: :password, label: "Confirm password", match: "#password") %>Form::InputComponent — autoformat: and formatter: proc
Pass autoformat: :phone (or another registered format key) to reformat the raw value on blur. For custom patterns, supply a formatter: proc that receives the raw string and returns the display value.
<%= midwest_input(name: "phone", label: "Phone number", autoformat: :phone) %>What Was Removed
Nothing removed.
Dev Process
Midwest::TurboTable concern and TablePresenter class
Two new server-side collaborators ship alongside TableComponent. TurboTable keeps sort/page/filter parameter wrangling out of controller actions; TablePresenter gives projects a conventional location for column definitions and scoping logic. See the new Rails Features docs for setup guides: Turbo Table and Table Presenter.
Code coverage
SimpleCov is now wired into the test suite and a coverage badge has been added to the README. Ensures new component code is tracked from the start.
Pre-commit lint hook
A Lefthook pre-commit hook runs RuboCop on staged .rb files before each commit, keeping lint debt from accumulating between CI runs.
CI: system tests suspended
System (browser) tests are temporarily disabled in CI to unblock the release pipeline while the Playwright/Chrome setup is stabilised on the CI runner. Unit tests continue to run on every push.
Expanded test coverage
New and updated test files shipping in this release:
TableComponentTest— column rendering, sortable headers, selectable rows, sticky mode, striped rows, empty and loading states, pagination slot.TableColumnComponentTest— label, key, sortable flag, sort direction toggles.BaseComponentTest— shared utility coverage.Midwest::TurboTableTest— sort param validation, direction toggling, page/per-page handling, filter allowlist, URL helpers, SQL injection guard.Midwest::TablePresenterTest— column DSL, scope wiring, auto-discovery.CarouselComponentTest— fade option added.TabComponentTest— icon-only variant.Form::InputComponentTest— match validation, autoformat behavior.