Skip to content

Bootstrap 4 switch: gem swap + theme mapping (WIP) - #5349

Draft
nimmolo wants to merge 81 commits into
mainfrom
nimmo-5348-bs4-cutover
Draft

nimmolo wants to merge 81 commits into
mainfrom
nimmo-5348-bs4-cutover

Conversation

@nimmolo

@nimmolo nimmolo commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Fixes #5348. WIP -- opened as a running record per that issue's sequencing, not ready for review. Won't be marked ready until every item in #5348 is verified live in the browser across themes.

Progress so far

  • Swapped gem("bootstrap-sass") for gem("bootstrap", "~> 4.6").
  • Re-vendored the JS via bin/importmap pin bootstrap@4.6.2 (an unpinned bin/importmap pin bootstrap resolves to BS5, which drops jQuery -- would break every .collapse()/.modal()/.tooltip() jQuery plugin call in the app's Stimulus controllers). popper.js pinned alongside it since BS4's JS imports it directly.
  • Wired mo/map_theme_to_bootstrap4.scss into the import chain ahead of @import "bootstrap", and fixed two bugs found while touching it: $gray-100/$gray-900 were hardcoded to $LIGHT_COLOR/$DARK_COLOR regardless of theme (backwards for a dark theme like Amanita) -- now derived from $BODY_BG_COLOR/$BODY_TEXT_COLOR, which already resolve light/dark per theme; $card-cap-bg used darken() instead of the project's scale-color() convention.
  • Deleted _utilities.scss's font-weight-bold/border-top/border-bottom/list-group-flush rules, duplicates of what BS4 already provides.

Finding: mo/map_theme_vars_to_bootstrap_vars.scss stays for now

The plan was to replace this bootstrap-sass-named mapping file outright. It cannot be deleted yet: about 20 of MO's stylesheets read its variables directly ($btn-default-color, $navbar-inverse-bg, $panel-default-text, $dropdown-link-hover-bg, and about 55 more names across _form_elements.scss, _left_nav.scss, _top_nav.scss, _name_lister.scss, _carousel.scss, _content.scss, and more), not just Bootstrap's component partials. Deleting it produces Sass "undefined variable" compile errors, not just visual drift. Both files are imported now: the new one for BS4-native names, the old one as a compatibility bridge for those ~20 files until each is swept to BS4-native names/mixins -- likely a separate pass within the HTML/CSS sweep step in #5348, given the size.

Test plan

article: no

nimmolo and others added 30 commits September 8, 2026 22:53
gem("bootstrap-sass") -> gem("bootstrap", "~> 4.6"), pulling in
popper_js as a dependency. Re-vendored the JS via
`bin/importmap pin bootstrap@4.6.2` (explicit version -- the unpinned
form resolves to BS5, which drops jQuery and would break every
.collapse()/.modal()/.tooltip() jQuery plugin call in the app's
Stimulus controllers); popper.js is pinned alongside it since BS4's
JS imports it directly.

mushroom_observer.scss now imports mo/map_theme_to_bootstrap4.scss
(BS4-native variable names) ahead of `@import "bootstrap"`, in
addition to -- not instead of -- the existing
mo/map_theme_vars_to_bootstrap_vars.scss: ~20 stylesheets in the app
read bootstrap-sass-named variables directly (`$btn-default-color`,
`$navbar-inverse-bg`, etc.), so removing that file breaks compilation
until those call sites are swept to BS4-native names or mixins.

Fixed two bugs in the BS4 mapping file: $gray-100/$gray-900 were
hardcoded to $LIGHT_COLOR/$DARK_COLOR regardless of theme, backwards
for a dark theme like Amanita -- now derived from
$BODY_BG_COLOR/$BODY_TEXT_COLOR, which already resolve light/dark per
theme. $card-cap-bg used darken() instead of the project's
scale-color() convention.

Deleted _utilities.scss's font-weight-bold/border-top/border-bottom/
list-group-flush rules, duplicates of what BS4 already provides
(cleared for deletion by #5340).

Issue #5348.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NuatY3dLBZM9dzYFLzymjT
Cross-checked all 107 variables the file sets against the installed
bootstrap 4.6.2 gem's _variables.scss. These two don't exist in
BS4 -- .btn-link reads $link-color/$link-hover-color directly
(confirmed in the gem's _buttons.scss). Leftover bootstrap-sass
variable names; harmless as dead assignments but not doing anything.

Issue #5348.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NuatY3dLBZM9dzYFLzymjT
.in -> .show (type-switch, form-images, Components::Collapsible's
server-rendered class), .open -> .show (autocompleter dropdown --
moved to pulldownTarget, not wrapTarget: BS4's .dropdown-menu.show is
a compound selector on the menu itself, unlike BS3's .open >
.dropdown-menu parent-child selector).

Deleted matrix-table_controller.js and name-panels_controller.js:
both existed only to manually equalize panel heights and insert
clearfix spacers for BS3's float-based .row -- BS4's .row is
display:flex, which does both natively. Deleted their supporting
wiring (MediaQueryTests, the breakpoint-detection sentinel
matrix-table_controller.js depended on; the name-panels-target data
attrs on ClassificationPanel/LifeformPanel).

Deleted collapse-fallback_controller.js: a code comment there already
recorded that BS4's collapse data-API calls preventDefault()
unconditionally, making the controller's one job redundant.

Migrated every $screen-*-min/$screen-*-max breakpoint reference
(_carousel.scss, _layout.scss, _lightgallery.scss,
_links_buttons_alerts.scss, _matrix_box.scss, _utilities.scss) to
BS4's breakpoint-min()/breakpoint-max() functions against
$grid-breakpoints, set in mo/map_theme_to_bootstrap4.scss (BS4
stock xs/sm/md/lg, xl widened to 1320px -- MO's custom "lg" value,
moved to the tier that is now the top one).

Components::Column's grid-width props (sm:/md:/lg:/xl:) and its
show_at:/hide_at: visibility classes both key off this same map now,
resolving the mismatch a comment on the component flagged: they
share breakpoint names and ran on different pixel thresholds by
design (grid on BS3 values, visibility pinned to match); now both
read the one map, so a given name means one width everywhere. Added
the .d-xl-* block to _utilities.scss, which had no BS3 counterpart
(xl was not a bootstrap-sass breakpoint).

Issue #5348.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NuatY3dLBZM9dzYFLzymjT
…s BS4

Every remaining bootstrap-sass-only variable name MO's stylesheets
referenced directly (button/navbar/panel/alert/dropdown/input color
and spacing names, the generic padding scale, $text-color, etc.) is
migrated to its BS4-native equivalent or, where BS4 has no equivalent
concept, to a direct expression or MO's theme variable:

- Panels -> cards: $panel-bg/$panel-default-heading-bg/
  $panel-default-text/$panel-inner-border/$panel-body-padding ->
  $card-bg/$card-cap-bg/$card-color/$card-border-color/
  $card-spacer-y (added to mo/map_theme_to_bootstrap4.scss).
- Navbar: BS4 only has navbar-light/navbar-dark TEXT color variables,
  no bg/border/hover-bg -- $navbar-default-*/$navbar-inverse-* call
  sites now read MO's $TOP_BAR_*/$LEFT_BAR_* theme variables directly
  instead of going through a bootstrap-sass-shaped intermediary.
- Buttons: $btn-active-variants (_form_elements.scss) now derives
  from $theme-colors ($primary/$success/etc.) using color-yiq() for
  text contrast, rather than bootstrap-sass's hardcoded white text --
  correct even where a variant's background is light (e.g. warning).
- Alerts: BS4 computes alert colors dynamically (theme-color-level()),
  with no exposed per-variant variables -- inlined the same rgba(...,
  0.2) soft-background formula against the BS4-native color name at
  each call site instead of naming new bridge variables.
- $input-border -> $input-border-color, $input-bg-disabled ->
  $input-disabled-bg, $dropdown-border -> $dropdown-border-color,
  $list-group-border -> $list-group-border-color, $hr-border ->
  $hr-border-color, $border-radius-base -> $border-radius,
  $brand-danger -> $danger, $text-color/$text_color -> $body-color,
  $padding-base-vertical/-horizontal/$padding-small-vertical ->
  $input-btn-padding-y/-x/-y-sm.
- $carousel-text-shadow (a design constant, not theme-derived) and
  the 480px top-nav sub-breakpoint (no BS4 tier at that width)
  inlined as literals instead of named variables.

mo/map_theme_vars_to_bootstrap_vars.scss is deleted -- every
variable it defined is now either unused or replaced at its call
sites. mo/_variables.scss (an unwired, complete bootstrap-sass
_variables.scss copy) was a useful reference throughout this pass
for what each dropped name used to resolve to; left in place, still
unimported.

Issue #5348.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NuatY3dLBZM9dzYFLzymjT
border-radius/border-radius-sm/border-radius-lg, btn-border-radius,
input-border-radius, and alert-border-radius were all 5px -- now
0.3125rem, matching how the rest of this file (font-size-base,
btn-padding-y/x, navbar-padding-y/x) already expresses measurements.

card-spacer-y no longer needs a value here: BS4's stock 0.75rem
already equals bootstrap-sass's old 12px panel padding at a 16px
root (MO doesn't override html's font-size). Only card-spacer-x
needs narrowing from BS4's wider 1.25rem default, expressed in rem
to match rather than as a 12px literal.

Border widths (input-border-width, alert-border-width), the tooltip
max-width, and the grid gutter stay in px -- that already matches
BS4's stock values for those specific variables.

Issue #5348.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NuatY3dLBZM9dzYFLzymjT
Every assertion pinned to Bootstrap 3's "in" class for an expanded
collapse pane now checks Components::Collapsible::EXPANDED_CLASS
(currently "show") instead of a hardcoded literal, across the
component/view/system/controller tests exercising it: Collapsible,
Accordion, the InatImports/aliases/reasons-fields/BlockedIps forms,
the observation and search forms, and the create-observation
spare-slip flow.

One functional fix alongside the test updates: mo/_icons.scss's
`tbody.collapse.in` rule (overriding Bootstrap's collapse
display:block back to table-row-group for a collapsible <tbody>)
would have silently stopped matching anything under BS4 -- fixed to
`.show`.

Issue #5348.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NuatY3dLBZM9dzYFLzymjT
names_lookup_field_group.rb#collapse_class returns a bare "in"/nil
string, not a call through Components::Collapsible -- a hand-rolled
duplicate of the same expanded-state logic. Every earlier sweep for
this migration grepped for the CSS-selector shape (.in), which
doesn't match a plain Ruby string return, so this survived until the
search form's collapse-expanded tests caught it live.

Issue #5348.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NuatY3dLBZM9dzYFLzymjT
`variant:` renames :default/:inverse to BS4's :light/:dark, matching
Bootstrap's navbar-light/navbar-dark classes. RIGHT_CLASS/LEFT_CLASS
switch from BS3's float-based navbar-right/navbar-left to BS4's
margin-auto utilities (ml-auto/mr-auto).

The `.navbar.navbar-flex` bridge rule in mo/_top_nav.scss is deleted:
checked the installed bootstrap-4.6.2.1 gem's _navbar.scss and
confirmed BS4's `.navbar` already sets the same display/flex-wrap/
align-items/justify-content the bridge rule copied, and the bridge's
padding value is moot regardless since Components::Navbar#base_class
always adds `.p-0 !important`. Updated mo/_layout.scss and
mo/_top_nav.scss's remaining `.navbar-default` selectors to
`.navbar-light` to match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NuatY3dLBZM9dzYFLzymjT
Re-vendoring jquery via `bin/importmap pin jquery@3.7.1` during the
gem swap replaced the working classic-UMD jquery.js (which attaches
window.jQuery/window.$ unconditionally) with jspm.io's ESM-wrapped
build. That build always invokes jQuery's UMD factory with
noGlobal=true (since window.document exists in a browser), so the
"undefined"===typeof n gate for attaching globals doesn't fire.

This broke every Stimulus controller calling bare $(...)
(search-type_controller, tooltip_controller, jquery-events-to-dom-events)
with "window.$ is not a function", and broke bootstrap.js's
jQueryDetection with "Cannot read properties of undefined (reading
'jquery')" -- bootstrap.js does `import*as e from"jquery"`, which
needs jquery.js's ESM `export default` to resolve to something with a
usable `.fn` object.

Patched jquery.js to unconditionally attach window.jQuery/window.$
(instead of gating on the noGlobal flag), keeping the ESM export
intact so both consumers work from the same file. Found live via
claude-in-chrome on the dev server: the search-bar "more options"
toggle rendered but did nothing, console showed both errors, and
test_faceted_search_panel_open_close_stays_on_page failed on
#search_nav not gaining the expanded class.

That same system test also had one leftover `.in` (pre-EXPANDED_CLASS)
selector from before Components::Collapsible::EXPANDED_CLASS existed --
fixed alongside since it was blocking verification of the jQuery fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NuatY3dLBZM9dzYFLzymjT
…MD one

The prior commit staged an intermediate state -- the classic UMD
jquery.js restored from main, before the jspm-based patch was applied.
That version has no ESM export, so bootstrap.js's
`import*as e from"jquery"` resolves to an empty module namespace and
Bootstrap's jQueryDetection throws.

This commit is the patched jspm.io ESM build (unconditional
window.jQuery/window.$ attachment, valid `export default`), confirmed
via claude-in-chrome: search-bar collapse, tooltip, and Bootstrap's JS
all run with no console errors.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NuatY3dLBZM9dzYFLzymjT
This test predates Components::Collapsible::EXPANDED_CLASS and still
asserted the Bootstrap 3 .in class directly, so it failed on the very
first assertion regardless of any other behavior -- which is what
surfaced the jQuery global-attach regression fixed in the previous two
commits.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NuatY3dLBZM9dzYFLzymjT
BS4 drops the .panel component entirely (renamed to .card), and
MO's SCSS only patched color/border on top of Bootstrap's base
panel box styling -- which no longer exists under the bootstrap gem.
Panel was rendering completely unstyled as a box (no border,
background, or padding structure) until this conversion.

Components::Panel now emits card/card-header/card-body/card-footer/
card-title instead of panel/panel-heading/panel-body/panel-footer/
panel-title, and drops the panel-default variant class entirely (BS4
cards have no default-variant modifier). panel-collapse-trigger and
panel-heading-links stay as MO-owned hooks; panel-collapse renames to
card-collapse for consistency with the rest of the sweep. Collapsible's
`panel:` kwarg keeps its name (still means "inside a Panel/card") but
now emits card-collapse.

The `sizing:` prop / .panel-sizing wrapper is removed outright -- its
comment already flagged it "Bootstrap 3 only, remove when migrating
to Bootstrap >= 4": BS4's flex-based .row/.col-* grid stretches
columns to equal height by default, so the fixed-height hack
Matrix::Box relied on isn't needed anymore.

Two standalone leftovers fixed alongside: namings/header.rb's
standalone panel-title h4, and projects/admin/show.rb's
panel_class: "panel-danger" (BS4 has no danger-panel equivalent --
switched to the border-danger utility class, generated from
$theme-colors).

Verified live via claude-in-chrome: matrix boxes, the search-form
cards, and the observations index all render as expected with the
new card structure.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NuatY3dLBZM9dzYFLzymjT
.align-middle already exists as a BS4-native utility class
(vertical-align: middle on the element itself) -- MO's version in
mo/_utilities.scss duplicated that base rule and additionally
special-cased combining it with .table on the same <table> element to
cascade vertical-align: middle onto every cell, overriding Bootstrap's
th/td vertical-align defaults. Reusing Bootstrap's class name for a
second, table-wide meaning was confusing, and now that BS4 provides
`.align-middle` as a utility class, it's a collision.

Checked Bootstrap 4.6.2's _tables.scss: the th/td vertical-align:top
/ thead th vertical-align:bottom defaults this override targets are
unchanged from Bootstrap 3, so the rule still does work under the new
gem.

Split the SCSS into a standalone .table-align-middle.table rule and
updated the only two callers that combine it with .table on the
<table> element itself (Admin::BlockedIps::Manager,
Admin::BlockedIps::Edit::IpSummary). Every other .align-middle caller
(individual <td>s, Components::Table column classes) just wants
Bootstrap's per-cell utility and is unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NuatY3dLBZM9dzYFLzymjT
app/assets/stylesheets/mo/_links_buttons_alerts.scss got a large
badge-id rewrite from origin/main that still used bootstrap-sass's
$text-color, which doesn't exist under the BS4 gem -- broke the
Amanita and BlackOnWhite theme compiles. The sibling rule two lines
above already uses $body-color for the same background-swap-on-hover
effect, confirming that's the intended variable.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NuatY3dLBZM9dzYFLzymjT
…up log-details/footer markup

Column-generated classes were still BS3 syntax (col-xs-N/col-xs-offset-N),
which don't exist in BS4's compiled CSS -- fixed to col-N/offset-N.
Removed Column entirely from the sidebar and #right_side (fixed-width
CSS instead), restored top-bar/sidebar border colors BS4's
navbar-light/dark classes don't set, fixed base font size, small/.small
sizing, and vote-btn-group sizing to match the old BS3 ratios.

Renamed Components::Matrix::Table/Box/Carousel to Components::Grid/
Grid::Box/Grid::Box::Carousel (including the .matrix-box CSS class to
.grid-box, the "matrix_box" turbo-stream context string to "grid_box",
the display_opts[:matrix] index option to :grid, and
matrix_caches_in_this_request? to grid_caches_in_this_request?) --
Matrix::Table was a literal <table> in the pre-Phlex layout; the
current implementation is a <ul> with BS4 row-cols-* utilities, so the
name no longer fit. DOM ids, the context: "matrix_box" turbo-stream
target strings tied to it, and Observation.matrix_box_includes were
left unrenamed -- functionally coupled to turbo-stream target matching
and out of scope for this pass.

Reworked the grid box's details/footer markup: the small/.small
wraps on every line (log-where, log-when-who, source-credit,
occurrence-link, rss-detail, user-detail, updated-at) compounded with
the wrap's own reduced font-size, rendering everything far smaller
than intended -- replaced with a single .log-text size on the
details/footer wrap and a ul.list-unstyled > li.hanging-indent
structure for each line, reusing the existing .hanging-indent utility
instead of introducing a competing one. Moved the title's IDBadge out
of the <h5> into a sibling flex wrapper now that its size is set
directly via the size: prop rather than inherited from the heading's
font-size. Restored BS4's dropped muted-small-in-headings behavior for
the author-citation text. Mixed card-header/footer's background color
between the card and page background instead of a small darken of the
card's own color (readable in both light and dark themes).

Removed trailing periods from the rss-log detail translations (some
had them, most didn't) and gave the comment log entries their own
template with a line break before the summary, since comment text is
often long.

CACHE_VERSION bumped repeatedly across this work (now v10) -- each of
the rendered-HTML changes above (class renames, markup restructure,
badge move/resize, translation text) invalidates previously-cached
Grid::Box fragments.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NuatY3dLBZM9dzYFLzymjT
…gative margins

The data-nav-target="offcanvas" wrapper used Components::Row, which
unconditionally adds .row -- BS4's .row sets display:flex plus
negative left/right margins (half the gutter width), and those
margins fought #main_container's px-sm-3 padding, pushing content
past its edge.

Rendered the wrapper as a plain div instead: it isn't a Bootstrap
grid row -- .row-offcanvas/.row-offcanvas-left already provide the
position/transition/slide mechanism -- and moved the display:flex +
flex-wrap:wrap that keeps the sidebar and #right_side side by side
onto .row-offcanvas directly, without the negative margins .row also
carried.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NuatY3dLBZM9dzYFLzymjT
…tional .row class

Components::Row always adds "row" (Bootstrap's negative-margin grid
class) with no opt-out, which fought #main_container's .px-sm-3
padding on the sidebar-offcanvas wrapper. Switched that wrapper to a
plain div and moved the needed flex layout directly onto
.row-offcanvas instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NuatY3dLBZM9dzYFLzymjT
…e dark theme

Carousel controls now match Bootstrap 4's carousel markup
(https://getbootstrap.com/docs/4.6/components/carousel/): a
<button type="button"> carrying data-target/data-slide as a sibling
of .carousel-inner, named .carousel-control-prev/-next, instead of
an <a role="button"> inside a .carousel-control-wrap div. Positioned
per-button via absolute positioning against .carousel's
position: relative, matching the gem's _carousel.scss. Icon size
reduced to match Bootstrap 3's $carousel-control-font-size (1.25rem
/ 1.875rem at md+, was 2rem/3rem), and the controls' bottom offset
now accounts for the indicator strip's height so the icon centers
over the photo, not the photo+indicators combined box.

.carousel-inner's background switched from .bg-light (Bootstrap's
$LIGHT_COLOR, a distinct accent color per theme -- a pale silver in
Amanita, clashing with its dark page background) to a new .bg-body
utility tied to $body-bg, the theme-aware page background. Named to
match Bootstrap 5.3's .bg-body so it is a no-op rename at a future
upgrade.

.card/.card-title/.card-header padding and margin brought in line
with Bootstrap 3's .panel defaults, which Bootstrap 4's .card leaves
to the caller: .card gets back its margin-bottom (.grid-box resets
it to 0, putting the margin on its <li> instead, since .card also
carries h-100 there); .card-title's margin-bottom is zeroed (was
inheriting $card-spacer-y's spacing, meant for body content, not a
header title); .card-header gets a smaller vertical padding than
$card-spacer-y provides, shared uniformly by $card-spacer-y/-x now
converged to Bootstrap 3's $panel-body-padding (15px, both axes)
instead of Bootstrap 4's smaller defaults.

