π v0.27.5 β Framed Cards & Time-Only Tiles
CardComponent learns two Turbo-era tricks: a frame: option that wraps the card (or each face of a flippable card) in a <turbo-frame> so a response can swap its contents in place, and a max_height: cap that lets the card's section scroll while its header and footer stay pinned. CalendarDateComponent gains time-only tiles for ranges inside a day the reader already knows. And a form-field name bug is fixed: standalone fields given an explicit name: with no form now actually submit their value.
What's New
Framed cards β swap a card's contents without losing its state
midwest_card(frame:) renders the wrapper that holds the card's faces as a <turbo-frame> instead of a <div>, so a Turbo response can replace what's inside the card while the card element itself stays put. That matters most for a flippable card: the flipped class and the Stimulus controller live on the <article>, so replacing the card outright would discard the flip state mid-interaction. Framing the faces instead lets a form on the back save while the front updates underneath it.
A flippable card frames each face separately, so a response can address one without disturbing the other β updating the front leaves the back's form exactly as the reader left it (typed values, scroll position, rich-text editor). A card with no back has no faces to separate, so it frames the wrapper.
<%# Derive the frame id from the card's own idβ¦ %><%= midwest_card(id: "lunch", frame: true) do |card| %> <% card.with_back_content do %>β¦formβ¦<% end %><% end %><%# β¦or name the frame directly %><%= midwest_card(frame: "lunch-editor") %>Because the server has to name these frames to address them β a controller answering a failed save, a broadcast replacing one face β the derivation lives in one place, Midwest::CardComponent.frame_id_for(base, face), so a response can build the same id (front, back, or the bare wrapper) without a card instance on hand. A companion returnToFront Stimulus action flips a saved card back to its front on turbo:frame-render, and leaves it on the back when the response comes back carrying validation errors.
Capped cards β a scrolling section inside a fixed height
midwest_card(max_height:) caps the card and lets each face's section scroll inside it, so a long body β or a flippable card whose back holds a form β can't grow without bound. Only the section scrolls; the header and footer are pinned by the card's own grid, so a heading stays readable and a Save button stays reachable however long the content is. A Numeric is treated as px; a String passes through, so any CSS length works:
<%= midwest_card(max_height: 400) %><%= midwest_card(max_height: "24rem") %><%= midwest_card(max_height: "70vh") %>Time-only calendar tiles
midwest_calendar_date(time_only:) drops the date half of the tile, leaving just the time β handy for a range inside a single day the reader already knows the date of (a 9:00 β 17:00 event block). The flag implies the time text, so it needs no show_time: alongside it, and it still cooperates with clock:, size:, and ranges. A value carrying no time of day is ignored rather than rendering an empty tile.
<%= midwest_calendar_date( start_date: Time.new(2026, 6, 24, 9, 0), end_date: Time.new(2026, 6, 24, 17, 0), time_only: true ) %>What Changed
Standalone form fields honour an explicit name: again
A form field constructed with an explicit name: but no form (Midwest::Form::SwitchComponent.new(name: "blank_schedule", β¦)) was silently rendering an input with no name attribute, so it submitted nothing. A local inside generated_name shadowed the name: option, and with no form to derive a name from there was nothing to fall back to. The option now wins when present, so standalone fields submit their value β and their hidden unchecked-value companion β as intended.
What Was Removed
Nothing was removed. No component options, slots, or helpers were dropped, and there are no breaking API changes β frame: and max_height: on cards and time_only: on calendar tiles all default to their previous behaviour (false / nil), so an app that upgrades and changes nothing renders exactly as it did on v0.27.4.
Dev Process
- Roadmap β the card and calendar refinements weren't scoped roadmap items; like
Form::RichTextComponentand the email-branding work before them, they land in the v0.27 line as net-new additions. The v0.27 milestone note records that v0.27.5 delivered framed cards, capped card heights, time-only calendar tiles, and the standalone form-field name fix. - Tests β new
CardComponentcoverage for framing (per-face vs wrapper frames,frame_id_for, theframe: true-without-id:guard) and themax_heightcap; newCalendarDateComponentcoverage for time-only tiles across sizes, ranges, and the clock; andSwitchComponenttests asserting a standalonename:reaches both the input and its hidden unchecked-value field. - Previews β
CalendarDateComponentPreviewgains a Time Only preview showing single, range, clock, and sized time-only tiles. - Version bumps β the gem, the
@unabridged/midwestnpm package, and the@unabridged/midwest-language-serverLSP package are all bumped to0.27.5.