πΊοΈ Roadmap
This roadmap groups items from the Future Plans reference into versioned milestones. Each minor release targets 2β3 new components and 5β6 improvements to existing ones. Ordering reflects priority and implementation dependency, not a delivery commitment.
v0.25 β Motion, Maps, Audio & Animation
Parallax effects, maps, web audio, and motion system.
Complete. Most of this milestone landed ahead of schedule in the v0.24.x line β Motion Primitives (
MotionComponent) plus the five motion-consistency improvements below shipped in v0.24.1, and Maps, Geocoding, Address Autocomplete, and Page Transitions shipped in v0.24.4. The remaining items β theget_motion_systemandget_performance_characteristicsMCP tools, the static AST/dependency-graph export (bin/ast), theget_bundle_analysistool pulled forward from v0.26, and theFormComponentautosave/ajax/close-on-success behaviors β all shipped in v0.25.0. Parallax, Web Audio, and the Animation Controller/API were cut from the milestone (see below).
New Components
Social Preview: Threads, Blueskyβ β Newer text-based social platform previews extending theSocialPreview::BasePlatformComponentpattern.ThreadsComponent(wordmark header, thread-line option, reply/repost/like/send actions, optional engagement summary) andBlueskyComponent(sky-blue wordmark, inline per-action counts, link preview card, quote-post support). Both wired into the parent orchestrator (PLATFORM_MAP) and Lookbook previewsParallaxβ β Cut. Scroll-driven parallax de-scoped from the pre-1.0 roadmapMap & Geocodingβ (v0.24.4) β Interactive maps with three interchangeable providers.MapComponentrenders via Leaflet (open-source, no key), Apple MapKit JS, or Google Maps, selected once throughMidwest.configure; nothing is loaded until a provider is set (privacy-first default). Four map types (:muteddefault,:standard,:hybrid,:satellite), auto/light/dark schemes, custom height, marker annotations (Midwest::Map::MarkerDataor plain hashes), zoom/compass/user-location controls, callouts that open on load, and an accessible placeholder before init. MapKit auth viatoken:/token_url:;Midwest::MapKit.token(team_id:, key_id:, private_key:)generates ES256 JWTs andMidwest::MapKit::Authenticatableprovides a self-refreshing endpoint. Also shipsForm::AddressComponent(autocomplete with hidden lat/lng/place-id fields, optional inline map preview) and theMidwest::Geocodablemodel concern withNominatim/MapKit/Googlebackends β the geocoder is configurable independently of the map renderer. Module-level CDN script deduplication prevents duplicate script injection when several maps mount at onceWeb Audioβ β Cut. AudioContext suite de-scoped from the pre-1.0 roadmapMotion Primitivesβ β Native, zero-dependency reveal animations.MotionComponentwraps content and animates it in with one or more of five primitives (animation:takes a single value or an array βfade/slide/scale/rotate/blur, withdirection:for slides); each primitive drives an independent CSS property (opacity, the individualtranslate/scale/rotatetransform properties, andfilter) so compounds like[:slide, :blur]layer cleanly; blur is a focus-in via animatablefilter: blur(); a smallmidwest-motionStimulus controller flips a singledata-motion-visibleattribute that CSS transitions animate against, driven by the shared motion tokens (duration:/easing:resolve--duration-*/--ease-*, or take raw values).trigger: :reveal(default) animates in on scroll-into-view via amidwest-motionStimulus controller (threshold:,once:to replay on re-entry) that flips adata-motion-visibleattribute; its hidden state is gated behind@media (scripting: enabled) and (prefers-reduced-motion: no-preference)so no-JS visitors see content with no flash.trigger: :loadneeds no controller at all β it animates in once on first render via a pure-CSS@starting-styleentry (reduced-motion-gated). Both triggers share one set of--motion-from-*custom properties for the hidden state, so the reveal and load paths never drift.delay:/distance:round it out. Wired into themidwest_motionhelper and Lookbook previewsPage Transitionsβ β Turbo navigation transitions via the View Transitions API.PageTransitionComponentplaces once in the root layout body; a Stimulus controller injects the<meta name="view-transition" content="same-origin">tag Turbo 8 requires and setsdata-midwest-page-transitionon<html>to scope CSS animations. Five styles:fade(default),slide(withdirection: :left/:right/:up/:down),scale, andnone. All respectprefers-reduced-motion. Duration and easing resolve through the motion token system.midwest_page_transition_nameview helper setsview-transition-namefor element-level morphing
MCP Server Features
Motion System Docsβ βget_motion_systemMCP tool documents the motion system holistically: the standardized duration/easing scales (token-derived, with what each step is for), the slide-distance scale, the animation/direction/trigger vocabulary, the library-wide prefers-reduced-motion/no-JS contract, and how app-wide defaults are configured and resolved. Sectioned (strategy/durations/easings/distances/vocabulary/reduced_motion/configuration); complements per-componentget_animation_infoand raw-valueget_design_tokens. MCP server now exposes 19 toolsPerformance Characteristicsβ βget_performance_characteristicsMCP tool documents each component's rendering-cost tier (light/moderate/heavy) and dataset limits. Structural signals are derived from the code so they never drift (renders a collection viarenders_many, ships a Stimulus controller, has looping keyframe motion); a curateddata/performance.ymloverlay adds recommended dataset sizes and scaling strategies (pagination, infinite scroll, lazy loading). Two modes: a per-componentprofile, or a library-wide overview ranking the dataset-sensitive components by tier. Derived cost caps atmoderateβheavyis a curated judgement call, since structure alone can't tell a 4-step wizard from a 10k-row table. MCP server now exposes 20 toolsStatic AST markdown/json files scriptβ (pulled forward from v0.26) βbin/astgenerates static per-component AST files (options, slots, constants, methods, Stimulus values/targets/actions) plus a whole-library dependency graph β which components render which, from every render syntax: typed slots (renders_* :x, Midwest::YComponent),midwest_*helper calls in the ERB, directrender Midwest::YComponent.newin the ERB, andMidwest::YComponent.newconstructions in the.rbdefinition (slot lambdas, helper methods) β with the inverse used-by. The Ruby side is parsed with Prism (a syntax-tree walk for.newon aMidwest::β¦Componentconstant, plusmidwest_*helper calls β includinghelpers.midwest_*β "unfurled" to the component each helper renders), so it's immune to class names in comments, strings,is_a?checks, or< Superclassinheritance β no regex, no false edges. Template comments are stripped before scanning too, so a helper merely named in an ERB doc comment isn't mistaken for a render. This render graph lives inComponentIndex(exposed asrender_edges_for), so theget_component_relationshipsMCP tool now reports the same complete set instead of justmidwest_*helpers. Each component gets a<name>.ast.jsonand a<name>.ast.md(the latter embeds a Mermaidgraph TDvisualizing its render edges, with the component itself highlighted) written into its own sidecar directory (so they travel with the component in the RubyGem viaapp/**and never reach the npm package, which ships onlydist/**+app/assets/**); the aggregategraph.json,index.json, andDEPENDENCIES.mdland in a top-levelast/directory (shipped with the gem, not npm). Everything is derived from the same parsers the MCP server uses, so the files never drift; Tailwind@source notrules keep the JSON out of content scanning. Regenerated automatically bybin/buildand staged bybin/releaseon every version cut. Logic lives in a testableMidwest::MCP::AstExporter;bin/astalso hassummary(default) andgraphcommands. Backs documentation and the future bundle-analyzer / dependency-graph MCP featuresAnimation APIβ β Cut. Documented the cancelled Animation Controller; de-scoped with it
Improvements
Motion Systemβ β Shipped with the v0.24.1 motion work: the standardized--duration-fast/base/slow/slowerand--ease-standard/in/out/emphasizedscales (tokens.css), theMidwest::MotionkeywordβCSS vocabulary, theMotionableconcern that gives every motion surface the same options, and app-wide defaults viaMidwest.configureβ--motion-default-*Animation Controllerβ β Cut. Runtime play/pause/seek transport controls de-scoped from the pre-1.0 roadmapβTooltipComponentβ entrance driven by the shared motion system (animation:/direction:/duration:/distance:, config-aware defaults)βDropdownComponentβ desktop menu entrance driven by the shared motion system (rendered throughmidwest_motion,trigger: :hover); mobile keeps its bespoke bottom-sheet. Applies to everyDropdownMenuComponentconsumer (dropdown, breadcrumb dropdown, split button, nested submenus)βPasswordRequirementsComponentβ the anchored (popover) panel's entrance driven by the shared motion system (trigger: :hover); inline/static modes unchangedβForm::AutocompleteComponentβ desktop dropdown entrance driven by the shared motion system via an opt-inmotion:flag on the sharedPopoverComponent(trigger: :hover); mobile bottom-sheet unchanged. Other popover consumers (date/time pickers, confirmation) keep the flag offβDialogComponentβ the first nested element (.dialog-content) is rendered throughmidwest_motion(trigger: :load), so the inner content animates in on each open via the shared motion system; the native<dialog>keeps backdrop + top-layer + focus, and the panel variant keeps its right-edge slideβ βFormComponentmidwest_form_withgains three opt-in behaviors driven by themidwest-formStimulus controller:autosave:(debounced background submit on field change βtruefor the 600 ms default or a number of ms),ajax:(force a Turbo background submission and emitmidwest-form:success/midwest-form:errorevents), andclose_modal_on_success:(reuses the dialog controller'shideaction to dismiss an enclosingmidwest_dialogon a successfulturbo:submit-end). See Form Behaviors
v0.26 β grab-bag of remaining pre-1.0 work
A grab-bag of remaining pre-1.0 work.
Complete. Everything in this milestone shipped in v0.26.0. The four New Components (
ContextMenuComponent,MasonryComponent,SplitViewComponent, andSocialPreview::MastodonComponent) landed alongside theSocialPreview::BasePlatformComponentrefactor; the three MCP-server tools (get_chart_relationships,get_chart_data_requirements,get_turbo_patterns) brought the server to 24 tools; and the improvements β the eject-component task,ChartComponentexport,TableComponentcolumn resizing / inline editing / multi-column sort, history-aware Hotwire modals, and theAssetsComponent/midwest_assetsTurbo asset loader β all shipped. The Bundle Analyzer was pulled forward and shipped earlier in v0.25.0, andTableComponent's advanced filtering was cut (see below).
New Components
Social Preview: Mastodonβ β Mastodon (fediverse) post preview extending the newSocialPreview::BasePlatformComponent.MastodonComponent: white card with the purple Mastodon wordmark header, rounded-square avatar in a side-by-side layout, an optional "X boosted" attribution row, a zero-JS native content-warning disclosure (spoiler text in a<summary>with a Show more/less toggle; the post body stays hidden until expanded), and a five-button action bar (reply/boost/favourite/bookmark/share) with inline reply/boost/favourite counts. No verification badge (Mastodon has none by design); counts accept raw integers (abbreviated viaformat_count) or pre-formatted strings. Wired into the parent orchestrator (PLATFORM_MAP/PLATFORM_OPTIONS) and Lookbook previewsContext Menuβ β Right-click (context) menu.ContextMenuComponentwraps a trigger area; right-clicking anywhere inside it opens a floating menu at the cursor, rendered in the top layer via the native Popover API so it escapes overflow/stacking contexts. Themidwest-context-menuStimulus controller positions and clamps the menu to the viewport and dismisses it on Escape, outside click, scroll, or resize. Shares the dropdown's shell and rows β items are supplied through theitemsslot asDropdownItemComponents (the same link/button/header/divider/nestedas:types, tooltips, confirmation dialogs, and one level of nested submenu). Entrance composed throughmidwest_motion(trigger: :hover, defaultscaleprimitive), so it draws on the same motion vocabulary, tokens, and app-wideMidwest.config.motiondefaults as the dropdown/tooltip/popoverMasonry Gridβ β Pinterest-style layout, zero JavaScript.MasonryComponentuses the native CSS masonry syntax where supported (shortest-lane placement in true left-to-right reading order), progressively enhances two experimental syntaxes (Firefox'sgrid-template-rows: masonryand WebKit'sdisplay: grid-lanes), and falls back to CSS multi-column layout everywhere else. Responsive column count (columns:, clamped 1β8, stepping down to 2 then 1 on narrower viewports) and configurablegap:, driven by--masonry-*custom properties. Items supplied through theitemsslotSplit Viewβ β Two resizable panes separated by a draggable divider.SplitViewComponentexposesprimary/secondaryslots; the primary pane's size is adjustable by dragging or with the arrow keys when the divider is focused, and β when astorage_key:is given β persisted to localStorage so it survives reloads. Double-clicking the divider resets to the initial size. Sizes are percentages (size:/min:/max:);orientation::horizontal(default, vertical divider) or:vertical(stacked, horizontal divider), with correct inverse ARIAaria-orientation. Themidwest-split-viewStimulus controller handles dragging, clamping, keyboard resizing, and persistence
MCP Server Features
Chart Relationship Graphβ βget_chart_relationshipsMCP tool documents how the chart subsystem is wired: theChartComponentfacade and its chart-type β chart-class map, theBaseChart β CartesianChart βconcrete-type inheritance, theSupportvalue objects (Series,DataPoint,SeriesBuilder,Geometry, config structs), the data-flow pipeline from a rawseries:array to inline SVG, and how rendering/styling work. Sectioned (overview/types/hierarchy/support/data_flow/rendering/related_tools); the chart-specific companion toget_component_relationships. A drift-guard test keeps its type map in sync withChartComponent::CHART_CLASS_MAPData Requirement Docsβ βget_chart_data_requirementsMCP tool documents theseries:data format each chart type expects β array shape, required keys, a worked example, and the most relevant options β for pie, column/bar, line/spline/area, stackedbar/stackedarea, and bubble, plus the sharedseries_from:collection builder. Scope withtype:or omit for all; drift-guarded againstCHART_CLASS_MAPTurbo Integration Patternsβ βget_turbo_patternsMCP tool documents how Midwest works with Hotwire Turbo: the Frame-driven pieces (remote + history-aware dialogs, theTurboTablesort/filter/paginate frame, command-palette and autocomplete frames), the Stream update patterns (the notifications container and single-row table updates via the sharedTableRowComponent), how Turbo Stream broadcasts over a channel plug in (the gem ships none of its own), dynamic per-arrival asset loading (midwest_assets), and the table-specific parsing gotchas. Sectioned (overview/frames/streams/channels/assets/gotchas/related_tools). MCP server now exposes 24 toolsBundle Analyzerβ (v0.25.0, pulled forward) βget_bundle_analysisMCP tool: the transitive set of components a given component renders (via slots, template helpers, direct renders, and Ruby constructions β from the Prism-backed render graph), the summed asset footprint of that set (co-located CSS + Stimulus controller source, with a per-component breakdown), and render-cycle detection. Built onComponentIndex#render_targets/#source_bytes_for. MCP server now exposes 21 tools. A CI drift-guard (bin/ast build+git statuscheck) keeps the committed AST/graph files from going stale between releases
Improvements
β βEject Component Toolbin/rails "midwest:eject:component[TableComponent]"copies a component's.rb+ sidecar (.html.erb/.css/_controller.ts/locale.yml) out of the gem and into the consuming app'sapp/components/midwest/, where Rails' app-before-engine load order makes the local copy override the gem's. Accepts any name form (table,table_component,Midwest::TableComponent,social_preview/mastodon), skips generated.ast.*sidecars, refuses to clobber existing files without[name,force], and prints next-steps guidance for wiring the ejected CSS/JS. Lives undernamespace :ejectinlib/tasks/midwest_tasks.rakeβ β ExtractedSocialPreview ComponentSocialPreview::BasePlatformComponentto share the common shell and header design, and migrated all ten platform previews (Facebook, Twitter/X, LinkedIn, Instagram, TikTok, YouTube, Threads, Bluesky, and the new Mastodon) onto it β not just Threads/Bluesky. Shell and header updated to match current platform brandingβ β Opt-inChartComponentβ export functionalityexport:toolbar (revealed on hover / keyboard focus) for downloading the chart, native and zero-dependency.export: trueoffers all formats; a symbol or array (%i[png csv]) narrows them. PNG and SVG rasterize the live inline<svg>client-side β the Stimulus controller clones it, inlines every node's computed presentation styles (so exports render faithfully without the component stylesheet), serializes to standalone XML, and either downloads it or paints it onto a 2Γ canvas for PNG. CSV serializes the new publicchart.data_table(categories Γ series for cartesian charts, flat Label/Value for pie β both pie input shapes collapse correctly), emitted into a hiddendata-element and turned into an Excel-friendly BOM-prefixed CSV. Suppressed for sparklines and empty charts; filename derives from the titleβ (advanced filtering cut):TableComponentβ Inline editing, column resizing, multi-sortColumn resizingβ βresizable: truelets you drag column borders (a handle appears on each header's right edge); the controller freezes the current widths, switches the table to a fixed layout so a drag doesn't reflow the other columns, clamps to a minimum width, double-click-to-auto-size, and β withresize_storage_key:β persists widths to localStorage across reloadsInline cell editingβ β mark a columneditable: trueto edit its key-based cells in place: double-click (or focus + Enter/F2) swaps the cell for an input; Enter commits, Escape cancels, blur commits. Committing dispatches a bubblingmidwest-table:editevent ({ field, value, previousValue, rowId }) and, when the column suppliesedit_url:(String or callable), PATCHes the value with the CSRF token (rendering any Turbo Stream response). Both covered by browser system testsMulti-column sortβ β click a header to sort by it, Shift-click to add it as a secondary sort. The chain is URL-encoded as parallel comma-separated lists (?sort=role,name&dir=desc,asc) and drives a multi-columnORDER BY; each sorted column shows a priority badge (1, 2, 3β¦). Shared, security-hardened parsing lives inMidwest::TableSortable(validates every column against the safe pattern +allowed_sorts:allowlist, de-dupes, honors a baredir) and backs both theTurboTablecontroller concern andTablePresenterβ somidwest_table_fromwires it automatically; single-column sort URLs stay backward-compatible. Sortable headers carry adata-multi-sort-url; the table controller'ssortColumnfollows it (frame-scoped) only on Shift-click, letting a plain click use the single-sort href. Each column cycles add β reverse β remove on repeated Shift-clicks. Covered by shared-module, presenter, controller, component, and browser (plain- vs shift-click) testsAdvanced filteringβ β Cut. Filter operators / typed filters / a filter-bar UI de-scoped; the existing param-basedfilter_paramsfiltering (viaTurboTable/TablePresenter+midwest_filter_form_tag) remains the supported approach
Hotwire Modalsβ β Modal frame navigation with browser history, built on the existing remote (turbo-frame)DialogComponent. Opt in withremote: true, history: true(ormidwest_remote_dialog(history: true)): the persistent turbo-frame gainsdata-turbo-action="advance", so loading content into it advances the URL and pushes a history entry (the modal URL is deep-linkable and shown in the address bar while open). The dialog controller closes the modal on Back (popstate) and, on a manual close (Escape/backdrop/close button), pops the modal's URL off the stack viahistory.back()so the address bar returns to the page behind it β while a form redirect still supersedes history restoration. Demonstrated by a dedicatedexample_historydialogable endpoint and covered by browser system tests (open advances URL, Back closes + restores, Escape closes + restores)Enqueuing a component's CSS and JS via Turbo when a component is loadedβ β NewAssetsComponent/midwest_assetshelper: render it alongside markup that may be delivered dynamically (into a Turbo Frame or appended by a Turbo Stream) and itsmidwest-assetsStimulus controller injects Midwest's compiled stylesheet (and, opt-in, its JS bundle) into<head>on connect β once, idempotent by resolved URL β so the component styles and behaves correctly even on a page that never loaded Midwest's assets.css: trueresolves the app's compiledmidweststylesheet (gracefully no-ops if the asset isn't in the pipeline); a string URL is used verbatim. Because Midwest ships a single bundled stylesheet this ensures that one file rather than per-component CSS. Covered by component tests and a browser system test asserting the<link>is injected into the head
v0.27 β Async assets & Email Sections
Per-component asset delivery, plus a mail-client-safe email component library.
Complete. Everything in this milestone shipped in v0.27.0. The async-asset work landed in full (per-component CSS/JS builds, render-time accounting, Turbo delivery, Rack serving/packaging, the
@sourceglob fix, and cascade determinism), and the milestone also delivered a net-new Email Sections library (40+ mail-client-safe sections and six full transactional templates) that was not previously on the roadmap. The single bundle stays the default and stays supported. See Async Assets and Email Sections. The v0.27.x patch line also shipped a second net-new addition β theForm::RichTextComponent(an ActionText/Trix editor in Midwest field chrome), which replaced the long-standingrich_text_areaplaceholder in v0.27.3. v0.27.4 then deepened the Email Sections library's brand fit: a heading-only display font, brand type treatment (heading_transform/heading_tracking), a complement colour scale and per-role palette overrides, an opt-in permanently-darkcolor_scheme, and I18n-backed component copy (the few strings a section emits itself now translate throughmidwest.email.*, overridable per app or per call). v0.27.5 added Turbo-era refinements to two elements components: framed cards (CardComponent'sframe:option wraps the card β or each face of a flippable card β in a<turbo-frame>so a response can swap its contents in place) and amax_height:cap that scrolls a card's section while pinning its header/footer, plus time-only calendar tiles (CalendarDateComponent'stime_only:) and a fix so standalone form fields honour an explicitname:.
Email Sections
Email Sections libraryβ β A deliberately separate library underapp/emails/midwest/email/(namespaceMidwest::Email::) of drop-content-in email sections that render correctly in real mail clients. Because an email cannot use Midwest's stylesheet (no cascade, no custom properties, no@layer, no Tailwind β Outlook renders through Word, Gmail strips<head>), these components take part in no CSS build, carry no.css/.tssidecars, and emit every declaration as an inlinestyleat render time. They share the design language only β colour scales, spacing ramp, type scale β resolved to literal values via aMidwest::Email::Palettethat mapsMidwest::Themeto semantic email roles, so a singleconfig.email.themeretints a whole message. 40+ sections spanning transactional (header, hero, content, CTA, alert, verification code, details, receipt, order summary, address, status stepper, data table, stats, user, quote, attachments, social, signature) and marketing (feature grid/row, product grid, article list, testimonial, logo cloud, pricing, FAQ, survey, video, app badges, countdown) plus supplemental sections (callout, button group, bullet list, steps, coupon, event, progress, rating). Opt-in Outlook-guarded custom web fonts viaconfig.email.web_font. Not a sending library β it renders HTML; ActionMailer sends itTransactional templatesβ β Six complete emails assembled from the sections (receipt, verification code, welcome, order shipped, alerts, monthly report), each rendered as a whole document through the real ActionMailer pathEmail visual regression testingβ βbin/email-vrt --pixels(headless-Chromium screenshots diffed with ImageMagick, light + dark baselines per section), a coverage test asserting every section has a baseline, andbin/email/scripts/export_emails.rbexporting every mailer and section to self-contained.htmlat release time for manual Litmus / Email on Acid checks
Improvements
- Per-component CSS build β
β
Midwest::Assets::CssSplitter+bin/rails midwest:css:asynccompiles a lean core (76 KB) plus one stylesheet per component, from the same sources the bundle uses. The technique that makes it viable issource(none)with an explicitly scoped@source: without it Tailwind's automatic content detection scans to the git root and hands every component the whole library's utility set (~35 KB each βicondrops from 42.5 KB to 683 B once disabled). Two checks run on every build: each component's@layer midwest.components.*block compiled standalone must be byte-identical to what the bundle emits for it (re-verified unminified when it trips, since Lightning CSS folds shorthands contextually), and every utility rule the library emits must survive somewhere across core + components. Incremental: each output is keyed by a digest of everything feeding it, so a rebuild is ~3 s against ~55 s cold - Per-component JS build β
β
Midwest::Assets::JsSplitter+midwest:js:asyncemits one ES module per component owning a Stimulus controller. The identifier β controller mapping is parsed out ofindex.tsrather than restated, so the two builds cannot disagree. Rollup's code splitting puts@hotwired/stimulusin a single shared 18 KB chunk every module imports, so a component module is only its own controller βbadgeis 195 bytes. A shared runtime queues registrations that arrive before the host application boots and flushes them when it appears, adoptingwindow.Stimulusautomatically - Render-time asset accounting β
β
midwest_asset_tagsemits exactly the components a page rendered. Rails renders the view template before the layout, so the registry populated fromBaseComponent#render_inis already complete when<head>is evaluated; nested components register themselves as they render, so no dependency graph is consulted. Layout chrome rendered after<head>is named once viaconfig.assets.always, and in development anything that renders too late warns with the exact line to add.config.assets.preloadprefetches components known to arrive later over Turbo - Turbo delivery β
β
Midwest::AsyncAssetsappends a fragment's assets to the document head for Streams (via Turbo'stargetsCSS-selector form, sinceheadhas no id) and inline for Frames.AssetsComponentbecomes async-aware, injecting only what the named components need; an explicitcomponents:list is expanded through the render graph. Emitting unconditionally is safe β a duplicate<link>is a browser no-op and a duplicate module script executes once - Serving and packaging β
β artifacts are served by a Rack mount the engine installs rather than the app's asset pipeline, because Rollup emits relative imports between chunks and neither Propshaft nor Sprockets rewrites JS import specifiers. Filenames carry content hashes, so they are served
immutable;config.assets.hostpoints them at a CDN. As of v0.27.2 the ~160 hashed artifacts are committed to the repo (staged withgit add -Aon release) so a git-based install serves them without a build step;scripts/check_async_package.rbverifies on every build that everything the manifest names is on disk @sourceglob fix β β five of the seven globs inmidwest/tailwind.csshad resolved to directories that do not exist since the v4 migration (a relative@sourceresolves against the file declaring it, and these were written from the importing entry's perspective). Nothing broke, because automatic content detection is a superset β they were documentation of an intent the build ignored. Correcting them leaves the bundle byte-identical;scripts/check_css_sources.rbnow fails on any glob matching nothing- Cascade determinism β
β three components opened a
@layerno order statement declared, so they landed after every declared layer in first-seen order. Harmless in a single bundle where emission order is fixed; load-order dependent as separate<link>s. Declared explicitly, withmap_component(previously unlayered) wrapped.scripts/check_css_layers.rbenforces all three leak modes
Measured
For a page rendering twelve components: 278 KB / 60 KB gzip against the bundles' 784 KB / 133 KB β 64.5% smaller raw, 54.7% gzipped, with the core cached across pages.
Post-v0.27 β unscoped polish
Releases shipped after the last milestone closed.
No open items. The v0.25βv0.27 milestones are all complete, and the v0.28.x line is net-new polish rather than milestone delivery β nothing below was a scoped roadmap item. v0.28.0 added two bare form primitives (
midwest_checkboxandmidwest_radio, the styled input with no label or wrapper) plus usability options on three components: avatarcolor: :auto(a stable per-name colour, and a single initial atxs), a collapsible split-view primary pane, and tree nodes that can be forced to render as expandable branches and forward arbitrary HTML attributes. v0.28.1 is aBadgeComponentpass β icon-only badges (an icon with no label renders round instead of a pill with an empty tail), a retuned auto-contrast threshold so light theme colours clear AA (with the icon chip resolving against its own background rather than the pill's), cap-height vertical centring viatext-box: trim-both cap alphabetic(the previoustext-box-trimwas a silent no-op on those flex spans), a support chip that matches the icon chip's height, and a smaller1.15emicon β alongsideget_turbo_patternsguidance for opening a remote dialog from a Turbo Stream and theautoclosenavigation trap underneath it.