Also: Grid::Box's custom-block layout (glossary-term image removal
grid) now wraps its content in Panel for card styling it was
missing; Grid::CACHE_VERSION bumped for the resulting fragment
shape changes; a handful of duplicate Bootstrap-4-default SCSS
variables deleted from mo/_variables.scss (carousel, unused).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NuatY3dLBZM9dzYFLzymjT
…lass

Bootstrap 4 renamed the class that makes a .fade element visible from
.in to .show (bootstrap/_transitions.scss: .fade:not(.show) sets
opacity: 0). Components::Modal's auto-open modal/backdrop and
Form::UploadGallery::Item's remove-image button both still emitted
"fade in", which no longer matches any rule under the BS4 gem --
both rendered permanently invisible (opacity: 0).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NuatY3dLBZM9dzYFLzymjT
… comments

.card/.card-header/.card-title move from mo/_content.scss into a new
mo/_card.scss, imported ahead of mo/_grid_box (whose .grid-box .card
override stays put -- it's grid-box-specific, not generic card
styling). Also trimmed a handful of comments across the carousel/
card/variables work that compared current values against Bootstrap
3's -- code comments should state the current fact, not narrate
history nobody reads.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NuatY3dLBZM9dzYFLzymjT
Two corrections were fighting each other and landing icons below
the text baseline: .inline-icon-link's vertical-align: middle, and a
separate top: 2px nudge on a:not(.btn):not(.inline-icon-link) >
.mo-icon / .list-group-item > .mo-icon (and their > span > variants).
Removed both -- the vertical-align: middle is dropped from
.inline-icon-link, and the top: 2px rule is deleted outright.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NuatY3dLBZM9dzYFLzymjT
Bootstrap 4's collapse data-API click handler calls preventDefault()
and stopPropagation() unconditionally (confirmed in the vendored
bootstrap.js), which blocks Turbo's click-driven frame navigation on
a trigger that carries both data-toggle="collapse" and
data-turbo-frame. The "Shared with [iNat] [MCP]" badges carry both,
so the accordion pane opened correctly but its turbo-frame didn't
fetch -- confirmed live: aria-expanded flipped and .show got added
to the pane, but the frame's src/innerHTML stayed empty and no
network request fired.

