Colour modes follow the operating system. bs_page(color_mode = "auto")
resolves the mode from prefers-color-scheme before the page paints, so
there is no flash of the wrong theme, and set_bs_color_mode("auto") hands
control back to the OS. A mode the user chose is remembered in the browser
and survives a reload. The mode in force is reported as
input$bootstrict_color_mode, always "light" or "dark", so the server
can render to match. An app that never mentions color_mode is untouched.
update_bs_carousel(action =) pauses and resumes cycling.
bs_scrollspy() takes Bootstrap 5.3's observer options, root_margin and
threshold; offset is deprecated upstream, which the documentation now
says.
bs_tooltip() and bs_popover() accept placement = "auto".
bs_table() covers the rest of the Bootstrap tables page: striped now
takes "columns" as well as TRUE/"rows" (.table-striped-columns),
and head_variant, group_divider, row_variant and caption_top give
the generated markup its <thead> colour, the .table-group-divider, the
per-row .table-* accents and .caption-top. row_variant is recycled
across the rows, and NA leaves one unstyled.
bs_navbar_nav(id =) reports its active link as input$id, like
bs_nav(id =), and takes one from update_bs_nav(). Bootstrap has no
"navbar page" component -- switching content is the application's job -- and
this is what makes that pattern expressible; the navigation vignette now
shows it.
bs_radio_button_input() and bs_checkbox_button_input() render Bootstrap's
segmented control (.btn-check), which had no equivalent: its input is a
sibling of its label, with autocomplete="off" and no wrapper, so it
cannot come out of shiny's generateOptions(). They are native controls,
driven by update_bs_toggle_buttons(). A checkbox group reports a character
vector, character(0) when nothing is picked.
bs_nav(id =) reports the value of its active link as input$id and
takes one from the server with update_bs_nav(); bs_nav_link() gains a
value (defaulting to its text). Clicking a link activates it without
jumping to the top of the page, so a nav can be used as a selector.
bs_pagination(id =) and bs_pagination_numbered(id =) report the active
page as input$id and take one from update_bs_pagination();
bs_page_item() gains a value. In a numbered pager the arrows now step
through the pages and disable themselves at either end, instead of being
inert links.
bs_dropdown(id =) and bs_nav_dropdown(id =) report their open state as
input$id, and show_bs_dropdown() / hide_bs_dropdown() /
toggle_bs_dropdown() drive them from the server. Bootstrap emits
shown.bs.dropdown and exposes the methods; neither was reachable.
bs_alert(id =) reports whether the alert is still on the page as
input$id, and close_bs_alert() dismisses it from the server. Alerts are
interactive in Bootstrap, but reported nothing and had no server helper.
bs_nav_dropdown() builds the menu a designer draws in a navbar: an
<li class="nav-item dropdown"> whose toggle is a .nav-link, per the
Bootstrap reference. bs_dropdown() emits a standalone
<div class="dropdown"> with a <button class="btn">, which is invalid as a
direct child of the <ul class="navbar-nav"> that bs_navbar_nav() and
bs_nav() produce, and renders as a grey button rather than a nav link —
yet bs_navbar()'s own documentation used to recommend it. Takes the same
menu items, plus active / disabled / align / dark.
bs_feedback() and set_bs_validation() make validation feedback work.
Bootstrap only displays a .valid-feedback / .invalid-feedback message
when it is a following sibling of the control carrying .is-valid /
.is-invalid. Every bs_*_input() returns its control wrapped in shiny's
div.form-group.shiny-input-container, so a bs_invalid_feedback() placed
after the input was a sibling of the wrapper, never of the control, and
stayed display: none — the pattern the forms vignette and the demo app
both showed could not work. bs_feedback(input, valid =, invalid =) inserts
the messages next to the control itself (once after the last option, for a
choice group), takes an optional initial state =, and
set_bs_validation(id, state, message) switches the state and the message
text from the server.
bs_download_button() and bs_download_link() wrap
shiny::downloadButton() / shiny::downloadLink(). shiny hardcodes the
Bootstrap 3 class .btn-default on the download button, which has no
Bootstrap 5 equivalent and leaves the button unstyled; the wrapper swaps it
for a real 5.3 variant and takes color / outline / size like
bs_button(). shiny's download plumbing (the shiny-download-link class,
href / target / download and the auto-enable dance) is left intact, so
the server side stays a plain shiny::downloadHandler(). icon defaults to
NULL rather than shiny's Font Awesome icon, which is outside Bootstrap 5.
inst/examples/quakewatch: Quake Watch, a realistic demo (a seismic monitor
for the Fiji region built on datasets::quakes) complementing the exhaustive
widget catalogue in inst/examples/demo. It exercises the designer hand-off
(bootstrict_theme(variables = "_variables.scss")), UI-declared overlays
(offcanvas filter drawer, modal event records, toasts), state-reporting
components driven by update_bs_*(), and the 5.3 surface (colour modes,
.nav-underline, .progress-stacked). Run it with
shiny::runApp(system.file("examples/quakewatch", package = "bootstrict")).The package now targets Bootstrap 5.3 (5.3.8, the release it vendors) instead of 5.2, resolving the former 5.2-markup / 5.3-runtime split.
bs_page() / bs_page_fluid() / bs_page_fillable() gain
color_mode (initial data-bs-theme on the page body) and the new
set_bs_color_mode() switches it from the server. The dark variants of
navbar (theme = "dark"), dropdown and carousel (dark = TRUE) and the
close button (white = TRUE) now emit data-bs-theme="dark" — their
5.2-era classes (.navbar-dark, .dropdown-menu-dark, .carousel-dark,
.btn-close-white) are deprecated in 5.3.role="progressbar" and the aria-value*
attributes live on the .progress track (which now carries the id), the
inner .progress-bar is purely visual, and passing several bars to
bs_progress() renders a .progress-stacked group. bs_progress_bar()
gains aria_label.bs_nav() / bs_tabset() accept type = "underline",
bs_img() gains object_fit (.object-fit-*), bs_icon_link() renders
the icon-link helper, and bs_navbar(bg =) accepts "body",
"body-secondary", "body-tertiary", "white", "black",
"transparent".bs_card_subtitle() uses
text-body-secondary (.text-muted is deprecated), bs_modal_title() is
an <h1 class="modal-title fs-5"> again, tab panes carry tabindex="0",
tooltips/popovers use data-bs-title (no more native-tooltip flash), and
help = text is wired to its control via aria-describedby.Bootstrap is vendored. The package no longer depends on bslib: it
ships Bootstrap 5.3.8 under inst/lib/bootstrap and compiles it with
sass, so the version reaching the browser is fixed here rather than by
whichever bslib happens to be installed. bslib::bs_theme(version = 5)
only ever meant "the Bootstrap 5 branch bundled by this bslib", which a
future release could move to 5.4. bootstrap_version() reports the
vendored release, and bootstrap_dep() returns its HTML dependency for a
UI built by hand.
Bootstrap's own build runs autoprefixer after Sass and sass has no such
step, so the vendored .scss carries the prefixes, generated with
Bootstrap's own browserslist targets (see dev/vendor-bootstrap.R).
bootstrict_theme() returns a bootstrict_theme rather than a
bslib::bs_theme(), and takes only ... and variables=. The arguments
that were bslib's rather than SASS's are gone: bootswatch= and preset=
(Bootswatch ships with bslib, not with Bootstrap), along with the
shorthands bg, fg, base_font, code_font, heading_font,
font_scale and brand — set the corresponding Bootstrap variables
instead ("body-bg", "body-color", "font-family-base", ...). A
bslib::bs_theme() passed to bs_page(theme =) is now rejected.
bs_date_input() and bs_date_range_input() are native
<input type="date"> fields and no longer delegate to shiny::dateInput(),
which loads bootstrap-datepicker — a third-party stylesheet whose calendar
markup (.datepicker, .datepicker-days, …) appears nowhere in the
Bootstrap documentation and which a designer's SASS sheet cannot reach. That
contradicted the one promise the package makes, so it is gone; the browser
now supplies the calendar, as the Bootstrap 5.3 forms page has it. No
third-party widget library is shipped any more, and a test enforces that.
What that costs: format, language, weekstart and datesdisabled are
gone, since the browser owns the presentation and Bootstrap offers no way to
ask it for another. shiny::updateDateInput() no longer reaches these
controls — use update_bs_date_input() / update_bs_date_range_input(),
which take NA to clear a field and NULL to leave it alone. input$id is
still a Date, NA while the field is empty, and a range is still a
length-2 Date. bs_date_range_input() gains separator, and the
container now carries the id while the field takes a derived one, so the
two no longer collide.
bs_img(), bs_card_img() and bs_figure_img() default alt to ""
rather than NULL. An <img> with no alt attribute at all is announced
by its file name; an empty one marks the image decorative, which is the
right default for one the caller did not describe.
bs_file_input() emits the Bootstrap 5.3 markup: a plain
<input class="form-control" type="file">. shiny builds the Bootstrap 3
compound widget instead — a "Browse" button beside a readonly text box
showing the file name, with the real input hidden off-screen — which is not
in the Bootstrap 5.3 docs at all, so a designer had no way to draw it. Only
shiny's own element and its progress bar are kept, so uploads and input$id
are unchanged; the browser draws the button and the file name itself. The
button_label and placeholder arguments are gone with the widget they
configured, and size and help are now accepted like on the other
controls.
bs_nav_dropdown(id =) now sets the id on the root <li> rather than on
the toggle <a>, so it addresses the widget the way every other interactive
constructor does and the new state reporting and server helpers can find it.
This only affects code written against the unreleased bs_nav_dropdown().
bs_table(data) now renders a data frame's row names as the reference
<th scope="row"> header cell, so bs_table(head(mtcars)) keeps the car
names it used to drop. This adds a leading column for frames that carry real
row names; automatic row names (a tibble, a freshly built data frame) are
unaffected. The new rownames argument forces the behaviour either way.
bs_page_fillable() loses its fillable argument: bslib::page_fillable()
has no such parameter, so the value leaked into the page markup as an
invalid fillable HTML attribute and controlled nothing.
bs_modal(backdrop = FALSE) now renders a modal with no backdrop
(data-bs-backdrop="false"), matching Bootstrap and bs_offcanvas().
Use backdrop = "static" for a backdrop that does not dismiss on outside
click (the previous behaviour of FALSE).
bs_input_group() and bs_floating_label() now raise an error when given
an input whose Shiny binding lives on its container (bs_date_input(),
bs_date_range_input(), bs_file_input(), bs_radio_input(),
bs_checkbox_group_input()). Previously they silently emitted a dead
control with no id, binding, or dependencies.
Layout constructors now validate their scales instead of emitting
non-existent classes: bs_col() spans (1-12, "auto", TRUE),
bs_row(cols=) (1-6, "auto"), gutters and stack gap (0-5), offset
(0-11), order (0-5, "first", "last"), heading levels (1-6),
bs_pagination_numbered(current=) (1..n), bs_color_input(value=)
(#rrggbb), and bs_tabset(selected=) must match a panel value.
bs_notify_toast() requires plain-text body/title (they are rendered
via textContent; tags now raise an error instead of displaying raw markup
or [object Object]), and its ... must be empty.
The natively built controls no longer carry shiny's classes.
bs_range_input(), bs_color_input(), bs_date_input(),
bs_date_range_input() and the two toggle-button controls emitted
.shiny-input-container and .form-group although they build their own
markup and their own binding. Neither is read by shiny's JavaScript, but
shiny's stylesheet caps .shiny-input-container at width: 300px — so a
segmented button group was clipped to 300px by CSS that is not Bootstrap's,
and .form-range was not the full-width control Bootstrap draws.
.form-group is a dead Bootstrap 3 class besides. bs_file_input() keeps
.form-group alone: shiny's binding finds the upload progress bar with
closest("div.form-group").
data-bs-theme is now set on the document root rather than the <body>.
Bootstrap's color-scheme declaration is what tells the browser to paint
scrollbars and native controls dark, and it only reaches them from <html>.
Dropdown options now reach the element Bootstrap reads them from. Passing
data-bs-auto-close through ... put it on the .dropdown wrapper, while
Bootstrap only ever looks at the toggle, so it did nothing. auto_close,
offset and reference are arguments and land on the toggle.
bs_carousel() no longer emits a duplicated data-bs-ride. Passing it
through ... produced data-bs-ride="carousel true", which is not a value.
autoplay = "resume" is Bootstrap's "true", and wrap, keyboard,
pause and touch are arguments.
A batch of small markup defects:
bs_tab_panel() and bs_accordion_panel() applied their named ... as
body text instead of attributes, so data-bs-theme = "dark" showed up as
visible text in the panel.bs_tab_panel(span("Home"), …) reported
data-value="<span>Home</span>". The value is now taken from
the title's text, and a title with no text asks for an explicit value.bs_input_group() dropped the .form-text node but kept the control's
aria-describedby, leaving a dangling ARIA reference.bs_floating_label() put a placeholder on a <select>, which has no
such attribute.bs_collapse_trigger() given several targets kept only the first in
data-bs-target while listing them all in aria-controls; Bootstrap
resolves that attribute with querySelectorAll, so the documented
multiple-target pattern now works.bs_scrollspy() inside a hidden tab pane never activated and reported
nothing: Bootstrap measured a zero-height container. It is refreshed when
its tab is shown.bs_nav_link(disabled = TRUE) kept href="#" without tabindex="-1", so
a disabled link stayed keyboard-activatable.bs_table(align =) was not validated, so any string became a
non-existent .align-* class.bs_modal() had no accessible name at all;
aria-labelledby was only ever set by the title shortcut, and now
points at a composed bs_modal_title().Forgetting the leading id of an interactive widget now raises an error
instead of rendering silently. bs_tabset(), bs_accordion(),
bs_carousel(), bs_collapse(), bs_modal(), bs_offcanvas() and
bs_toast() take their id first, so the first child landed on id:
bs_tabset(bs_tab_panel("A", "a")) rendered an empty <ul> whose id was
the deparsed panel object. On an API whose central convention is "the id
comes first", that is the mistake everyone makes.
bs_tabset(), bs_accordion(), bs_carousel() and bs_progress() accept
children built with lapply(). Generating panels in a loop is the usual
thing to do in a data-driven app, and dev/CONVENTIONS.md says to let
htmltools flatten lists, but each constructor validated the type of its
children first and so saw the list itself:
bs_tabset("t", lapply(...)) failed with "All ... arguments must be
bs_tab_panel()s". Children are now flattened before the check, without
descending into a panel object, which is itself a classed list.
bs_date_range_input() no longer renders small whatever you asked, and its
separator is a real Bootstrap 5 add-on. shiny hardcodes .input-group-sm on
the group, and wraps the " to " text in a Bootstrap 3
<span class="input-group-addon input-group-prepend input-group-append">;
add-ons must be direct children of .input-group under Bootstrap 5, or
the corner rounding between the two fields breaks. The size is now decided
by a size argument.
shiny::updateRadioButtons() and shiny::updateCheckboxGroupInput() no
longer strip the Bootstrap 5 markup off bs_radio_input() and
bs_checkbox_group_input(). Both replace the whole options block with HTML
generated server-side by shiny:::generateOptions(), which has no theme
branch and always emits Bootstrap 3 (<div class="radio"><label><input>);
bootstrict's enhancement ran once, at render time, so the first update with
choices silently lost .form-check, .form-check-input and
.form-check-label. The classes are now put back client-side, which keeps
the documented promise that shiny's own updaters work unchanged — inline
and reverse cannot be recovered from the replaced HTML, so they are
recorded on the container.
The package tarball no longer ships an internal Posit Connect deployment
record (inst/examples/quakewatch/rsconnect/…/quakewatch.dcf, carrying a
server host name and a user name). .Rbuildignore now excludes any
inst/examples/*/rsconnect directory.
bs_table() no longer renders the wrong values for a tibble. Cells were
extracted with data[i, j], which drops to a vector for a data.frame but
keeps a 1x1 frame for a tibble, so as.character() rendered the underlying
storage: a factor as its integer code, a Date as its day number. Cells are
now read from the column.
bs_table() formats numbers the way a reader expects. as.character()
rendered 100000 as "1e+05" and 1/3 with fifteen significant digits.
An .input-group-text addon holding a checkbox is no longer destroyed.
bs_input_group() descended into any child containing a
.shiny-input-container and returned the first control it found, discarding
everything around it, so
bs_input_group(bs_input_group_text(bs_checkbox_input("cb", NULL)), …) lost
its <span class="input-group-text"> and left the checkbox as a bare
sibling of the text input. Addons are now passed through untouched, and
bs_input_group_text() builds Bootstrap's documented addon: a lone
.form-check-input.mt-0, without the .shiny-input-container and
.form-check wrappers whose indent is meant for a labelled control in a
form.
bs_list_unstyled() and bs_list_inline() no longer nest an <li> inside
an <li>. Every child was wrapped in a fresh <li>, including one that
already was an <li> — the usage the content vignette documents for richer
items. The HTML parser closes the outer item at the inner start tag, so what
survived was an empty <li class="list-inline-item"> followed by a bare
<li> carrying none of the list's classes, which breaks the inline layout
and made the Bootstrap nested-list example unreachable. An <li> child is
now passed through (gaining .list-inline-item where it applies), and a
child that is a bare list (an lapply() result, a tagList()) is expanded
into one item per element instead of being wrapped whole in one <li>.
bs_progress(height =) no longer wipes out a stacked group. Each segment
received two separate style attributes, and htmltools joins duplicated
attributes with a space rather than "; ", so the rendered attribute was
style="width: 15% height: 10px" — a single malformed declaration the
browser drops entirely. The segments came out zero-width (invisible) as soon
as height was supplied. Width and height are now one declaration.
The close button of a responsive bs_offcanvas() now closes it. A
responsive panel carries .offcanvas-{bp} instead of .offcanvas, and
Bootstrap's dismiss handler resolves its target as
getElementFromSelector(this) || this.closest(".offcanvas"): with no
explicit target it found nothing, threw
TypeError: Cannot read properties of undefined (reading 'backdrop') and
left the panel open. The header close button now carries an explicit
data-bs-target (escaped, so module ids work), which is harmless on a plain
offcanvas since it resolves to the same element.
A hand-composed bs_modal() no longer nests its header and footer inside a
.modal-body. bs_modal()'s own documentation invites composing the dialog
with bs_modal_header() / bs_modal_body() / bs_modal_footer(), but every
unnamed child was wrapped in a .modal-body regardless, so the documented
path produced .modal-content > .modal-body > (.modal-header, .modal-body, .modal-footer) — invalid structure that breaks the sticky header and the
scrollable body. Children that already carry one of those classes are now
emitted as siblings of .modal-content; bare children are still wrapped, in
place, so the two styles can be mixed.
Re-rendering a bs_modal() or bs_offcanvas() while it is open no longer
leaves the page permanently unscrollable. Both bindings tore the widget down
with inst.hide(); inst.dispose(); in the same tick, but hide() is
transition based: the synchronous dispose() aborted Bootstrap's teardown,
so the backdrop node was removed while body.modal-open and the inline
overflow: hidden; padding-right: … scroll lock stayed behind. Any overlay
living inside a renderUI() froze the page as soon as it was re-rendered
while shown. Disposal now waits for hidden.bs.modal /
hidden.bs.offcanvas (Bootstrap fires it on a timeout even for a detached
element), and a safety net drops the body scroll lock once nothing is shown.
bootstrict_theme() now puts each value in the Sass layer that can compile
it, instead of handing everything to bslib::bs_theme() as a named
argument. Two forms used to fail outright: a theme colour defined from
another variable — the way Bootstrap ships its own defaults —
(bootstrict_theme(secondary = "$gray-600")) aborted on bslib's HTML-colour
validation, and any value derived from a Bootstrap variable
("link-hover-color" = "shade-color($primary, 20%)") aborted at compile
time with Undefined variable: "$primary", because named arguments land in
the defaults layer, which is emitted before Bootstrap's own variables.
Values built from literals or from the sheet's own variables now go to the
defaults layer, in sheet order, so $primary: $brand-orange works and still
feeds $theme-colors; values referring to one of Bootstrap's variables go to
the declarations layer, where those exist. bs_theme() keeps the arguments
that are not Sass variables (bg, fg, the fonts, preset, bootswatch).
A theme colour redefined from one of Bootstrap's own variables now compiles
but lands after $theme-colors is built, so it does not restyle
.btn-secondary; this is documented on bootstrict_theme().
parse_scss_variables() no longer drops most of a designer's sheet. It
matched $name: value; line by line, so any declaration spanning several
lines matched nothing and was discarded without a warning — that is the
shape of every Bootstrap map ($theme-colors, $grid-breakpoints,
$spacers, $container-max-widths, $font-sizes, $utilities). On
Bootstrap's own _variables.scss it read 916 of 963 declarations and
theme-colors was absent. Stripping // comments with no notion of strings
also truncated any value containing one, silently losing $web-font-path
and url("https://…"); a ; inside a quoted string cut the value short;
and a final declaration with no trailing ; was lost.
The file is now scanned instead of split into lines: declarations may span
any number of lines, ; / // / /* */ inside a quoted string or an
unquoted url() are read as data, #{} interpolation is not mistaken for a
rule block, and a rule block no longer bleeds into the declaration that
follows it.
bs_tooltip() / bs_popover() no longer disable the Shiny input they
decorate. Both were initialised through a Shiny.InputBinding, and since
Shiny binds at most one input per element and later registrations take
precedence, the bootstrict binding claimed the element and the real one never
bound: bs_tooltip(bs_button("save", "Save"), "Ctrl+S") left input$save
permanently NULL. Tooltips and popovers are now initialised from the DOM
(an initial sweep plus a MutationObserver, so renderUI() / insertUI()
content is still covered) and disposed when their element is removed.
bs_tooltip() / bs_popover() no longer break the tag they decorate when it
is already a data-API trigger (bs_modal_trigger(), bs_offcanvas_trigger(),
bs_collapse_trigger(), a dropdown toggle…). They used to append a second
data-bs-toggle value (e.g. "offcanvas tooltip"), which Bootstrap's exact
[data-bs-toggle="offcanvas"] delegated selector no longer matched — the
trigger went dead. The existing attribute is now left untouched; tooltip and
popover initialisation never relied on it (it is driven by
data-bootstrict-tip).
Named arguments in ... are now applied to the documented element:
the bs_dropdown() wrapper (they were rendered as visible page text), the
bs_modal() root (they landed on .modal-body), and the bs_navbar()
<nav> (they landed on the collapse <div>).
Extra attributes passed to bs_checkbox_input() / bs_switch_input() are
no longer silently dropped, and bs_file_input()'s ... now lands on the
real <input type="file"> instead of the readonly display box (internal
has_class() only saw the first of several class attribute entries).
Attributes passed through ... now replace a same-named attribute set by
shiny instead of merging with it (bs_text_input("x", type = "email") no
longer renders the invalid type="text email").
First server-driven update on a never-toggled collapse/accordion panel no
longer does the opposite of what was asked: Bootstrap Collapse instances are
now created with {toggle: false} (the constructor default toggle: true
toggled the panel before the requested action ran).
Responsive offcanvas (bs_offcanvas(responsive =)) now binds its state
input: the JS selector matched .offcanvas only, never .offcanvas-{bp},
so input$id was never registered. Above the breakpoint the inline-shown
panel now reports TRUE.
bs_carousel(autoplay = FALSE) now omits data-bs-ride entirely; it used
to emit data-bs-ride="true", which resumes autoplay after the first user
interaction.
update_bs_list_group(id, selected = NULL) is now the documented no-op; it
used to clear the whole selection and reset input$id. A selected value
matching no item warns and leaves the selection unchanged.
update_bs_accordion(): open = TRUE now opens all panels (it used to
send the literal string "TRUE"); open/close = FALSE are no-ops.
bs_button(href =, disabled = TRUE) now renders the .disabled class and
tabindex="-1" (the anchor was still clickable).
Accordion state no longer leaks between nested accordions (panel discovery
and Bootstrap events are now scoped to the accordion's own panels), and an
accordion with every panel closed reports NULL instead of list().
Progress: percentages are clamped to 0-100 as documented; a min/max-only
update recomputes the width; a colour update no longer strips
bg-opacity-* / bg-gradient classes (see also the Bootstrap 5.3 markup
restructure above).
Ids containing CSS-special characters (e.g. dotted module namespaces) no
longer break declarative wiring: every generated data-bs-target /
data-bs-parent / trigger href selector is now CSS-escaped.
Dynamic UI lifecycle: Bootstrap instances are disposed when Shiny unbinds a
widget (modals/offcanvas hide first — no more stuck backdrops after a
renderUI() re-render), bs_notify_toast() disposes each toast on hide
(it leaked a detached DOM node and instance per notification), the
list-group click handler unbinds cleanly (it used to stack up across
unbind/rebind cycles), tooltip/popover auto-ids no longer collide (a shared
timestamp gave duplicate ids) and their instances are disposed on unbind,
and carousels/scrollspys inserted via renderUI() are now initialised at
bind time (Bootstrap only scans on page load).
bs_range_input() dragging is debounced as documented (input events now
route through the rate policy; change still submits immediately), and the
range/color JS selectors are scoped to bootstrict's own data-bootstrict
marker so hand-written Bootstrap markup is not hijacked.
bs_range_input() and bs_color_input() participate in Shiny bookmarking
(restoreInput()).
parse_scss_variables() strips multi-line /* ... */ comments (variables
inside a commented block were parsed as real) and reads every declaration
on a line, not just the first.
use_bootstrict_golem() scaffolds _variables.scss (SCSS syntax, which
parse_scss_variables() reads) instead of _variables.sass (indented
syntax, which silently parsed to nothing).
Radio/checkbox group labels get .form-label, per-option and single
checkbox labels get .form-check-label, and bs_file_input() drops the
leftover Bootstrap 3/4 markup (.input-group-btn wrapper, BS3 progress
animation classes).
Selectable / action list groups no longer emit invalid HTML (<li> items
inside their <div> container are converted to <div>s); disabled anchor
items get tabindex="-1".
Modals and offcanvas with a title now wire aria-labelledby to the title
element; vertical tabsets set aria-orientation="vertical";
bs_collapse_trigger() gains expanded for a correct initial
aria-expanded/.collapsed state; bs_breadcrumb(divider =) escapes
quotes/backslashes.
Responsive bs_dropdown(align = list(...)) now sets
data-bs-display="static" on the toggle, without which Bootstrap ignores
the responsive alignment classes.
update_bs_list_group(id, selected = character(0)) clears the selection
(deselects every item and resets input$id to NULL). selected = NULL
remains the no-op that leaves the current selection untouched, so a group
can now be cleared server-side without the surprise reset that the pre-NULL
no-op behaviour carried.bs_checkbox_input(), bs_switch_input(), bs_date_input() and
bs_date_range_input() gain the help argument the other inputs already
had.bs_notify_toast() gains autohide (use FALSE for a persistent
notification); header-less notifications now include a close button (they
were undismissable), dark-background notifications get the dark-context
close button (data-bs-theme="dark"), and the auto-created container is
an aria-live region.bs_scrollspy() gains an id (auto-generated by default) and reports the
active section's link as input$id.bs_offcanvas_trigger() delegates to bs_button() like
bs_modal_trigger() (it was a bare unstyled .btn).console.warn with the
offending id instead of failing silently (the most common module
namespacing mistake becomes visible).bootstrict_dep() is built once per session and cached.