Fix: give the turbo-frame a src + loading="lazy" instead of relying
on the click. Turbo fetches it once the pane becomes visible,
independent of Bootstrap's click handling.

Also: info_frame.rb's "On iNaturalist:"/"On MyCoPortal:" label was an
h5, sized too large for a compact accordion pane -- dropped to h6.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NuatY3dLBZM9dzYFLzymjT
)

The file's header comment has said "All duplicates should be
deleted here if updating to 4" since #5340's prep pass -- that pass
flagged and fixed the 4 classes whose value diverged from Bootstrap's
(font-weight-bold, border-top, border-bottom, list-group-flush) and
left everything else as ordinary duplicates for deletion once the
BS4 gem was loaded. This is that step.

Removed: every m-*/p-*/mx-*/my-*/mt-*/mb-*/ml-*/mr-*/px-*/py-*/pt-*/
pb-*/pl-*/pr-*/mx-auto spacing utility Bootstrap generates natively
at the base tier, plus the xs-tier d-*/float-*/text-align/align-items/
justify-content/flex-*/position-*/align-*/rounded-* utilities and
their sm/md/lg/xl responsive variants, wherever the value matched
Bootstrap's. A handful matched in every respect except missing
!important (.my-0, .flex-column/.flex-row/.flex-wrap, .rounded-circle)
-- deleted too, since Bootstrap's !important version already wins
any cascade fight the non-!important MO version was winning by luck.

Three classes sat at a value that doesn't match Bootstrap's scale
(.mt-5/.mb-5/.pb-5 at 2rem vs Bootstrap's 3rem) with live callers
across ~25 call sites -- deleted anyway per direction, accepting
Bootstrap's value. Kept: classes Bootstrap has no equivalent for
(.min-w-100/.max-w-100/.min-h-100/.max-h-100, .indent, .bg-*,
the .table-align-middle cascade helper, etc.), plus .mx-sm-2/
.mx-sm-3/.mr-sm-4 (em-based, not matching Bootstrap's rem scale,
live callers in top_nav.rb -- flagged, not verified against which
value the page needs).

937 -> 336 lines.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NuatY3dLBZM9dzYFLzymjT
Missed in the previous sweep -- matches Bootstrap's img-fluid()
mixin (max-width: 100%; height: auto;).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NuatY3dLBZM9dzYFLzymjT
Byte-for-byte copy of Bootstrap's utilities/_stretched-link.scss,
already commented "delete after upgrade" -- upgrade's here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NuatY3dLBZM9dzYFLzymjT
… _content.scss

Page-specific styling, not utility classes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NuatY3dLBZM9dzYFLzymjT
Bootstrap's \$overflows list only generates auto/hidden in this
gem version -- .overflow-visible stays (not a duplicate),
.overflow-hidden matches and is removed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NuatY3dLBZM9dzYFLzymjT
nimmolo and others added 4 commits September 10, 2026 21:46
#right_side's calc-based width reduction applied at every breakpoint,
even though .sidebar-offcanvas is absolutely positioned and slid
off-screen below lg -- so the main content column was losing 9.9rem +
24px of width on mobile for a sidebar that isn't there.

Restructured mobile-first, matching MO's pre-BS4-cutover approach
(bootstrap-sass era, min-width(lg)-gated): .sidebar-offcanvas's
off-screen positioning and the .row-offcanvas-*/.active offset rules
are now the unguarded base (mobile) rules; a single
min-width(breakpoint-min(lg)) block resets .sidebar-offcanvas to
position: static and applies #right_side's width reduction, once the
sidebar has room to sit in-flow. Left `hidden-overflow-x`/
`navbar-light`/`navbar-form` in a separate max-width(md) block --
unrelated to this layout mechanism.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NuatY3dLBZM9dzYFLzymjT
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NuatY3dLBZM9dzYFLzymjT
All test-only fixes -- each assertion was pinned to markup/classes
from before a component change earlier on this branch, not caught
locally since these span multiple test types (component, controller,
integration).

- show_prev_next_nav_test.rb: dropped the mr-auto assertion -- that
  class was removed from BTN_CLASSES earlier on this branch.
- external_links_test.rb (component) x4: .p-3 -> .p-card, matching
  the component's switch to the .p-card utility.
- external_links_controller_test.rb: h5 -> h6, matching InfoFrame's
  current heading level.
- footer_test.rb: div -> li, matching Grid::Box::Footer's current
  hanging-indent li structure.
- lurker_integration_test.rb: .badge-md -> .badge-lg, matching
  Grid::Box's current IDBadge size.
- observations_integration_test.rb: added normalize_ws: true --
  ObjectTitle's title span now sits inside a wrapping div (for the
  owner-naming line), which introduces incidental whitespace between
  the id badge and title text in the raw HTML that rack_test's text
  extraction doesn't collapse without it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NuatY3dLBZM9dzYFLzymjT
@coveralls-official

coveralls-official Bot commented Sep 11, 2026

Copy link
Copy Markdown

Coverage Status

coverage: 99.424%. remained the same — nimmo-5348-bs4-cutover into main

nimmolo and others added 25 commits September 11, 2026 14:34
Several tests were pinned to a specific tag (h5/h6, div/li) or a
sizing class (.badge-md/-lg) that changed under the BS4 cutover but
carried no behavioral meaning. Replace with contract-level selectors
(structural attributes, existence checks) that survive future markup
tweaks.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…mark labeling

InputGroup::Addon still emitted BS3's .input-group-btn/.input-group-addon,
which BS4 doesn't style as flex children -- rebuilt it as
.input-group-append/-prepend wrapping .input-group-text, matching BS4's
markup. Removed the dead BS3 flex shim in _links_buttons_alerts.scss
that targeted the old class names.

The sort dropdown and the pagination goto widget were misaligned by a
few pixels: a stray default <ul> margin-bottom on the sort bar (neither
.list-unstyled nor .flex-bar reset it) and an asymmetric $btn-padding-y
override that didn't apply to $input-padding-y. Converted the sort bar's
<ul> to a <div> (Components::Dropdown gained an `element:` prop so its
wrapper isn't hardcoded to <li>) and removed the button-only padding
override so buttons and inputs share the same height.

ShowPrevNextNav now uses Components::ButtonGroup instead of a
hand-rolled <ul>/<li> list, wrapped in a <nav aria-label="Results
navigation"> landmark. Audited every <nav> in the top-nav/title-bar/
sidebar area: `.show_title_nav` and `.show_object_nav` were unlabeled
<nav> landmarks wrapping non-navigation content (title+edit-icons,
interest-icons) -- demoted both to plain <div>s. Added aria-label to
#top_nav and #sidebar so screen readers can tell the multiple <nav>
landmarks apart. interest_icons.rb converted from a <ul class="nav">
to a <div class="btn-toolbar" role="toolbar">, since it's a cluster of
borderless icon buttons, not a navigation list.

Also swapped PatternSearch and Identify::Form from the MO-owned
.flex-bar bridge class to BS4's native .form-inline, and dropped
.flex-bar from top_nav.rb's CONTAINER_CLASSES -- it's a .container-fluid
inside .navbar, which BS4 already gives flex + justify-content:
space-between + align-items: center.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…input-group/dropdown patterns

The Actions/user dropdowns pushed #top_nav's height open instead of
floating over the page: BS4's `.navbar-nav .dropdown-menu` defaults to
`position: static` (meant for a collapsed/stacked navbar-nav), and
#top_nav doesn't collapse via BS4's mechanism -- the offcanvas
sidebar is the mobile nav. Added `.navbar-expand` (bare, unconditional)
scoped to the right-side div, which supplies `position: absolute`
without the two-row `w-100` stacking break `.navbar-expand`'s
flex-flow change would cause on the outer nav. Both dropdown menus also
get `.dropdown-menu-right` so they anchor to the trigger's right edge
instead of overflowing off it.

`.navbar-link` (`Components::Navbar::LINK_CLASS`/`LINK_CLASSES`) had
zero CSS or JS consumers anywhere in the app -- removed the constants
and every call site (ShowPrevNextNav, IndexPaginationNav's page links,
PatternSearch's/SearchBar's toggle buttons), along with the
raw-constant-value tests that only asserted the dead strings equaled
themselves.

`RIGHT_CLASSES`'s `justify-content-end` was inert: the `<ul>` it wraps
already carries `ml-auto`, and an auto margin consumes all leftover
main-axis space before `justify-content` gets to distribute any.
Removed it. Moved the dropdown `<li>` spacing from a `_top_nav.scss`
`@extend .ml-4` reaching into Phlex-rendered markup to an explicit
`wrapper_class: "ml-2"` on each `Dropdown(...)` call. Unified the
search-toggle and QR-code buttons under one shared `.btn-toolbar`
instead of an asymmetric `.navbar-form` div + bare button.

BS4 dropped BS3's `.has-feedback`/`.form-control-feedback` icon-overlay
pattern entirely -- PatternSearch's and Identify::Form's search icons
now use the documented replacement, an `.input-group` with the icon in
a prepended `.input-group-text` (`Components::InputGroup` +
`InputGroup::Addon`), verified against the identify form's live
autocomplete dropdown to confirm its position: absolute suggestion
list still anchors correctly. Deleted the fully-dead has-feedback/
has-search CSS block, including an already-orphaned `.page-input
.form-control-feedback` rule with no live caller.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sorter's "Sort by:" label gets .pr-2 (breathing room before the
dropdown when visible at sm+); the dropdown's wrapper_class drops
px-2 for px-0 so it sits flush left against the sorter's pl-3 when
the label is hidden on mobile.

Also reduces the left-nav-toggle's padding and rebalances the
QR-code button's margins at narrow widths.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…oup column gutter

vertical-align: -0.15em on icon-bearing buttons replaces Bootstrap's
default `middle`, which aligns to half the parent font's x-height
above baseline rather than the visual center of surrounding text.

$yiq-text-dark/$yiq-text-light now point at $DARK_COLOR/$LIGHT_COLOR
instead of Bootstrap's stock $gray-900/$white -- $gray-900 is remapped
to the theme's body text color, which is light in Amanita, so
color-yiq() picked light text for buttons whose background should get
dark text.

List-group columns (namings table, comments) get a tighter 0.5rem
gutter instead of Bootstrap's default; a column wrapping a nested
.row is zeroed so its padding doesn't stack with the inner columns'.

InlineCRUDLinks gets a margin_class (ml-1) via a new prop on
InlineLinkBlock, distinct from that component's other four callers.

Responsive body font size: 15px from the lg breakpoint up (was 14px
everywhere), with .btn and .modal-body following since their rem-based
sizing doesn't otherwise track body's computed size. Headings stay
pinned to their existing rem values.

Reorganizes app/assets/stylesheets/mo/_utilities.scss: several rules
that were card/table-specific move to _card.scss/_tables.scss, and
.dashed-border/.dotted-border consolidate into .border-dashed
(form-images_controller.js updated to match).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Vote buttons and the selected-vote span now share matching padding
and border-width (transparent border on the buttons, visible on the
selected one), so casting a vote no longer changes the row height.

Tooltip controller disposes a vote button's Bootstrap tooltip when
its trigger element disconnects -- Turbo's turbo_stream.replace on
the vote interface swaps the trigger out without firing Bootstrap's
hide handlers, leaving the tooltip popup orphaned in the DOM.

Drops the vote-buttons row's top margin and trims the carousel
caption's .caption-content padding to reduce excess whitespace
around the vote buttons.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ssertion hook

.well/.well-sm had no BS4 equivalent and MO's .well rule supplied no
box chrome -- three live callers (Help#render_well,
Help::CollapseBlock, Form::CameraInfo) rendered an unstyled box.
Components::Well replaces the hand-rolled divs with a bordered,
slim-padded box (no background, no size variant).

.help-block gains .form-text (BS4's display:block + top-margin
class) for the plain field-help shape; the class stays as an
identifier. Deletes the dead color: $card-color rules for
.help-block/.help-note (both no-ops -- nothing sets a different
color for either to override).

Adds .claude/hooks/block_cosmetic_class_assertions.sh, blocking
assert_html/assert_no_html/assert_selector/assert_includes/
assert_not_includes calls in test/**/*_test.rb that pin button
chrome, margin/padding utilities (numeric, auto, or negative n<N>
steps), or font-weight/text-muted classes -- a mechanical backstop
after the same cosmetic-class-assertion anti-pattern recurred twice
in one session despite being documented in project memory.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Bootstrap 3's .checkbox/.radio reserved this gap via an
absolutely-positioned input plus label padding-left; Bootstrap 4
doesn't define these classes, so nothing supplied the gap anymore --
confirmed live, checkbox input had margin-right: 0px. Adds
margin-right: $custom-control-gutter (BS4's 0.5rem control-gutter
variable) on the checkbox/radio input.

.help-block.form-text gets margin-bottom: 0.5rem -- .form-text only
sets margin-top, and the field wrapper above it has its bottom margin
zeroed to avoid doubling the top-margin space, so nothing was adding
space below the help text before the next element. Scoped to
`.help-block.form-text` (not bare `.help-block`) so it doesn't
collide with `.well`'s margin-bottom on the well-shaped variant.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Cut narrative asides (a specific caller's margin choice compared
against another class's own margin value, the "not an oversight"
defensive framing) and redundant possessives, and dropped the
margin_class default from the comment since that value is subject to
change independent of the component's contract.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Three related issues in Header::IndexPaginationNav's letter/page
"goto" widgets, found on the names index:

- "By letter" wrapped onto two lines. The label div had default
  flex-shrink and no white-space protection, so it shrank and wrapped
  under the flex row's space pressure once it moved inside the
  input-group (see below) tightened that pressure further. Confirmed
  in the browser: the fix is text-nowrap.

- The input-group-append button was 1px taller than the input
  (35.4px vs 34px) in both widgets. `.btn` already has a `lg`+
  breakpoint bumping its font-size to match body's bump (see the
  existing rule in _elements.scss), but `.form-control` was missing
  the same bump, so its line-height came out a pixel short at that
  breakpoint. `align-items: stretch` can't fix this since
  `.form-control` sets an explicit height, not `auto` -- confirmed by
  toggling the button's font-size in the browser and watching both
  heights converge. Added the same breakpoint rule for `.form-control`,
  mirroring the existing `.btn` one.

- "By letter" was a plain div, not a <label> tied to the <input> it
  describes. InputGroup::Addon gets a new `variant: :label` (a
  <label for:> in the same input-group-text shape as `variant: :addon`,
  with .font-weight-normal baked in -- the sitewide `label { font-weight:
  bold }` rule would otherwise bold it). The label now lives inside the
  input-group as a prepend, matching Bootstrap's input-group-with-label
  convention, with an id unique per nav position (top/bottom) so the
  label's for= doesn't collide when the nav renders twice on one page.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
# Conflicts:
#	test/components/grid_test.rb
#	test/system/observation_form_system_test.rb
Adds .px-card/.py-card, split out of the existing .p-card
($card-spacer-y $card-spacer-x), so horizontal-only or vertical-only
card padding can be reused outside .card.

#top_nav's two .container-fluid rows now use px-card instead of
px-3, so top-nav content lines up with card content below it.

Header::IndexPaginationNav's .pagination-top/.pagination-bottom bars
carry the edge padding (px-card) directly now, instead of the sorter
and pager children each carrying left-padding. Sorter's pl-3 is
dropped accordingly.

Also fixes a handful of stale exact-class-string test assertions
that broke or were touched incidentally by this change (cosmetic
utility classes and non-semantic div tag pins), per the ongoing
bs4-cutover cosmetic-test-assertion cleanup.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ApgAvYYM7QvhpaFeXb2CFV
content_padding's :no_panels branch and Components::ContentPadded
both switch from p-3 to p-card, so plain (non-panel) page content
lines up with card edges.

Sets $dropdown-item-padding-y/-x (0.25rem/0.5rem) in
map_theme_to_bootstrap4.scss as the single source of truth for
dropdown-row padding, tighter than BS4's default (.25rem 1.5rem).
#top_nav's form.button_to/button.btn-link dropdown rows and the
autocompleter's virtual-list item rows reference these variables
instead of a literal padding value.

Nests li.dropdown-item under ul.virtual_list, scoping it to the
autocompleter's list instead of every dropdown-item in the app.
Moves the row's padding from li.dropdown-item to its descendant a.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ApgAvYYM7QvhpaFeXb2CFV
Components::Dropdown, the autocompleter's virtual-list rows, and
Views::Controllers::Observations::Identify::Form's dual-target
dropdown all move from BS3-style <ul>/<li>-nested markup to BS4's
flat convention: .dropdown-menu (a <div>) holds .dropdown-item
elements (<a>/<form>) directly, no <li> wrapper. This is what lets
the autocompleter's row padding live on the row itself instead of
being split between a <li> and its nested <a> (patched earlier this
session, now the structural fix that made those patches necessary
in the first place). base_controller.js's row clone/scroll-math
functions are updated to treat each row as the <a> directly.

Browser-verified: DOM shape, keyboard/mouse row highlighting,
click-to-select, and virtual-scroll past 10 matches all work
unchanged after the markup change.

Running the full system-test suite surfaced a pre-existing bug:
Components::Accordion did not set data-parent on its collapse panes.
Bootstrap 4's collapse.js reads data-parent off the collapse target
that was clicked into (the pane), not the trigger that was clicked --
the codebase had it on the trigger instead, at four call sites, so
sibling panes did not auto-close. Fixed at the source (Accordion's
pane slot sets it now), and removed the dead trigger-side wiring
(including the unused Form#cancel_parent prop) at every caller.

Also fixes two stale BS3 collapse-class assertions (`.in`, replaced
by BS4's `.show`) in system tests that predate this session's work.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ApgAvYYM7QvhpaFeXb2CFV
AutocompleterField's collapsible help renders as a sibling right
after .form-group -- when expanded, .form-group's default
margin-bottom (~1rem) read as a gap between the field and the help
describing it. .form-group now gets mb-2 in that case, and the outer
.autocompleter wrapper gets mb-3 to keep the same overall spacing to
whatever field comes next. Plain (non-collapsible) help was already
handled correctly elsewhere and is untouched.

Also fixes autocompleter_field's top-level helper: unlike
text_field/textarea_field/checkbox_field/select_field, it did not
call set_help_slot, so a plain `help: "..."` string with no block
silently rendered no help (confirmed live against names/form.rb's
correct_spelling field, which uses this shape). Needed to verify
the margin fix in the browser in the first place.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ApgAvYYM7QvhpaFeXb2CFV
Sidebar list-group-items no longer get a separate .indent class --
the heading styling (bold, disabled) already distinguishes headings
from the links under them, so the extra left padding was redundant.
Removed from Sidebar::CSS_CLASSES (both the standalone indent key and
admin's " indent" suffix) and every consumer (Section, ContextNav,
User, Languages). Swept the sidebar test suite for other cosmetic-
class assertions (.font-weight-bold, .disabled, .btn.btn-link, .ml-2,
non-semantic div tag pins) while in these files, deleting tests whose
entire purpose was pinning styling.

Also fixes .dropdown-menu not tracking body's responsive font-size
bump (mo/_elements.scss bumps body to 0.9375rem at the lg breakpoint;
Bootstrap's dropdown font-size is a compile-time rem value relative
to the root, not to body, so it didn't follow). Consolidated into the
existing shared breakpoint block alongside .btn/.form-control, which
already had the same gap patched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ApgAvYYM7QvhpaFeXb2CFV
Moves the collapsible search-bar row out of TopNav into a sibling
component, Views::Layouts::SearchNav, rendered directly below TopNav
(Views::Layouts::Application#render_right_side) instead of as a
second row nested inside TopNav's navbar. SearchNav gets a Navbar
wrap (independent padding, border-top-0 so it doesn't double up
against TopNav's bottom border) instead of inheriting TopNav's.
SearchBar moves out of the top_nav/ subdirectory to a flat
Views::Layouts::SearchBar alongside it.

The collapse target is the navbar wrapper itself, not a div nested
inside it -- a collapsed inner div left the navbar's padding and
border visible as an empty bar. Collapsing the whole navbar
(`.collapse` class on the Navbar element itself) makes it disappear
entirely. TopNav's `.mb-2` is dropped via `#top_nav:has(+
#search_nav.show)`/`.collapsing` while the search bar is open or
mid-animation, since SearchNav's vertical padding already supplies
that gap once it's visible -- otherwise the two stacked.

Also fixes #pattern_search_form on mobile: removes extraneous
.form-group wrapper divs around the type-select and submit button,
adds flex-nowrap + min-width: 0 so the input group, select, and
submit button share one line instead of wrapping, and gives the
input group/select width: auto so they can shrink below their
BS4-default width: 100%. Search Help / Advanced / Simple collapse
toggles at either end of the search bar get pl-0/pr-0 so they sit
flush against the container edge.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ApgAvYYM7QvhpaFeXb2CFV
.faceted-search-form's flash target and form columns now sit inside
a Row (flex-column, so its negative margin offsets the columns'
padding on both axes instead of just horizontally), popping them out
of the containing navbar's padding to align with the columns' edges
rather than sitting inset from them.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ApgAvYYM7QvhpaFeXb2CFV
render_help_toggle, render_form_toggle (search_bar.rb), and
render_search_bar_toggle (form/search.rb) pass size: :lg -- CollapseToggle
has its size: prop already, no need to reach for Components::Button for
that. Their padding drops the asymmetric pl-0/pr-2 vs pl-2/pr-0 edge
scheme (BAR_TOGGLE_CLASSES removed) for a flat p-0 on all three;
browser-tested, the surrounding elements still leave sufficient spacing.

Also drops the .btn.btn-link cosmetic class assertions from
search_bar_test.rb per the no-cosmetic-classes-in-component-tests
convention.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ApgAvYYM7QvhpaFeXb2CFV
Below md, the title bar's id badge, edit icons, and interest-icons/
pager now share one row (Header::PageTitle#render_mobile_top_row),
with the object name/owner-naming on a separate row below. The id
badge moves out of Header::ObjectTitle into its content_for(:id_badge)
slot (Views::FullPageBase::Title#add_id_badge), so Header::PageTitle
can place it twice -- next to the title on desktop, and again in the
mobile row -- without an id collision (IDBadge, EditDeleteIcons,
InterestIcons, and ShowPrevNextNav all emit no HTML id). Badge size
moves to :xl since neither placement nests it inside a heading
anymore. .h3.page-title and #owner_naming.h5 get mobile-first
font-size rules (smaller by default, full size at sm+). Desktop's
badge+title+edit-icons row switches from justify-content-between to
justify-content-start with ml-auto on the edit-icons wrapper, so the
badge sits close to the title instead of spread away from it, plus mr-3
mt-md-1 to align the badge with the title's baseline.

Also:
- Fixes #right_side overflowing horizontally on narrow viewports: a
  flex item's default min-width: auto blocked it from shrinking below
  an unconstrained descendant's content width (a full-bleed image, in
  the observation-show case). min-width: 0 on #right_side lets
  flex-shrink do its job.
- Fixes String#small_author's inserted <small class="text-nowrap">
  swallowing the leading space between the name and the author into
  the nowrap span, leaving the binomial's genus/epithet space as the
  only wrap point in the whole run -- narrow viewports broke the name
  itself instead of wrapping before the author. The space now stays
  outside the span, a normal wrap point.
- interest-icons spacing: each eye button gets px-1, the toolbar gets
  ml-2.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ApgAvYYM7QvhpaFeXb2CFV
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bootstrap 3 -> 4 cutover: gem swap, JS renames, navbar + Components::Panel updates, HTML sweep

1 participant