From 97016b8725ad1ad508e87abebec36d59c4057e8b Mon Sep 17 00:00:00 2001 From: E Nelson Date: Mon, 16 Mar 2026 11:11:09 -0400 Subject: [PATCH 01/49] Add design spec for toolbar_download_button() Addresses issue #1292 - adds a download button styled for toolbars. Co-Authored-By: Claude Opus 4.6 --- ...26-03-16-toolbar-download-button-design.md | 219 ++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 design/2026-03-16-toolbar-download-button-design.md diff --git a/design/2026-03-16-toolbar-download-button-design.md b/design/2026-03-16-toolbar-download-button-design.md new file mode 100644 index 000000000..303cffe97 --- /dev/null +++ b/design/2026-03-16-toolbar-download-button-design.md @@ -0,0 +1,219 @@ +# toolbar_download_button() Design Spec + +**Date:** 2026-03-16 +**Issue:** https://github.com/rstudio/bslib/issues/1292 +**Status:** Approved + +## Summary + +Add `toolbar_download_button()` to bslib's toolbar component family, providing a download button styled consistently with other toolbar inputs for use in card headers, footers, and other toolbar contexts. + +## Motivation + +Users want an elegant way to add download functionality to card toolbars. Currently, using Shiny's `downloadButton()` in a toolbar requires manual styling workarounds (adding classes, inline styles, and ARIA attributes). A dedicated `toolbar_download_button()` would provide first-class support with proper styling and accessibility out of the box. + +## Design + +### Function Signature + +```r +toolbar_download_button <- function( + outputId, + label = "Download", + icon = shiny::icon("download"), + show_label = FALSE, + tooltip = !show_label, + ..., + disabled = FALSE, + border = FALSE +) +``` + +#### Parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `outputId` | character | required | The download output ID (connects to `downloadHandler` in server) | +| `label` | character/tag | `"Download"` | Button label text; used for tooltip when `show_label = FALSE` | +| `icon` | icon | `shiny::icon("download")` | Icon to display; can be overridden with custom icon | +| `show_label` | logical | `FALSE` | Whether to show label text (icon-only by default) | +| `tooltip` | logical/character | `!show_label` | Tooltip behavior; `TRUE` shows label as tooltip, `FALSE` disables, or custom string | +| `...` | | | Additional HTML attributes passed to the `` tag | +| `disabled` | logical | `FALSE` | Initial disabled state. Since `` tags have no native `disabled` attribute, this adds `class="disabled"`, `aria-disabled="true"`, and `tabindex="-1"` | +| `border` | logical | `FALSE` | Show border around button | + +#### Return Value + +An `` tag suitable for use in a `toolbar()`, styled to match other toolbar components. + +### HTML Structure + +The generated HTML follows the same accessibility patterns as `toolbar_input_button()`: + +```html + + + + + + + + + + + + +``` + +Key structural decisions: +- **Built directly with `tags$a()`** - NOT wrapping `shiny::downloadButton()` or `shiny::actionButton()`. We construct the `` tag from scratch to have full control over structure. +- Uses `bslib-toolbar-download-button` class (new) plus `shiny-download-link` (Shiny's download machinery) +- Nested `span.action-icon > span.bslib-toolbar-icon` and `span.action-label > span.bslib-toolbar-label` - These wrappers are added manually to match the structure that `actionButton()` generates for `toolbar_input_button`, ensuring CSS rules apply consistently to both. The `aria-hidden` and `style` attributes go on the inner `.bslib-toolbar-icon` span, matching the pattern from `toolbar_input_button()` (see R/toolbar.R:430-435). +- Same `data-type` attribute pattern (`"icon"`, `"label"`, `"both"`) for CSS targeting +- `aria-labelledby` points to the label span for screen reader support +- Tooltip wrapper follows same pattern as `toolbar_input_button` + +### Update Function + +```r +update_toolbar_download_button <- function( + outputId, + disabled = NULL, + session = get_current_session() +) +``` + +Supports updating only the `disabled` state (not label/icon), as download buttons rarely need dynamic updates beyond enable/disable. + +#### Implementation Pattern + +Follows the same pattern as `toolbar_input_button` and bslib's tooltip/popover components: + +**R side:** +```r +update_toolbar_download_button <- function( + outputId, + disabled = NULL, + session = get_current_session() +) { + message <- dropNulls(list(disabled = disabled)) + session$sendInputMessage(outputId, message) +} +``` + +**TypeScript side:** A minimal input binding that handles `receiveMessage`: + +```typescript +class BslibToolbarDownloadButtonBinding extends InputBinding { + find(scope: HTMLElement) { + return $(scope).find(".bslib-toolbar-download-button"); + } + + getValue(el: HTMLElement) { + return null; // Not used as input + } + + receiveMessage(el: HTMLElement, message: { disabled?: boolean }) { + if (hasDefinedProperty(message, "disabled")) { + if (message.disabled) { + el.classList.add("disabled"); + el.setAttribute("aria-disabled", "true"); + el.setAttribute("tabindex", "-1"); + } else { + el.classList.remove("disabled"); + el.removeAttribute("aria-disabled"); + el.removeAttribute("tabindex"); + } + } + } +} + +registerBinding(BslibToolbarDownloadButtonBinding, "toolbar-download-button"); +``` + +**Why this works:** This pattern (input binding for a non-input component) is established precedent in bslib. Both `BslibTooltip` and `BslibPopover` set `static isShinyInput = true` to enable `sendInputMessage()` for server-to-client updates, even though they're not traditional inputs. Similarly, `toolbar_input_button` uses a standalone `InputBinding` class (see `toolbarInputButton.ts`). We follow the same standalone binding approach here. + +## Usage Example + +```r +library(shiny) +library(bslib) + +ui <- page_fluid( + card( + card_header( + "Flower Data", + toolbar( + align = "right", + toolbar_download_button("download_data", label = "Download") + ) + ), + card_body( + reactable::reactable(iris) + ) + ) +) + +server <- function(input, output, session) { + output$download_data <- downloadHandler( + filename = function() { + paste("iris-", Sys.Date(), ".csv", sep = "") + }, + content = function(file) { + write.csv(iris, file, row.names = FALSE) + } + ) +} + +shinyApp(ui, server) +``` + +## Files to Modify/Create + +| File | Action | Description | +|------|--------|-------------| +| `R/toolbar.R` | Modify | Add `toolbar_download_button()` and `update_toolbar_download_button()` | +| `srcts/src/components/toolbarDownloadButton.ts` | Create | New input binding for update handling | +| `inst/components/scss/toolbar.scss` | Modify | Update `data-type` attribute selectors (lines ~71, ~78, ~85) to include `.bslib-toolbar-download-button` alongside `.bslib-toolbar-input-button`. Base button styles use Bootstrap `.btn` classes. | +| `tests/testthat/test-toolbar.R` | Modify | Add tests for new functions | +| `man/toolbar_download_button.Rd` | Generated | Roxygen-generated documentation | +| `NAMESPACE` | Generated | Export new functions | + +## Testing Plan + +**Unit tests (automated):** +1. **Snapshot test** - Verify HTML structure matches expected output +2. **Parameter validation** - Test that `outputId` is required, label defaults work +3. **Tooltip behavior** - Test tooltip is added when `show_label = FALSE`, not added when `show_label = TRUE` +4. **Border/disabled states** - Test class application +5. **Icon override** - Test custom icon replaces default + +**Integration test (manual):** +6. **Download functionality** - Manual verification that download works in a Shiny app. Automated download testing is complex due to browser security restrictions; manual testing via the example app (`shiny::runExample("toolbar", package = "bslib")`) is sufficient. + +## Accessibility + +- `aria-labelledby` points to label span for screen reader support +- Tooltip provides accessible name when label is visually hidden +- Disabled state uses both `disabled` class and `aria-disabled="true"` +- Icon wrapped with `aria-hidden="true"` to prevent duplicate announcements + +## Alternatives Considered + +**Custom Download Input Binding:** Create a ` + # toolbar_input_button() tooltip parameter @@ -391,7 +391,7 @@ Output - + + + + + + + + + tag, not a + # toolbar_input_button() tooltip parameter @@ -489,3 +489,20 @@ +# toolbar_download_button() border = TRUE + + Code + show_raw_html(toolbar_download_button("dl_border", label = "Download", + show_label = TRUE, border = TRUE)) + Output + + + + + + Download + + + diff --git a/tests/testthat/test-toolbar.R b/tests/testthat/test-toolbar.R index ddc7aea93..c5fcbfaa1 100644 --- a/tests/testthat/test-toolbar.R +++ b/tests/testthat/test-toolbar.R @@ -162,13 +162,13 @@ test_that("toolbar_input_button() tooltip parameter", { ) ) - # show_label = TRUE means tooltip = FALSE by default + # show_label = TRUE means tooltip = FALSE by default; no bslib_fragment wrapper btn_no_tooltip <- toolbar_input_button( id = "label_visible", label = "Visible Label", show_label = TRUE ) - expect_false(inherits(btn_no_tooltip, "bslib_tooltip")) + expect_false(inherits(btn_no_tooltip, "bslib_fragment")) # But you can explicitly add tooltip when show_label = TRUE expect_snapshot_html( @@ -182,6 +182,13 @@ test_that("toolbar_input_button() tooltip parameter", { ) }) +test_that("toolbar_input_button() aborts when show_label = FALSE and no icon", { + expect_error( + toolbar_input_button("btn", label = "X", show_label = FALSE, icon = NULL), + "icon.*must be provided" + ) +}) + test_that("toolbar_input_button() validates label for accessibility", { # Empty label should warn when show_label = FALSE expect_warning( @@ -527,8 +534,10 @@ test_that("toolbar_input_select() icon parameter", { tooltip = FALSE ) ) - html_output <- as.character(select_no_icon) - expect_false(grepl("bslib-toolbar-input-select-icon", html_output)) + # Icon wrapper is always rendered; when no icon is given its only child is NULL + # (CSS hides the span via :empty when there's no rendered content) + icon_span <- tagQuery(select_no_icon)$find(".bslib-toolbar-icon")$selectedTags()[[1]] + expect_null(icon_span$children[[1]]) # With icon expect_snapshot_html( @@ -797,6 +806,19 @@ test_that("toolbar_input_select() validates selected is in choices", { }) }) +test_that("update_toolbar_input_select() warns when selected given without choices", { + session <- list( + sendInputMessage = function(id, message) { + session$last_message <<- message + }, + input = list() + ) + expect_warning( + update_toolbar_input_select("test_id", selected = "B", session = session), + "cannot be set without `choices`" + ) +}) + test_that("update_toolbar_input_select() validates selected is in choices", { session <- list( sendInputMessage = function(id, message) { @@ -1135,19 +1157,47 @@ test_that("toolbar_download_button() custom icon", { ) }) -test_that("toolbar_download_button() warns on invalid enabled value", { - expect_warning( - btn <- toolbar_download_button("dl_bad", enabled = "yes"), +test_that("toolbar_download_button() aborts when show_label = FALSE and no icon", { + expect_error( + toolbar_download_button("dl", icon = NULL, show_label = FALSE), + "icon.*must be provided" + ) +}) + +test_that("toolbar_download_button() label-only type (icon = NULL, show_label = TRUE)", { + btn <- toolbar_download_button("dl_label_only", icon = NULL, show_label = TRUE) + expect_match(htmltools::tagGetAttribute(btn, "data-type"), "label") +}) + +test_that("toolbar_download_button() border = TRUE", { + expect_snapshot_html( + toolbar_download_button("dl_border", label = "Download", show_label = TRUE, border = TRUE) + ) +}) + +test_that("toolbar_download_button() errors on invalid enabled value", { + expect_error( + toolbar_download_button("dl_bad", enabled = "yes"), '`enabled` must be TRUE, FALSE, or "auto"' ) - # Falls back to "auto" — starts disabled, no data-shiny-disable-auto-enable + expect_error( + toolbar_download_button("dl_bad2", enabled = 1), + '`enabled` must be TRUE, FALSE, or "auto"' + ) +}) + +test_that("toolbar_download_button() merges class arg into button class", { + btn <- toolbar_download_button("dl_cls", label = "Export", class = "btn-success") btn_tag <- tagQuery(as.tags(btn))$find("a")$selectedTags()[[1]] - expect_match(htmltools::tagGetAttribute(btn_tag, "class"), "disabled") - expect_null(htmltools::tagGetAttribute(btn_tag, "data-shiny-disable-auto-enable")) + cls <- htmltools::tagGetAttribute(btn_tag, "class") + expect_match(cls, "bslib-toolbar-download-button") + expect_match(cls, "btn-success") +}) +test_that("toolbar_download_button() warns on empty label", { expect_warning( - toolbar_download_button("dl_bad2", enabled = 1), - '`enabled` must be TRUE, FALSE, or "auto"' + toolbar_download_button("dl_empty", label = ""), + "non-empty string label" ) }) @@ -1193,32 +1243,35 @@ test_that("update_toolbar_download_button() updates label, show_label, icon", { expect_equal(session$last_message$showLabel, FALSE) }) -test_that("update_toolbar_download_button() drops NULL args from message", { +test_that("update_toolbar_download_button() sends only specified fields", { session <- list( sendCustomMessage = function(type, message) { + session$last_type <<- type session$last_message <<- message } ) + # Only specified fields appear — NULLs are dropped update_toolbar_download_button("dl_target", disabled = TRUE, session = session) - - # Only `id` and `disabled` should be present. expect_equal(sort(names(session$last_message)), c("disabled", "id")) + + # No args → id-only message on the correct channel + update_toolbar_download_button("dl_target", session = session) + expect_equal(session$last_type, "bslib.toolbar-download-button") + expect_equal(names(session$last_message), "id") + expect_equal(session$last_message$id, "dl_target") }) -test_that("update_toolbar_download_button() with no updates still sends id-only message", { +test_that("update_toolbar_download_button() warns on empty label", { session <- list( sendCustomMessage = function(type, message) { - session$last_type <<- type session$last_message <<- message } ) - - update_toolbar_download_button("dl_target", session = session) - - expect_equal(session$last_type, "bslib.toolbar-download-button") - expect_equal(names(session$last_message), "id") - expect_equal(session$last_message$id, "dl_target") + expect_warning( + update_toolbar_download_button("dl_target", label = "", session = session), + "non-empty string label" + ) }) test_that("check_shiny_supports_download_button_enabled() passes on supported shiny", { From 7ac2b78eb4345d78929158ff901a5e4d3238f37c Mon Sep 17 00:00:00 2001 From: E Nelson Date: Fri, 29 May 2026 17:00:58 -0400 Subject: [PATCH 37/49] docs: use @inheritParams for toolbar_download_button, expand example with shinyApp + downloadHandler --- R/toolbar.R | 44 ++++++++++++---------- man/toolbar_download_button.Rd | 69 +++++++++++++++++++++++----------- 2 files changed, 72 insertions(+), 41 deletions(-) diff --git a/R/toolbar.R b/R/toolbar.R index a1aea032b..a94a5b0e7 100644 --- a/R/toolbar.R +++ b/R/toolbar.R @@ -967,16 +967,7 @@ toolbar_spacer <- function() { #' A download button designed to fit well in small places such as in a [toolbar()]. #' #' @param outputId The download output ID (connects to [shiny::downloadHandler()] in server). -#' @param label The button label. By default, `label` is not shown but is used by -#' `tooltip`. Set `show_label = TRUE` to show the label. -#' @param icon An icon. Defaults to `shiny::icon("download")`. -#' @param show_label Whether to show the label text. If `FALSE` (the default), -#' only the icon is shown. If `TRUE`, the label text is shown alongside the icon. -#' @param tooltip Tooltip text to display when hovering. Can be: -#' * `TRUE` (default when `show_label = FALSE`) - shows tooltip with `label` text -#' * `FALSE` (default when `show_label = TRUE`) - no tooltip -#' * A character string - shows tooltip with custom text -#' @param ... Additional attributes passed to the button tag. +#' @inheritParams toolbar_input_button #' @param enabled Controls the initial enabled/disabled state and whether Shiny #' manages auto-enabling: #' * `"auto"` (default) — button starts disabled; Shiny auto-enables it once @@ -990,22 +981,37 @@ toolbar_spacer <- function() { #' [update_toolbar_download_button()] to manage enabled/disabled state. #' Note: if the button is inside a `renderUI`, re-renders reset it to the #' initial HTML state; use `enabled = FALSE` for persistent manual control. -#' @param border Whether to show a border around the button. #' #' @return Returns a download button suitable for use in a toolbar. #' #' @examplesIf rlang::is_interactive() -#' # Download button in a card toolbar -#' card( -#' card_header( -#' "Flower Data", -#' toolbar( -#' align = "right", -#' toolbar_download_button("download_data", label = "Download") -#' ) +#' library(shiny) +#' library(bslib) +#' +#' ui <- page_fluid( +#' card( +#' card_header( +#' "Flower Data", +#' toolbar( +#' align = "right", +#' toolbar_download_button("download_data", label = "Download CSV") +#' ) +#' ), +#' tableOutput("table") #' ) #' ) #' +#' server <- function(input, output, session) { +#' output$table <- renderTable(head(iris)) +#' +#' output$download_data <- downloadHandler( +#' filename = function() "iris.csv", +#' content = function(file) write.csv(iris, file, row.names = FALSE) +#' ) +#' } +#' +#' shinyApp(ui, server) +#' #' @family toolbar components #' @export toolbar_download_button <- function( diff --git a/man/toolbar_download_button.Rd b/man/toolbar_download_button.Rd index 9c9215f50..66b6e54c0 100644 --- a/man/toolbar_download_button.Rd +++ b/man/toolbar_download_button.Rd @@ -28,22 +28,31 @@ update_toolbar_download_button( \arguments{ \item{outputId}{The download output ID (connects to \code{\link[shiny:downloadHandler]{shiny::downloadHandler()}} in server).} -\item{label}{The button label. By default, \code{label} is not shown but is used by -\code{tooltip}. Set \code{show_label = TRUE} to show the label.} +\item{label}{The input label. By default, \code{label} is not shown but is used by +\code{tooltip}. Set \code{show_label = TRUE} to show the label (see \code{tooltip} for +details on how this affects the tooltip behavior).} -\item{icon}{An icon. Defaults to \code{shiny::icon("download")}.} +\item{icon}{An icon. If provided without \code{show_label = TRUE}, only the icon +will be visible.} \item{show_label}{Whether to show the label text. If \code{FALSE} (the default), -only the icon is shown. If \code{TRUE}, the label text is shown alongside the icon.} +only the icon is shown (if provided). If \code{TRUE}, the label text is shown +alongside the icon. Note that \code{show_label} can be dynamically updated using +\code{\link[=update_toolbar_input_button]{update_toolbar_input_button()}}.} -\item{tooltip}{Tooltip text to display when hovering. Can be: +\item{tooltip}{Tooltip text to display when hovering over the input. Can be: \itemize{ -\item \code{TRUE} (default when \code{show_label = FALSE}) - shows tooltip with \code{label} text +\item \code{TRUE} (default when \code{show_label = FALSE}) - shows a tooltip with the +\code{label} text \item \code{FALSE} (default when \code{show_label = TRUE}) - no tooltip -\item A character string - shows tooltip with custom text -}} +\item A character string - shows a tooltip with custom text +} + +Defaults to \code{!show_label}. When a tooltip is created, it will have an ID of +\code{"{id}_tooltip"} which can be used to update the tooltip text dynamically +via \code{\link[=update_tooltip]{update_tooltip()}}.} -\item{...}{Additional attributes passed to the button tag.} +\item{...}{Additional attributes to pass to the button.} \item{enabled}{Controls the initial enabled/disabled state and whether Shiny manages auto-enabling: @@ -82,23 +91,39 @@ A download button designed to fit well in small places such as in a \code{\link[ }} \examples{ \dontshow{if (rlang::is_interactive()) withAutoprint(\{ # examplesIf} -# Download button in a card toolbar -card( - card_header( - "Flower Data", - toolbar( - align = "right", - toolbar_download_button("download_data", label = "Download") - ) +library(shiny) +library(bslib) + +ui <- page_fluid( + card( + card_header( + "Flower Data", + toolbar( + align = "right", + toolbar_download_button("download_data", label = "Download CSV") + ) + ), + tableOutput("table") ) ) + +server <- function(input, output, session) { + output$table <- renderTable(head(iris)) + + output$download_data <- downloadHandler( + filename = function() "iris.csv", + content = function(file) write.csv(iris, file, row.names = FALSE) + ) +} + +shinyApp(ui, server) \dontshow{\}) # examplesIf} } \seealso{ -Other toolbar components: -\code{\link{toolbar}()}, -\code{\link{toolbar_divider}()}, -\code{\link{toolbar_input_button}()}, -\code{\link{toolbar_input_select}()} +Other toolbar components: +\code{\link[=toolbar]{toolbar()}}, +\code{\link[=toolbar_divider]{toolbar_divider()}}, +\code{\link[=toolbar_input_button]{toolbar_input_button()}}, +\code{\link[=toolbar_input_select]{toolbar_input_select()}} } \concept{toolbar components} From eef37ddb0e67a08ffcb2db6ffd675219459cef8d Mon Sep 17 00:00:00 2001 From: E Nelson Date: Fri, 29 May 2026 17:22:33 -0400 Subject: [PATCH 38/49] fix: guard label_text check with !is.null(label) in update functions; align comments --- R/toolbar.R | 14 ++++++++------ tests/testthat/test-toolbar.R | 9 +++++++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/R/toolbar.R b/R/toolbar.R index a94a5b0e7..c78490e81 100644 --- a/R/toolbar.R +++ b/R/toolbar.R @@ -406,7 +406,6 @@ toolbar_input_button <- function( # Validate that label has text for accessibility label_text <- paste(unlist(find_characters(label)), collapse = " ") - # Verifies the label contains non-empty text if (!nzchar(trimws(label_text))) { warning( "Consider providing a non-empty string label for accessibility." @@ -480,11 +479,13 @@ update_toolbar_input_button <- function( disabled = NULL, session = get_current_session() ) { - label_text <- paste(unlist(find_characters(label)), collapse = " ") - if (!nzchar(trimws(label_text))) { - rlang::warn( - "Consider providing a non-empty string label for accessibility." - ) + if (!is.null(label)) { + label_text <- paste(unlist(find_characters(label)), collapse = " ") + if (!nzchar(trimws(label_text))) { + rlang::warn( + "Consider providing a non-empty string label for accessibility." + ) + } } icon <- validateIcon(icon) @@ -1045,6 +1046,7 @@ toolbar_download_button <- function( if (show_label) "both" else "icon" } + # Validate that label has text for accessibility label_text <- paste(unlist(find_characters(label)), collapse = " ") if (!nzchar(trimws(label_text))) { warning("Consider providing a non-empty string label for accessibility.") diff --git a/tests/testthat/test-toolbar.R b/tests/testthat/test-toolbar.R index c5fcbfaa1..d9192b30a 100644 --- a/tests/testthat/test-toolbar.R +++ b/tests/testthat/test-toolbar.R @@ -999,6 +999,15 @@ test_that("update_toolbar_input_button() warns for blank label", { ) }) +test_that("update_toolbar_input_button() does not warn when label is NULL", { + session <- list( + sendInputMessage = function(id, message) invisible(NULL) + ) + expect_no_warning( + update_toolbar_input_button("test_id", disabled = TRUE, session = session) + ) +}) + test_that("update_toolbar_input_button() can disable and reenable button", { # Mock session that captures sendInputMessage calls session <- list( From c6f0172e6ecd15da4544d30fd8526edb33f3e7cc Mon Sep 17 00:00:00 2001 From: E Nelson Date: Fri, 29 May 2026 17:24:22 -0400 Subject: [PATCH 39/49] =?UTF-8?q?docs:=20remove=20renderUI=20note=20from?= =?UTF-8?q?=20enabled=20param=20=E2=80=94=20general=20Shiny=20behavior,=20?= =?UTF-8?q?not=20specific=20here?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- R/toolbar.R | 2 -- man/toolbar_download_button.Rd | 2 -- 2 files changed, 4 deletions(-) diff --git a/R/toolbar.R b/R/toolbar.R index c78490e81..62d1bb9b6 100644 --- a/R/toolbar.R +++ b/R/toolbar.R @@ -980,8 +980,6 @@ toolbar_spacer <- function() { #' * `FALSE` — button starts disabled with `data-shiny-disable-auto-enable`, #' permanently opting out of Shiny's auto-enable. Use #' [update_toolbar_download_button()] to manage enabled/disabled state. -#' Note: if the button is inside a `renderUI`, re-renders reset it to the -#' initial HTML state; use `enabled = FALSE` for persistent manual control. #' #' @return Returns a download button suitable for use in a toolbar. #' diff --git a/man/toolbar_download_button.Rd b/man/toolbar_download_button.Rd index 66b6e54c0..0b8c7df0e 100644 --- a/man/toolbar_download_button.Rd +++ b/man/toolbar_download_button.Rd @@ -66,8 +66,6 @@ render (preserves state set by e.g. \code{shinyjs::disable()}). \item \code{FALSE} — button starts disabled with \code{data-shiny-disable-auto-enable}, permanently opting out of Shiny's auto-enable. Use \code{\link[=update_toolbar_download_button]{update_toolbar_download_button()}} to manage enabled/disabled state. -Note: if the button is inside a \code{renderUI}, re-renders reset it to the -initial HTML state; use \code{enabled = FALSE} for persistent manual control. }} \item{border}{Whether to show a border around the button.} From 58e486fb6bdbbbd99075491af8de8c9aa8767faf Mon Sep 17 00:00:00 2001 From: E Nelson Date: Fri, 29 May 2026 17:27:29 -0400 Subject: [PATCH 40/49] chore: restore height comment in toolbar button block --- inst/components/scss/toolbar.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/inst/components/scss/toolbar.scss b/inst/components/scss/toolbar.scss index 0b558ad0a..a33e88f1f 100644 --- a/inst/components/scss/toolbar.scss +++ b/inst/components/scss/toolbar.scss @@ -28,11 +28,11 @@ .bslib-toolbar-input-button, .bslib-toolbar-download-button { - // Keep labels and icons centered; height keeps icon-only and label+icon buttons consistent + // Keep labels and icons centered align-items: center; justify-content: center; line-height: 1; // Override Bootstrap's line-height to avoid too much vertical space - height: var(--_toolbar-btn-size); + height: var(--_toolbar-btn-size); // Keep square icon and label + icon button heights consistent display: flex; // Prevent Shiny from overwriting flex centering // Remove margins from action button elements From 741bfeb8be786d11f968b7bf3605e9c99666e9c5 Mon Sep 17 00:00:00 2001 From: E Nelson Date: Fri, 29 May 2026 17:30:20 -0400 Subject: [PATCH 41/49] chore: expand hover/active rules, clarify color comment in toolbar button block --- inst/components/scss/toolbar.scss | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/inst/components/scss/toolbar.scss b/inst/components/scss/toolbar.scss index a33e88f1f..542db6a19 100644 --- a/inst/components/scss/toolbar.scss +++ b/inst/components/scss/toolbar.scss @@ -43,14 +43,24 @@ // Reset Bootstrap's .btn-default styling so toolbar buttons use the // toolbar's own text color and a transparent background, with subtle - // emphasis-color highlights on interaction. + // emphasis-color highlights on interaction. `color: currentColor` is + // repeated on each state to prevent Bootstrap's link hover styles from + // overriding the text color (needed for the -based download button). &.btn-default:not(.btn-primary):not(.btn-secondary):not(.btn-success):not(.btn-danger):not(.btn-warning):not(.btn-info):not(.btn-light):not(.btn-dark) { background-color: transparent !important; background-image: none !important; color: currentColor !important; - &:hover { background-color: rgba(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.08) !important; color: currentColor !important; } - &:active { background-color: rgba(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.16) !important; color: currentColor !important; } + &:hover { + background-color: rgba(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.08) !important; + color: currentColor !important; + } + + &:active { + background-color: rgba(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.16) !important; + color: currentColor !important; + } + &:focus-within { background-color: rgba(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.12) !important; // Match the focus styling used by toolbar buttons (uses emphasis color for both border and shadow) From 63aa1e0f33aea9410b6440375a8741d08075544a Mon Sep 17 00:00:00 2001 From: E Nelson Date: Fri, 29 May 2026 17:34:52 -0400 Subject: [PATCH 42/49] chore: recompile CSS after rebase on main --- inst/components/dist/components.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inst/components/dist/components.css b/inst/components/dist/components.css index a19139bda..c5ff06ded 100644 --- a/inst/components/dist/components.css +++ b/inst/components/dist/components.css @@ -1 +1 @@ -.accordion .accordion-header{font-size:calc(1.325rem + .9vw);margin-top:0;margin-bottom:.5rem;font-weight:400;line-height:1.2;color:var(--bs-heading-color);margin-bottom:0}@media (min-width: 1200px){.accordion .accordion-header{font-size:2rem}}.accordion .accordion-icon:not(:empty){margin-right:0.75rem;display:flex}.accordion .accordion-button:not(.collapsed){box-shadow:none}.accordion .accordion-button:not(.collapsed):focus{box-shadow:var(--bs-accordion-btn-focus-box-shadow)}.bslib-card{overflow:auto}.bslib-card .card-body+.card-body{padding-top:0}.bslib-card .card-body{overflow:auto}.bslib-card .card-body p{margin-top:0}.bslib-card .card-body p:last-child{margin-bottom:0}.bslib-card .card-body{max-height:var(--bslib-card-body-max-height, none)}.bslib-card[data-full-screen="true"]>.card-body{max-height:var(--bslib-card-body-max-height-full-screen, none)}.bslib-card .card-header{display:flex;flex-direction:row;align-items:center;align-self:stretch;min-height:2.5rem;padding-block:calc(var(--bs-card-cap-padding-y) / 2);gap:0.25rem}.bslib-card .card-header>.nav{flex:1;min-width:0}.bslib-card .card-header>.bslib-nav-spacer{margin-left:auto}.bslib-card .card-header .form-group{margin-bottom:0}.bslib-card .card-header .selectize-control{margin-bottom:0}.bslib-card .card-header .selectize-control .item{margin-right:1.15rem}.bslib-card .card-footer{margin-top:auto}.bslib-card .bslib-navs-card-title{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center}.bslib-card .bslib-navs-card-title .nav{margin-left:auto}.bslib-card .bslib-sidebar-layout:not([data-bslib-sidebar-border="true"]){border:none}.bslib-card .bslib-sidebar-layout:not([data-bslib-sidebar-border-radius="true"]){border-top-left-radius:0;border-top-right-radius:0}.bslib-card[data-full-screen="true"]{position:fixed;inset:3.5rem 1rem 1rem;height:auto !important;max-height:none !important;width:auto !important;z-index:1070}.bslib-full-screen-enter{position:absolute;bottom:var(--bslib-full-screen-enter-bottom, 0.2rem);right:var(--bslib-full-screen-enter-right, 0);top:var(--bslib-full-screen-enter-top);left:var(--bslib-full-screen-enter-left);color:var(--bslib-color-fg, var(--bs-card-color));background-color:var(--bslib-color-bg, var(--bs-card-bg, var(--bs-body-bg)));border:var(--bs-card-border-width) solid var(--bslib-color-fg, var(--bs-card-border-color));box-shadow:0 2px 4px rgba(0,0,0,0.15);margin:0.2rem 0.4rem;padding:0.55rem !important;font-size:.8rem;cursor:pointer;opacity:0;z-index:1070}.card:hover>*>.bslib-full-screen-enter,.card:focus-within>*>.bslib-full-screen-enter{opacity:0.6}.card:hover>*>.bslib-full-screen-enter:hover,.card:hover>*>.bslib-full-screen-enter:focus,.card:focus-within>*>.bslib-full-screen-enter:hover,.card:focus-within>*>.bslib-full-screen-enter:focus{opacity:1}.card[data-full-screen="false"]:hover>*>.bslib-full-screen-enter{display:block}.bslib-has-full-screen .bslib-full-screen-enter{display:none !important}.bslib-full-screen-exit{position:relative;top:1.35rem;font-size:0.9rem;cursor:pointer;text-decoration:none;display:flex;float:right;margin-right:2.15rem;align-items:center;color:rgba(var(--bs-body-bg-rgb), 0.8)}.bslib-full-screen-exit:hover{color:rgba(var(--bs-body-bg-rgb), 1)}.bslib-full-screen-exit svg{margin-left:0.5rem;font-size:1.5rem}#bslib-full-screen-overlay{position:fixed;inset:0;background-color:rgba(var(--bs-body-color-rgb), 0.6);backdrop-filter:blur(2px);-webkit-backdrop-filter:blur(2px);z-index:1069;animation:bslib-full-screen-overlay-enter 400ms cubic-bezier(0.6, 0.02, 0.65, 1) forwards}@keyframes bslib-full-screen-overlay-enter{0%{opacity:0}100%{opacity:1}}@media (max-width: 575.98px){.bslib-card[data-full-screen="true"]{inset:2.5rem 0.5rem 0.5rem}.bslib-full-screen-exit{top:0.75rem;margin-right:1.25rem}}.bslib-grid{--_item-column-span: 1;display:grid !important;gap:var(--bslib-spacer, 1rem);height:var(--bslib-grid-height)}.bslib-grid>*{grid-column:auto/span var(--_item-column-span, 1)}.bslib-grid.grid{grid-template-columns:repeat(var(--bs-columns, 12), minmax(0, 1fr));grid-template-rows:unset;grid-auto-rows:var(--bslib-grid--row-heights);--bslib-grid--row-heights--xs: unset;--bslib-grid--row-heights--sm: unset;--bslib-grid--row-heights--md: unset;--bslib-grid--row-heights--lg: unset;--bslib-grid--row-heights--xl: unset;--bslib-grid--row-heights--xxl: unset}.bslib-grid.grid.bslib-grid--row-heights--xs{--bslib-grid--row-heights: var(--bslib-grid--row-heights--xs)}@media (min-width: 576px){.bslib-grid.grid.bslib-grid--row-heights--sm{--bslib-grid--row-heights: var(--bslib-grid--row-heights--sm)}}@media (min-width: 768px){.bslib-grid.grid.bslib-grid--row-heights--md{--bslib-grid--row-heights: var(--bslib-grid--row-heights--md)}}@media (min-width: 992px){.bslib-grid.grid.bslib-grid--row-heights--lg{--bslib-grid--row-heights: var(--bslib-grid--row-heights--lg)}}@media (min-width: 1200px){.bslib-grid.grid.bslib-grid--row-heights--xl{--bslib-grid--row-heights: var(--bslib-grid--row-heights--xl)}}@media (min-width: 1400px){.bslib-grid.grid.bslib-grid--row-heights--xxl{--bslib-grid--row-heights: var(--bslib-grid--row-heights--xxl)}}.bslib-grid>*>.shiny-input-container{width:100%}bslib-layout-columns.bslib-grid{--_item-column-span: 6}bslib-layout-columns[hidden-until-init]>*{display:none}@media (max-width: 767.98px){bslib-layout-columns:where(.bslib-grid)>*{grid-column:1 / -1}}@media (max-width: 575.98px){.bslib-grid{grid-template-columns:1fr !important;height:var(--bslib-grid-height-mobile)}.bslib-grid.grid{height:unset !important}}.bslib-input-submit-textarea{margin:0 auto}.bslib-submit-textarea-container{display:flex;flex-direction:column;gap:0.5rem;padding:0.5rem;border:var(--bs-border-width, 1px) solid var(--bs-gray-500, #ced4da);border-radius:var(--bs-border-radius-sm, 4px);background-color:var(--bs-body-bg, white);transition:border-color 0.2s, box-shadow 0.2s}.bslib-submit-textarea-container:focus-within{border-color:var(--bs-primary, #007bff);box-shadow:0 0 0 var(--bs-focus-ring-width, 0.25rem) var(--bs-focus-ring-color, rgba(13,110,253,0.25))}.bslib-submit-textarea-container>textarea{border:none;resize:none;min-height:1rem;max-height:10rem;background-color:transparent;padding:0;color:var(--bs-body-color, #212529)}.bslib-submit-textarea-container>textarea:focus{outline:none;box-shadow:none}.bslib-submit-textarea-container>footer{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:0.5rem}.bslib-submit-textarea-container .bslib-submit-textarea-btn{margin-left:auto}.bslib-toolbar{display:flex;align-items:center;gap:0.25rem}.bslib-submit-key{border-radius:var(--bs-border-radius-sm, 4px);padding:0.25em 0.5em;font-weight:300;font-size:0.7em;vertical-align:0.15em}:not(.disabled) .bslib-submit-key{background-color:rgba(var(--bs-body-color-rgb, 0, 0, 0), 0.2)}@media (min-width: 576px){.nav:not(.nav-hidden){display:flex !important;display:-webkit-flex !important}.nav:not(.nav-hidden):not(.nav-stacked):not(.flex-column){float:none !important}.nav:not(.nav-hidden):not(.nav-stacked):not(.flex-column)>.bslib-nav-spacer{margin-left:auto !important}.nav:not(.nav-hidden):not(.nav-stacked):not(.flex-column)>.form-inline{margin-top:auto;margin-bottom:auto}.nav:not(.nav-hidden).nav-stacked{flex-direction:column;-webkit-flex-direction:column;height:100%}.nav:not(.nav-hidden).nav-stacked>.bslib-nav-spacer{margin-top:auto !important}}.bslib-page-fill{width:100%;height:100%;margin:0;padding:var(--bslib-spacer, 1rem);gap:var(--bslib-spacer, 1rem)}@media (max-width: 575.98px){.bslib-flow-mobile>.html-fill-item{flex:0 0 auto}.bslib-flow-mobile.bslib-page-sidebar>.html-fill-item,.bslib-flow-mobile.bslib-page-navbar.has-page-sidebar>.html-fill-item{flex:1 1 auto}.bslib-flow-mobile.bslib-page-sidebar>.bslib-sidebar-layout>.main>.html-fill-item,.bslib-flow-mobile.bslib-page-navbar.has-page-sidebar>.html-fill-container>.bslib-sidebar-layout>.main>.html-fill-item{flex:0 0 auto}}.navbar+.container-fluid:has(>.tab-content>.tab-pane.active.html-fill-container){padding-left:0;padding-right:0}.navbar+.container-fluid>.tab-content>.tab-pane.active.html-fill-container{padding:var(--bslib-spacer, 1rem);gap:var(--bslib-spacer, 1rem)}.navbar+.container-fluid>.tab-content>.tab-pane.active.html-fill-container:has(>.bslib-sidebar-layout:only-child){padding:0}.navbar+.container-fluid>.tab-content>.tab-pane.active.html-fill-container>.bslib-sidebar-layout:only-child:not([data-bslib-sidebar-border="true"]){border-left:none;border-right:none;border-bottom:none}.navbar+.container-fluid>.tab-content>.tab-pane.active.html-fill-container>.bslib-sidebar-layout:only-child:not([data-bslib-sidebar-border-radius="true"]){border-radius:0}.navbar+div>.bslib-sidebar-layout{border-top:var(--bslib-sidebar-border)}:root{--bslib-page-sidebar-title-bg: #202020;--bslib-page-sidebar-title-color: #fff}.bslib-page-sidebar>.navbar{--bs-navbar-brand-color: var(--bslib-page-sidebar-title-color);border-bottom:var(--bs-border-width) solid var(--bs-border-color-translucent);background-color:var(--bslib-page-sidebar-title-bg);color:var(--bslib-page-sidebar-title-color)}.bslib-page-sidebar .bslib-page-title{margin-bottom:0;line-height:var(--bs-body-line-height)}@media (max-width: 991.98px){.bslib-page-sidebar>.bslib-sidebar-layout.sidebar-collapsed:not(.sidebar-right)>.main,.bslib-page-navbar>div>.bslib-sidebar-layout.sidebar-collapsed:not(.sidebar-right)>.main{padding-right:var(--_padding)}.bslib-page-sidebar>.bslib-sidebar-layout.sidebar-collapsed.sidebar-right>.main,.bslib-page-navbar>div>.bslib-sidebar-layout.sidebar-collapsed.sidebar-right>.main{padding-left:var(--_padding)}}@media (min-width: 576px){.bslib-sidebar-layout .bslib-page-main.html-fill-container{min-height:var(--bslib-page-main-min-height, 576px)}.bslib-sidebar-layout:not(.sidebar-collapsed) .bslib-page-main.html-fill-container,.bslib-sidebar-layout.transitioning .bslib-page-main.html-fill-container{min-width:var(--bslib-page-main-min-width, 576px)}}.bslib-sidebar-layout{container-type:style;--_transition-duration: 0;--_transition-easing-x: var(--bslib-sidebar-transition-easing-x, cubic-bezier(0.8, 0.78, 0.22, 1.07));--_border: var(--bslib-sidebar-border, var(--bs-card-border-width, var(--bs-border-width)) solid var(--bs-card-border-color, var(--bs-border-color-translucent)));--_border-radius: var(--bslib-sidebar-border-radius, var(--bs-border-radius));--_vert-border: var(--bslib-sidebar-vert-border, var(--_border));--_sidebar-width: var(--bslib-sidebar-width, 250px);--_sidebar-bg: var(--bslib-sidebar-bg, RGBA(var(--bs-body-bg-rgb), 0.05));--_sidebar-fg: var(--bslib-sidebar-fg, var(--_main-fg));--_main-fg: var(--bslib-sidebar-main-fg, var(--bs-body-color));--_main-bg: var(--bslib-sidebar-main-bg, transparent);--_toggle-bg: var(--bslib-sidebar-toggle-bg, rgba(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.1));--_padding: var(--bslib-sidebar-padding, var(--bslib-spacer, 1.5rem));--_icon-size: var(--bslib-sidebar-icon-size, 1rem);--_icon-button-size: var(--bslib-sidebar-icon-button-size, calc(var(--_icon-size, 1rem) * 2));--_padding-icon: calc(var(--_icon-button-size, 2rem) * 1.5);--_toggle-border-radius: var(--bslib-collapse-toggle-border-radius, var(--bs-border-radius, 3px));--_toggle-transform: var(--bslib-collapse-toggle-transform, 0deg);--_toggle-transition-easing: var(--bslib-sidebar-toggle-transition-easing, cubic-bezier(1, 0, 0, 1));--_toggle-right-transform: var(--bslib-collapse-toggle-right-transform, 180deg);--_toggle-position-y: calc(var(--_js-toggle-count-this-side, 0) * calc(var(--_icon-size) + var(--_padding)) + var(--_icon-size, 1rem) / 2);--_toggle-position-x: calc(-2.5 * var(--_icon-size) - var(--bs-card-border-width, 1px));--_mobile-max-height: var(--bslib-sidebar-mobile-max-height, var(--bslib-sidebar-max-height-mobile));--_sidebar-mobile-opacity: var(--bslib-sidebar-mobile-opacity);--_main-mobile-expanded-opacity: var(--bslib-sidebar-main-mobile-expanded-opacity, 0);--_sidebar-mobile-max-width: var(--bslib-sidebar-mobile-max-width);--_sidebar-mobile-box-shadow: var(--bslib-sidebar-mobile-box-shadow);--_column-main: minmax(0, 1fr);--_toggle-collective-height: calc(calc(var(--_icon-button-size) + 0.5em) * var(--_js-toggle-count-max-side, 1));--_resize-handle-width: var(--bslib-sidebar-resize-handle-width, 12px);--_resize-indicator-color: var(--_sidebar-fg, var(--bs-emphasis-color, black));--_resize-indicator-color-active: var(--bslib-sidebar-resize-indicator-color-active, var(--bs-primary, #0d6efd));display:grid !important;grid-template-columns:Min(calc(100% - var(--_padding-icon)), var(--_sidebar-width)) var(--_column-main);position:relative;transition:grid-template-columns ease-in-out var(--_transition-duration),background-color linear var(--_transition-duration);border:var(--_border);border-radius:var(--_border-radius)}@container style(--bs-card-color: not " "){.bslib-sidebar-layout{--_main-fg: var(--bslib-sidebar-main-fg, var(--bs-card-color, var(--bs-body-color)))}}.bslib-sidebar-layout.transitioning{--_transition-duration: max(var(--bslib-sidebar-transition-duration, 300ms), 5ms)}@media (prefers-reduced-motion: reduce){.bslib-sidebar-layout{transition:none}}.bslib-sidebar-layout,.html-fill-container>.bslib-sidebar-layout.html-fill-item{min-height:var(--_toggle-collective-height)}.bslib-sidebar-layout[data-bslib-sidebar-border="false"]{border:none}.bslib-sidebar-layout[data-bslib-sidebar-border-radius="false"]{border-radius:initial}.bslib-sidebar-layout>.main,.bslib-sidebar-layout>.sidebar{grid-row:1 / 2;border-radius:inherit;overflow:auto}.bslib-sidebar-layout>.main{grid-column:2 / 3;border-top-left-radius:0;border-bottom-left-radius:0;padding:var(--_padding);transition:padding var(--_transition-easing-x) var(--_transition-duration);color:var(--_main-fg);background-color:var(--_main-bg)}.bslib-sidebar-layout>.sidebar{grid-column:1 / 2;width:100%;border-right:var(--_vert-border);border-top-right-radius:0;border-bottom-right-radius:0;color:var(--_sidebar-fg);background-color:var(--_sidebar-bg);position:relative}.bslib-sidebar-layout>.sidebar>.sidebar-content{display:flex;flex-direction:column;gap:var(--bslib-spacer, 1rem);padding:var(--_padding);padding-top:var(--_padding-icon)}.bslib-sidebar-layout>.sidebar>.sidebar-content>:last-child:not(.sidebar-title){margin-bottom:0}.bslib-sidebar-layout>.sidebar>.sidebar-content>.accordion{margin-left:calc(-1 * var(--_padding));margin-right:calc(-1 * var(--_padding))}.bslib-sidebar-layout>.sidebar>.sidebar-content>.accordion:last-child{margin-bottom:calc(-1 * var(--_padding))}.bslib-sidebar-layout>.sidebar>.sidebar-content>.accordion:not(:last-child){margin-bottom:1rem}.bslib-sidebar-layout>.sidebar>.sidebar-content>.accordion .accordion-body{display:flex;flex-direction:column}.bslib-sidebar-layout>.sidebar>.sidebar-content>.accordion:not(:first-child) .accordion-item:first-child{border-top:var(--bs-accordion-border-width) solid var(--bs-accordion-border-color)}.bslib-sidebar-layout>.sidebar>.sidebar-content>.accordion:not(:last-child) .accordion-item:last-child{border-bottom:var(--bs-accordion-border-width) solid var(--bs-accordion-border-color)}.bslib-sidebar-layout>.sidebar>.sidebar-content.has-accordion>.sidebar-title{border-bottom:none;padding-bottom:0}.bslib-sidebar-layout>.sidebar .shiny-input-container{width:100%}.bslib-sidebar-layout>.collapse-toggle{grid-row:1 / 2;grid-column:1 / 2;z-index:1000;display:inline-flex;align-items:center;position:absolute;right:calc(var(--_icon-size));top:calc(var(--_icon-size, 1rem) / 2);border:none;border-radius:var(--_toggle-border-radius);height:var(--_icon-button-size, 2rem);width:var(--_icon-button-size, 2rem);display:flex;align-items:center;justify-content:center;padding:0;color:var(--_sidebar-fg);background-color:unset;transition:color var(--_transition-easing-x) var(--_transition-duration),top var(--_transition-easing-x) var(--_transition-duration),right var(--_transition-easing-x) var(--_transition-duration),left var(--_transition-easing-x) var(--_transition-duration)}.bslib-sidebar-layout>.collapse-toggle:hover{background-color:var(--_toggle-bg)}.bslib-sidebar-layout>.collapse-toggle>.collapse-icon{opacity:0.8;width:var(--_icon-size);height:var(--_icon-size);transform:rotateY(var(--_toggle-transform));transition:transform var(--_toggle-transition-easing) var(--_transition-duration)}.bslib-sidebar-layout>.collapse-toggle:hover>.collapse-icon{opacity:1}.bslib-sidebar-layout .sidebar-title{font-size:1.25rem;line-height:1.25;margin-top:0;margin-bottom:1rem;padding-bottom:1rem;border-bottom:var(--_border)}.bslib-sidebar-layout.sidebar-right{grid-template-columns:var(--_column-main) Min(calc(100% - var(--_padding-icon)), var(--_sidebar-width))}.bslib-sidebar-layout.sidebar-right>.main{grid-column:1 / 2;border-top-right-radius:0;border-bottom-right-radius:0;border-top-left-radius:inherit;border-bottom-left-radius:inherit}.bslib-sidebar-layout.sidebar-right>.sidebar{grid-column:2 / 3;border-right:none;border-left:var(--_vert-border);border-top-left-radius:0;border-bottom-left-radius:0}.bslib-sidebar-layout.sidebar-right>.collapse-toggle{grid-column:2 / 3;left:var(--_icon-size);right:unset;border:var(--bslib-collapse-toggle-border)}.bslib-sidebar-layout.sidebar-right>.collapse-toggle>.collapse-icon{transform:rotateY(var(--_toggle-right-transform))}.bslib-sidebar-layout.transitioning>.sidebar>.sidebar-content{display:none}.bslib-sidebar-layout.sidebar-collapsed{--_toggle-transform: 180deg;--_toggle-right-transform: 0deg;--_vert-border: none;grid-template-columns:0 minmax(0, 1fr)}.bslib-sidebar-layout.sidebar-collapsed.sidebar-right{grid-template-columns:minmax(0, 1fr) 0}.bslib-sidebar-layout.sidebar-collapsed:not(.transitioning)>.sidebar>*{display:none}.bslib-sidebar-layout.sidebar-collapsed>.main{border-radius:inherit;padding-left:var(--_padding-icon);padding-right:var(--_padding-icon)}.bslib-sidebar-layout.sidebar-collapsed>.collapse-toggle{color:var(--_main-fg);top:var(--_toggle-position-y);right:var(--_toggle-position-x)}.bslib-sidebar-layout.sidebar-collapsed.sidebar-right>.collapse-toggle{left:var(--_toggle-position-x);right:unset}.bslib-sidebar-layout .bslib-sidebar-resize-handle{position:absolute;top:0;bottom:0;width:var(--_resize-handle-width);left:calc(calc(-1 * var(--_resize-handle-width)) - 2px);grid-column:2;cursor:ew-resize;user-select:none;z-index:calc(1000 + 1)}.bslib-sidebar-layout .bslib-sidebar-resize-handle::before{content:"";position:absolute;top:0;bottom:0;left:0;right:calc(-1 * var(--_resize-handle-width) - 2px);z-index:calc(1000 + 1)}.bslib-sidebar-layout .bslib-sidebar-resize-handle .resize-indicator{position:absolute;top:50%;right:2px;width:2px;height:30px;transform:translate(-50%, -50%);background-color:var(--_resize-indicator-color);opacity:0.1;border-radius:1px;transition:all 0.15s ease}.bslib-sidebar-layout .bslib-sidebar-resize-handle:hover .resize-indicator,.bslib-sidebar-layout .bslib-sidebar-resize-handle:focus .resize-indicator,.bslib-sidebar-layout .bslib-sidebar-resize-handle:active .resize-indicator,.bslib-sidebar-layout .bslib-sidebar-resize-handle:focus .resize-indicator{opacity:1}.bslib-sidebar-layout .bslib-sidebar-resize-handle:hover .resize-indicator,.bslib-sidebar-layout .bslib-sidebar-resize-handle:focus .resize-indicator{width:3px;height:40px}.bslib-sidebar-layout .bslib-sidebar-resize-handle:active .resize-indicator{background-color:var(--_resize-indicator-color-active);width:4px;height:50px}.bslib-sidebar-layout .bslib-sidebar-resize-handle:focus{outline:none}.bslib-sidebar-layout .bslib-sidebar-resize-handle:focus .resize-indicator{outline:2px solid var(--bs-focus-ring-color, rgba(13,110,253,0.25));outline-offset:2px}.bslib-sidebar-layout.sidebar-right>.bslib-sidebar-resize-handle{left:2px}.bslib-sidebar-layout.transitioning>.bslib-sidebar-resize-handle,.bslib-sidebar-layout.sidebar-collapsed>.bslib-sidebar-resize-handle{display:none}.bslib-sidebar-layout.sidebar-resizing{user-select:none}.bslib-sidebar-layout.sidebar-resizing>.bslib-sidebar-resize-handle .resize-indicator{background-color:var(--_resize-indicator-color-active);width:4px;height:50px}.bslib-sidebar-layout{--bslib-sidebar-js-window-size: desktop}@media (max-width: 575.98px){.bslib-sidebar-layout{--bslib-sidebar-js-window-size: mobile}.bslib-sidebar-layout .bslib-sidebar-resize-handle{display:none !important}}@media (min-width: 576px){.bslib-sidebar-layout[data-collapsible-desktop="false"]{--_padding-icon: var(--_padding)}.bslib-sidebar-layout[data-collapsible-desktop="false"]>.collapse-toggle{display:none}.bslib-sidebar-layout[data-collapsible-desktop="false"]>.sidebar[hidden]{display:block !important}.bslib-sidebar-layout[data-collapsible-desktop="false"]>.sidebar[hidden]>.sidebar-content{display:flex !important}}@media (max-width: 575.98px){.bslib-sidebar-layout>.sidebar,.bslib-sidebar-layout.sidebar-right>.sidebar{border:none}.bslib-sidebar-layout>.main,.bslib-sidebar-layout.sidebar-right>.main{grid-column:1 / 3}.bslib-sidebar-layout[data-collapsible-mobile="true"]{grid-template-rows:calc(var(--_icon-button-size) + var(--_padding)) 1fr;grid-template-columns:100% 0}.bslib-sidebar-layout[data-collapsible-mobile="true"]>.collapse-toggle{grid-row:1 / 2}.bslib-sidebar-layout[data-collapsible-mobile="true"]>.main{grid-row:2 / 3}.bslib-sidebar-layout[data-collapsible-mobile="true"]>.sidebar{grid-row:1 / 3}.bslib-sidebar-layout[data-collapsible-mobile="true"]:not(.sidebar-collapsed)>.sidebar,.bslib-sidebar-layout[data-collapsible-mobile="true"].transitioning>.sidebar{z-index:1045}.bslib-sidebar-layout[data-collapsible-mobile="true"]:not(.sidebar-collapsed)>.collapse-toggle,.bslib-sidebar-layout[data-collapsible-mobile="true"].transitioning>.collapse-toggle{z-index:1045}.bslib-sidebar-layout[data-collapsible-mobile="true"]>.collapse-toggle{top:unset;position:relative;align-self:center}.bslib-sidebar-layout[data-collapsible-mobile="true"]:not(.sidebar-right)>.collapse-toggle{left:var(--_icon-size);right:unset;justify-self:left}.bslib-sidebar-layout[data-collapsible-mobile="true"].sidebar-right>.collapse-toggle{right:var(--_icon-size);left:unset;justify-self:right}.bslib-sidebar-layout[data-collapsible-mobile="true"]>.sidebar{opacity:var(--_sidebar-mobile-opacity, 1);max-width:var(--_sidebar-mobile-max-width, 100%);box-shadow:var(--_sidebar-mobile-box-shadow)}.bslib-sidebar-layout[data-collapsible-mobile="true"]>.sidebar{margin:0;padding-top:var(--_padding-icon)}.bslib-sidebar-layout[data-collapsible-mobile="true"]>.sidebar>.sidebar-content{padding-top:0;height:100%;overflow-y:auto}.bslib-sidebar-layout[data-collapsible-mobile="true"]:not(.sidebar-right)>.sidebar{margin-right:auto}.bslib-sidebar-layout[data-collapsible-mobile="true"].sidebar-right>.sidebar{margin-left:auto}.bslib-sidebar-layout[data-collapsible-mobile="true"].sidebar-right{grid-template-columns:0 100%}.bslib-sidebar-layout[data-collapsible-mobile="true"].sidebar-collapsed{grid-template-columns:0 100%}.bslib-sidebar-layout[data-collapsible-mobile="true"].sidebar-collapsed.sidebar-right{grid-template-columns:100% 0}.bslib-sidebar-layout[data-collapsible-mobile="true"]>.main{padding-top:1px;padding-left:var(--_padding);padding-right:var(--_padding)}.bslib-sidebar-layout[data-collapsible-mobile="true"]>.main{opacity:var(--_main-mobile-expanded-opacity);transition:opacity var(--_transition-easing-x) var(--_transition-duration)}.bslib-sidebar-layout[data-collapsible-mobile="true"].sidebar-collapsed>.main{opacity:1}.bslib-sidebar-layout[data-collapsible-mobile="true"]>.main{background-color:none}.bslib-sidebar-layout[data-collapsible-mobile="true"],.bslib-sidebar-layout[data-collapsible-mobile="true"].sidebar-collapsed{background-color:var(--_main-bg)}}@media (max-width: 575.98px){.bslib-sidebar-layout[data-collapsible-mobile="false"]{display:block !important;--_padding-icon: var(--_padding);--_vert-border: var(--_border)}.bslib-sidebar-layout[data-collapsible-mobile="false"]>.sidebar[hidden]{display:block !important}.bslib-sidebar-layout[data-collapsible-mobile="false"]>.sidebar[hidden]>.sidebar-content{display:flex !important}.bslib-sidebar-layout[data-collapsible-mobile="false"]>.sidebar{max-height:var(--_mobile-max-height);overflow-y:auto}.bslib-sidebar-layout[data-collapsible-mobile="false"][data-open-mobile="always"]>.sidebar{border-top:var(--_vert-border)}.bslib-sidebar-layout[data-collapsible-mobile="false"][data-open-mobile="always-above"]>.sidebar{border-bottom:var(--_vert-border)}.bslib-sidebar-layout[data-collapsible-mobile="false"]>.collapse-toggle{display:none}}html[data-bslib-sidebar-resizing="true"]{cursor:ew-resize !important;user-select:none !important}.toast{--bslib-toast-shadow: var(--bs-box-shadow);box-shadow:var(--bslib-toast-shadow);position:relative;overflow:hidden}.toast-body:empty{display:none}.text-bg-primary.toast .toast-body .btn-close,.text-bg-secondary.toast .toast-body .btn-close,.text-bg-success.toast .toast-body .btn-close,.text-bg-info.toast .toast-body .btn-close,.text-bg-warning.toast .toast-body .btn-close,.text-bg-danger.toast .toast-body .btn-close,.text-bg-dark.toast .toast-body .btn-close,.text-white.toast .toast-body .btn-close,.text-light.toast .toast-body .btn-close{filter:var(--bs-btn-close-white-filter)}@keyframes bslib-toast-progress{from{transform:scaleX(0)}to{transform:scaleX(1)}}.bslib-toast-progress-bar{position:absolute;top:0;left:0;height:2px;width:100%;pointer-events:none;z-index:1;transform-origin:left;border-radius:inherit;pointer-events:none;background-color:currentColor}.bslib-toolbar{--_divider-height: var(--bslib-toolbar-divider-height, 1lh);--_divider-width: var(--bslib-toolbar-divider-width, 2px);--_divider-gap: var(--bslib-toolbar-divider-gap, 1rem);--_divider-color: var(--bslib-toolbar-divider-color, var(--bs-border-color-translucent, rgba(40, 70, 94, 0.1)));--_toolbar-btn-size: var(--bslib-toolbar-btn-size, 1.75rem);display:flex;flex-wrap:wrap;align-items:center;gap:0}.bslib-toolbar[data-align="left"]{margin-right:auto;justify-content:start}.bslib-toolbar[data-align="right"]{margin-left:auto;justify-content:end}.bslib-toolbar .bslib-toolbar-input-button,.bslib-toolbar .bslib-toolbar-download-button{align-items:center;justify-content:center;line-height:1;height:var(--_toolbar-btn-size);display:flex}.bslib-toolbar .bslib-toolbar-input-button .action-icon,.bslib-toolbar .bslib-toolbar-input-button .action-label,.bslib-toolbar .bslib-toolbar-download-button .action-icon,.bslib-toolbar .bslib-toolbar-download-button .action-label{margin:0}.bslib-toolbar .bslib-toolbar-input-button.btn-default:not(.btn-primary):not(.btn-secondary):not(.btn-success):not(.btn-danger):not(.btn-warning):not(.btn-info):not(.btn-light):not(.btn-dark),.bslib-toolbar .bslib-toolbar-download-button.btn-default:not(.btn-primary):not(.btn-secondary):not(.btn-success):not(.btn-danger):not(.btn-warning):not(.btn-info):not(.btn-light):not(.btn-dark){background-color:transparent !important;background-image:none !important;color:currentColor !important}.bslib-toolbar .bslib-toolbar-input-button.btn-default:not(.btn-primary):not(.btn-secondary):not(.btn-success):not(.btn-danger):not(.btn-warning):not(.btn-info):not(.btn-light):not(.btn-dark):hover,.bslib-toolbar .bslib-toolbar-download-button.btn-default:not(.btn-primary):not(.btn-secondary):not(.btn-success):not(.btn-danger):not(.btn-warning):not(.btn-info):not(.btn-light):not(.btn-dark):hover{background-color:rgba(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.08) !important;color:currentColor !important}.bslib-toolbar .bslib-toolbar-input-button.btn-default:not(.btn-primary):not(.btn-secondary):not(.btn-success):not(.btn-danger):not(.btn-warning):not(.btn-info):not(.btn-light):not(.btn-dark):active,.bslib-toolbar .bslib-toolbar-download-button.btn-default:not(.btn-primary):not(.btn-secondary):not(.btn-success):not(.btn-danger):not(.btn-warning):not(.btn-info):not(.btn-light):not(.btn-dark):active{background-color:rgba(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.16) !important;color:currentColor !important}.bslib-toolbar .bslib-toolbar-input-button.btn-default:not(.btn-primary):not(.btn-secondary):not(.btn-success):not(.btn-danger):not(.btn-warning):not(.btn-info):not(.btn-light):not(.btn-dark):focus-within,.bslib-toolbar .bslib-toolbar-download-button.btn-default:not(.btn-primary):not(.btn-secondary):not(.btn-success):not(.btn-danger):not(.btn-warning):not(.btn-info):not(.btn-light):not(.btn-dark):focus-within{background-color:rgba(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.12) !important;box-shadow:0 0 0 0.25rem rgba(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.25) !important;color:currentColor !important}.bslib-toolbar .bslib-toolbar-download-button{text-decoration:none}.bslib-toolbar .bslib-toolbar-download-button:hover,.bslib-toolbar .bslib-toolbar-download-button:active,.bslib-toolbar .bslib-toolbar-download-button:focus-within{text-decoration:none !important}.bslib-toolbar .bslib-toolbar-download-button.disabled{opacity:0.65;pointer-events:none}.bslib-toolbar .bslib-toolbar-icon.action-icon svg,.bslib-toolbar .bslib-toolbar-icon.action-icon img{margin:0 !important}.bslib-toolbar .bslib-toolbar-input-button[data-type="label"] .action-label,.bslib-toolbar .bslib-toolbar-download-button[data-type="label"] .action-label{padding-left:0}.bslib-toolbar .bslib-toolbar-input-button[data-type="both"] .action-icon,.bslib-toolbar .bslib-toolbar-download-button[data-type="both"] .action-icon{margin-right:0.35rem}.bslib-toolbar .bslib-toolbar-input-button[data-type="icon"],.bslib-toolbar .bslib-toolbar-download-button[data-type="icon"]{aspect-ratio:1;line-height:1 !important}.bslib-toolbar .bslib-toolbar-input-button[data-type="icon"] .action-label,.bslib-toolbar .bslib-toolbar-download-button[data-type="icon"] .action-label{padding-left:0}.bslib-toolbar .bslib-toolbar-input-button[data-type="icon"]>.action-icon,.bslib-toolbar .bslib-toolbar-download-button[data-type="icon"]>.action-icon{display:flex;align-items:center;justify-content:center;line-height:1;margin:0}.bslib-toolbar .bslib-toolbar-divider{align-self:center;height:var(--_divider-height);width:var(--_divider-gap)}.bslib-toolbar .bslib-toolbar-divider::before{content:"";display:block;width:var(--_divider-width);height:100%;background-color:var(--_divider-color);margin:0 auto}.bslib-toolbar .bslib-toolbar-spacer{margin-left:auto}.bslib-toolbar,.bslib-toolbar *{font-size:0.9rem}.bslib-toolbar>*{margin-bottom:0 !important;width:auto;align-self:center}label:has(>.bslib-toolbar){width:100%}label>.bslib-toolbar{width:100%}.bslib-toolbar-input-select{padding-inline:0.25rem;height:var(--_toolbar-btn-size, 1.75rem);display:inline-flex;align-items:center;width:auto !important;border-radius:var(--bs-border-radius-sm, 0.25rem);gap:0.05rem;transition:border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out}.bslib-toolbar-input-select select{appearance:auto;background-image:none;padding:0.1rem 0.5rem 0.1rem 0.1rem;border:none;background-color:transparent;color:currentColor;line-height:1;width:auto;min-width:fit-content;font-family:inherit}.bslib-toolbar-input-select select:focus{outline:none;box-shadow:none}.bslib-toolbar-input-select:hover{background-color:rgba(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.08)}.bslib-toolbar-input-select:active{background-color:rgba(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.16)}.bslib-toolbar-input-select:focus-within{background-color:rgba(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.12);box-shadow:0 0 0 0.25rem rgba(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.25)}.bslib-toolbar-input-select .bslib-toolbar-icon{display:inline-flex;align-items:center;color:var(--bs-secondary-color);margin-left:0.15rem}.bslib-toolbar-input-select .bslib-toolbar-icon:empty{display:none}.bslib-toolbar-input-select label,.bslib-toolbar-input-select label.control-label{font-weight:600;margin-bottom:0;display:inline-flex;align-items:center}.bslib-toolbar-input-select .bslib-toolbar-label{margin-left:0.15rem}.bslib-value-box{container-name:bslib-value-box;container-type:inline-size}.bslib-value-box.default{--bslib-value-box-bg-default: var(--bs-card-bg, #fff);--bslib-value-box-border-color-default: var(--bs-card-border-color, var(--bs-border-color-translucent));color:var(--bslib-value-box-color, var(--bs-body-color));background-color:var(--bslib-value-box-bg, var(--bslib-value-box-bg-default));border-color:var(--bslib-value-box-border-color, var(--bslib-value-box-border-color-default))}.bslib-value-box .value-box-grid{display:grid;grid-template-areas:"left right";align-items:center;overflow:hidden}.bslib-value-box .value-box-showcase{height:100%;max-height:var(---bslib-value-box-showcase-max-h, 100%)}.bslib-value-box .value-box-showcase,.bslib-value-box .value-box-showcase>.html-fill-item{width:100%}.bslib-value-box[data-full-screen="true"] .value-box-showcase{max-height:var(---bslib-value-box-showcase-max-h-fs, 100%)}@media screen and (min-width: 575.98px){@container bslib-value-box (max-width: 300px){.bslib-value-box:not(.showcase-bottom) .value-box-grid{grid-template-columns:1fr !important;grid-template-rows:auto auto;grid-template-areas:"top" "bottom"}.bslib-value-box:not(.showcase-bottom) .value-box-grid .value-box-showcase{grid-area:top !important}.bslib-value-box:not(.showcase-bottom) .value-box-grid .value-box-area{grid-area:bottom !important;justify-content:end}}}.bslib-value-box .value-box-area{justify-content:center;padding:1.5rem 1rem;font-size:.9rem;font-weight:500}.bslib-value-box .value-box-area *{margin-bottom:0;margin-top:0}.bslib-value-box .value-box-title{font-size:1rem;margin-top:0;margin-bottom:.5rem;font-weight:400;line-height:1.2}.bslib-value-box .value-box-title:empty::after{content:'\00a0 '}.bslib-value-box .value-box-value{font-size:calc(1.325rem + .9vw);margin-top:0;margin-bottom:.5rem;font-weight:400;line-height:1.2}@media (min-width: 1200px){.bslib-value-box .value-box-value{font-size:2rem}}.bslib-value-box .value-box-value:empty::after{content:'\00a0 '}.bslib-value-box .value-box-showcase{align-items:center;justify-content:center;margin-top:auto;margin-bottom:auto;padding:1rem}.bslib-value-box .value-box-showcase .bi,.bslib-value-box .value-box-showcase .fa,.bslib-value-box .value-box-showcase .fab,.bslib-value-box .value-box-showcase .fas,.bslib-value-box .value-box-showcase .far{opacity:.85;min-width:50px;max-width:125%}.bslib-value-box .value-box-showcase .bi,.bslib-value-box .value-box-showcase .fa,.bslib-value-box .value-box-showcase .fab,.bslib-value-box .value-box-showcase .fas,.bslib-value-box .value-box-showcase .far{font-size:4rem}.bslib-value-box.showcase-top-right .value-box-grid{grid-template-columns:1fr var(---bslib-value-box-showcase-w, 50%)}.bslib-value-box.showcase-top-right .value-box-grid .value-box-showcase{grid-area:right;margin-left:auto;align-self:start;align-items:end;padding-left:0;padding-bottom:0}.bslib-value-box.showcase-top-right .value-box-grid .value-box-area{grid-area:left;align-self:end}.bslib-value-box.showcase-top-right[data-full-screen="true"] .value-box-grid{grid-template-columns:auto var(---bslib-value-box-showcase-w-fs, 1fr)}.bslib-value-box.showcase-top-right[data-full-screen="true"] .value-box-grid>div{align-self:center}.bslib-value-box.showcase-top-right:not([data-full-screen="true"]) .value-box-showcase{margin-top:0}@container bslib-value-box (max-width: 300px){.bslib-value-box.showcase-top-right:not([data-full-screen="true"]) .value-box-grid .value-box-showcase{padding-left:1rem}}.bslib-value-box.showcase-left-center .value-box-grid{grid-template-columns:var(---bslib-value-box-showcase-w, 30%) auto}.bslib-value-box.showcase-left-center[data-full-screen="true"] .value-box-grid{grid-template-columns:var(---bslib-value-box-showcase-w-fs, 1fr) auto}.bslib-value-box.showcase-left-center:not([data-fill-screen="true"]) .value-box-grid .value-box-showcase{grid-area:left}.bslib-value-box.showcase-left-center:not([data-fill-screen="true"]) .value-box-grid .value-box-area{grid-area:right}.bslib-value-box.showcase-bottom .value-box-grid{grid-template-columns:1fr;grid-template-rows:1fr var(---bslib-value-box-showcase-h, auto);grid-template-areas:"top" "bottom";overflow:hidden}.bslib-value-box.showcase-bottom .value-box-grid .value-box-showcase{grid-area:bottom;padding:0;margin:0}.bslib-value-box.showcase-bottom .value-box-grid .value-box-area{grid-area:top}.bslib-value-box.showcase-bottom[data-full-screen="true"] .value-box-grid{grid-template-rows:1fr var(---bslib-value-box-showcase-h-fs, 2fr)}.bslib-value-box.showcase-bottom[data-full-screen="true"] .value-box-grid .value-box-showcase{padding:1rem} +.accordion .accordion-header{font-size:calc(1.325rem + .9vw);margin-top:0;margin-bottom:.5rem;font-weight:400;line-height:1.2;color:var(--bs-heading-color);margin-bottom:0}@media (min-width: 1200px){.accordion .accordion-header{font-size:2rem}}.accordion .accordion-icon:not(:empty){margin-right:0.75rem;display:flex}.accordion .accordion-button:not(.collapsed){box-shadow:none}.accordion .accordion-button:not(.collapsed):focus{box-shadow:var(--bs-accordion-btn-focus-box-shadow)}.bslib-card{overflow:auto}.bslib-card .card-body+.card-body{padding-top:0}.bslib-card .card-body{overflow:auto}.bslib-card .card-body p{margin-top:0}.bslib-card .card-body p:last-child{margin-bottom:0}.bslib-card .card-body{max-height:var(--bslib-card-body-max-height, none)}.bslib-card[data-full-screen="true"]>.card-body{max-height:var(--bslib-card-body-max-height-full-screen, none)}.bslib-card .card-header{display:flex;flex-direction:row;align-items:center;align-self:stretch;min-height:2.5rem;padding-block:calc(var(--bs-card-cap-padding-y) / 2);gap:0.25rem}.bslib-card .card-header>.nav{flex:1;min-width:0}.bslib-card .card-header>.bslib-nav-spacer{margin-left:auto}.bslib-card .card-header .form-group{margin-bottom:0}.bslib-card .card-header .selectize-control{margin-bottom:0}.bslib-card .card-header .selectize-control .item{margin-right:1.15rem}.bslib-card .card-footer{margin-top:auto}.bslib-card .bslib-navs-card-title{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center}.bslib-card .bslib-navs-card-title .nav{margin-left:auto}.bslib-card .bslib-sidebar-layout:not([data-bslib-sidebar-border="true"]){border:none}.bslib-card .bslib-sidebar-layout:not([data-bslib-sidebar-border-radius="true"]){border-top-left-radius:0;border-top-right-radius:0}.bslib-card[data-full-screen="true"]{position:fixed;inset:3.5rem 1rem 1rem;height:auto !important;max-height:none !important;width:auto !important;z-index:1070}.bslib-full-screen-enter{position:absolute;bottom:var(--bslib-full-screen-enter-bottom, 0.2rem);right:var(--bslib-full-screen-enter-right, 0);top:var(--bslib-full-screen-enter-top);left:var(--bslib-full-screen-enter-left);color:var(--bslib-color-fg, var(--bs-card-color));background-color:var(--bslib-color-bg, var(--bs-card-bg, var(--bs-body-bg)));border:var(--bs-card-border-width) solid var(--bslib-color-fg, var(--bs-card-border-color));box-shadow:0 2px 4px rgba(0,0,0,0.15);margin:0.2rem 0.4rem;padding:0.55rem !important;font-size:.8rem;cursor:pointer;opacity:0;z-index:1070}.card:hover>*>.bslib-full-screen-enter,.card:focus-within>*>.bslib-full-screen-enter{opacity:0.6}.card:hover>*>.bslib-full-screen-enter:hover,.card:hover>*>.bslib-full-screen-enter:focus,.card:focus-within>*>.bslib-full-screen-enter:hover,.card:focus-within>*>.bslib-full-screen-enter:focus{opacity:1}.card[data-full-screen="false"]:hover>*>.bslib-full-screen-enter{display:block}.bslib-has-full-screen .bslib-full-screen-enter{display:none !important}.bslib-full-screen-exit{position:relative;top:1.35rem;font-size:0.9rem;cursor:pointer;text-decoration:none;display:flex;float:right;margin-right:2.15rem;align-items:center;color:rgba(var(--bs-body-bg-rgb), 0.8)}.bslib-full-screen-exit:hover{color:rgba(var(--bs-body-bg-rgb), 1)}.bslib-full-screen-exit svg{margin-left:0.5rem;font-size:1.5rem}#bslib-full-screen-overlay{position:fixed;inset:0;background-color:rgba(var(--bs-body-color-rgb), 0.6);backdrop-filter:blur(2px);-webkit-backdrop-filter:blur(2px);z-index:1069;animation:bslib-full-screen-overlay-enter 400ms cubic-bezier(0.6, 0.02, 0.65, 1) forwards}@keyframes bslib-full-screen-overlay-enter{0%{opacity:0}100%{opacity:1}}@media (max-width: 575.98px){.bslib-card[data-full-screen="true"]{inset:2.5rem 0.5rem 0.5rem}.bslib-full-screen-exit{top:0.75rem;margin-right:1.25rem}}.bslib-grid{--_item-column-span: 1;display:grid !important;gap:var(--bslib-spacer, 1rem);height:var(--bslib-grid-height)}.bslib-grid>*{grid-column:auto/span var(--_item-column-span, 1)}.bslib-grid.grid{grid-template-columns:repeat(var(--bs-columns, 12), minmax(0, 1fr));grid-template-rows:unset;grid-auto-rows:var(--bslib-grid--row-heights);--bslib-grid--row-heights--xs: unset;--bslib-grid--row-heights--sm: unset;--bslib-grid--row-heights--md: unset;--bslib-grid--row-heights--lg: unset;--bslib-grid--row-heights--xl: unset;--bslib-grid--row-heights--xxl: unset}.bslib-grid.grid.bslib-grid--row-heights--xs{--bslib-grid--row-heights: var(--bslib-grid--row-heights--xs)}@media (min-width: 576px){.bslib-grid.grid.bslib-grid--row-heights--sm{--bslib-grid--row-heights: var(--bslib-grid--row-heights--sm)}}@media (min-width: 768px){.bslib-grid.grid.bslib-grid--row-heights--md{--bslib-grid--row-heights: var(--bslib-grid--row-heights--md)}}@media (min-width: 992px){.bslib-grid.grid.bslib-grid--row-heights--lg{--bslib-grid--row-heights: var(--bslib-grid--row-heights--lg)}}@media (min-width: 1200px){.bslib-grid.grid.bslib-grid--row-heights--xl{--bslib-grid--row-heights: var(--bslib-grid--row-heights--xl)}}@media (min-width: 1400px){.bslib-grid.grid.bslib-grid--row-heights--xxl{--bslib-grid--row-heights: var(--bslib-grid--row-heights--xxl)}}.bslib-grid>*>.shiny-input-container{width:100%}bslib-layout-columns.bslib-grid{--_item-column-span: 6}bslib-layout-columns[hidden-until-init]>*{display:none}@media (max-width: 767.98px){bslib-layout-columns:where(.bslib-grid)>*{grid-column:1 / -1}}@media (max-width: 575.98px){.bslib-grid{grid-template-columns:1fr !important;height:var(--bslib-grid-height-mobile)}.bslib-grid.grid{height:unset !important}}.bslib-input-submit-textarea{margin:0 auto}.bslib-submit-textarea-container{display:flex;flex-direction:column;gap:0.5rem;padding:0.5rem;border:var(--bs-border-width, 1px) solid var(--bs-gray-500, #ced4da);border-radius:var(--bs-border-radius-sm, 4px);background-color:var(--bs-body-bg, white);transition:border-color 0.2s, box-shadow 0.2s}.bslib-submit-textarea-container:focus-within{border-color:var(--bs-primary, #007bff);box-shadow:0 0 0 var(--bs-focus-ring-width, 0.25rem) var(--bs-focus-ring-color, rgba(13,110,253,0.25))}.bslib-submit-textarea-container>textarea{border:none;resize:none;min-height:1rem;max-height:10rem;background-color:transparent;padding:0;color:var(--bs-body-color, #212529)}.bslib-submit-textarea-container>textarea:focus{outline:none;box-shadow:none}.bslib-submit-textarea-container>footer{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:0.5rem}.bslib-submit-textarea-container .bslib-submit-textarea-btn{margin-left:auto}.bslib-toolbar{display:flex;align-items:center;gap:0.25rem}.bslib-submit-key{border-radius:var(--bs-border-radius-sm, 4px);padding:0.25em 0.5em;font-weight:300;font-size:0.7em;vertical-align:0.15em}:not(.disabled) .bslib-submit-key{background-color:rgba(var(--bs-body-color-rgb, 0, 0, 0), 0.2)}@media (min-width: 576px){.nav:not(.nav-hidden){display:flex !important;display:-webkit-flex !important}.nav:not(.nav-hidden):not(.nav-stacked):not(.flex-column){float:none !important}.nav:not(.nav-hidden):not(.nav-stacked):not(.flex-column)>.bslib-nav-spacer{margin-left:auto !important}.nav:not(.nav-hidden):not(.nav-stacked):not(.flex-column)>.form-inline{margin-top:auto;margin-bottom:auto}.nav:not(.nav-hidden).nav-stacked{flex-direction:column;-webkit-flex-direction:column;height:100%}.nav:not(.nav-hidden).nav-stacked>.bslib-nav-spacer{margin-top:auto !important}}.bslib-page-fill{width:100%;height:100%;margin:0;padding:var(--bslib-spacer, 1rem);gap:var(--bslib-spacer, 1rem)}@media (max-width: 575.98px){.bslib-flow-mobile>.html-fill-item{flex:0 0 auto}.bslib-flow-mobile.bslib-page-sidebar>.html-fill-item,.bslib-flow-mobile.bslib-page-navbar.has-page-sidebar>.html-fill-item{flex:1 1 auto}.bslib-flow-mobile.bslib-page-sidebar>.bslib-sidebar-layout>.main>.html-fill-item,.bslib-flow-mobile.bslib-page-navbar.has-page-sidebar>.html-fill-container>.bslib-sidebar-layout>.main>.html-fill-item{flex:0 0 auto}}.navbar+.container-fluid:has(>.tab-content>.tab-pane.active.html-fill-container){padding-left:0;padding-right:0}.navbar+.container-fluid>.tab-content>.tab-pane.active.html-fill-container{padding:var(--bslib-spacer, 1rem);gap:var(--bslib-spacer, 1rem)}.navbar+.container-fluid>.tab-content>.tab-pane.active.html-fill-container:has(>.bslib-sidebar-layout:only-child){padding:0}.navbar+.container-fluid>.tab-content>.tab-pane.active.html-fill-container>.bslib-sidebar-layout:only-child:not([data-bslib-sidebar-border="true"]){border-left:none;border-right:none;border-bottom:none}.navbar+.container-fluid>.tab-content>.tab-pane.active.html-fill-container>.bslib-sidebar-layout:only-child:not([data-bslib-sidebar-border-radius="true"]){border-radius:0}.navbar+div>.bslib-sidebar-layout{border-top:var(--bslib-sidebar-border)}:root{--bslib-page-sidebar-title-bg: #202020;--bslib-page-sidebar-title-color: #fff}.bslib-page-sidebar>.navbar{--bs-navbar-brand-color: var(--bslib-page-sidebar-title-color);border-bottom:var(--bs-border-width) solid var(--bs-border-color-translucent);background-color:var(--bslib-page-sidebar-title-bg);color:var(--bslib-page-sidebar-title-color)}.bslib-page-sidebar .bslib-page-title{margin-bottom:0;line-height:var(--bs-body-line-height)}@media (max-width: 991.98px){.bslib-page-sidebar>.bslib-sidebar-layout.sidebar-collapsed:not(.sidebar-right)>.main,.bslib-page-navbar>div>.bslib-sidebar-layout.sidebar-collapsed:not(.sidebar-right)>.main{padding-right:var(--_padding)}.bslib-page-sidebar>.bslib-sidebar-layout.sidebar-collapsed.sidebar-right>.main,.bslib-page-navbar>div>.bslib-sidebar-layout.sidebar-collapsed.sidebar-right>.main{padding-left:var(--_padding)}}@media (min-width: 576px){.bslib-sidebar-layout .bslib-page-main.html-fill-container{min-height:var(--bslib-page-main-min-height, 576px)}.bslib-sidebar-layout:not(.sidebar-collapsed) .bslib-page-main.html-fill-container,.bslib-sidebar-layout.transitioning .bslib-page-main.html-fill-container{min-width:var(--bslib-page-main-min-width, 576px)}}.bslib-sidebar-layout{container-type:style;--_transition-duration: 0;--_transition-easing-x: var(--bslib-sidebar-transition-easing-x, cubic-bezier(0.8, 0.78, 0.22, 1.07));--_border: var(--bslib-sidebar-border, var(--bs-card-border-width, var(--bs-border-width)) solid var(--bs-card-border-color, var(--bs-border-color-translucent)));--_border-radius: var(--bslib-sidebar-border-radius, var(--bs-border-radius));--_vert-border: var(--bslib-sidebar-vert-border, var(--_border));--_sidebar-width: var(--bslib-sidebar-width, 250px);--_sidebar-bg: var(--bslib-sidebar-bg, RGBA(var(--bs-body-bg-rgb), 0.05));--_sidebar-fg: var(--bslib-sidebar-fg, var(--_main-fg));--_main-fg: var(--bslib-sidebar-main-fg, var(--bs-body-color));--_main-bg: var(--bslib-sidebar-main-bg, transparent);--_toggle-bg: var(--bslib-sidebar-toggle-bg, rgba(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.1));--_padding: var(--bslib-sidebar-padding, var(--bslib-spacer, 1.5rem));--_icon-size: var(--bslib-sidebar-icon-size, 1rem);--_icon-button-size: var(--bslib-sidebar-icon-button-size, calc(var(--_icon-size, 1rem) * 2));--_padding-icon: calc(var(--_icon-button-size, 2rem) * 1.5);--_toggle-border-radius: var(--bslib-collapse-toggle-border-radius, var(--bs-border-radius, 3px));--_toggle-transform: var(--bslib-collapse-toggle-transform, 0deg);--_toggle-transition-easing: var(--bslib-sidebar-toggle-transition-easing, cubic-bezier(1, 0, 0, 1));--_toggle-right-transform: var(--bslib-collapse-toggle-right-transform, 180deg);--_toggle-position-y: calc(var(--_js-toggle-count-this-side, 0) * calc(var(--_icon-size) + var(--_padding)) + var(--_icon-size, 1rem) / 2);--_toggle-position-x: calc(-2.5 * var(--_icon-size) - var(--bs-card-border-width, 1px));--_mobile-max-height: var(--bslib-sidebar-mobile-max-height, var(--bslib-sidebar-max-height-mobile));--_sidebar-mobile-opacity: var(--bslib-sidebar-mobile-opacity);--_main-mobile-expanded-opacity: var(--bslib-sidebar-main-mobile-expanded-opacity, 0);--_sidebar-mobile-max-width: var(--bslib-sidebar-mobile-max-width);--_sidebar-mobile-box-shadow: var(--bslib-sidebar-mobile-box-shadow);--_column-main: minmax(0, 1fr);--_toggle-collective-height: calc(calc(var(--_icon-button-size) + 0.5em) * var(--_js-toggle-count-max-side, 1));--_resize-handle-width: var(--bslib-sidebar-resize-handle-width, 8px);--_resize-indicator-color: var(--_sidebar-fg, var(--bs-emphasis-color, black));--_resize-indicator-color-active: var(--bslib-sidebar-resize-indicator-color-active, var(--bs-primary, #0d6efd));display:grid !important;grid-template-columns:Min(calc(100% - var(--_padding-icon)), var(--_sidebar-width)) var(--_column-main);position:relative;transition:grid-template-columns ease-in-out var(--_transition-duration),background-color linear var(--_transition-duration);border:var(--_border);border-radius:var(--_border-radius)}@container style(--bs-card-color: not " "){.bslib-sidebar-layout{--_main-fg: var(--bslib-sidebar-main-fg, var(--bs-card-color, var(--bs-body-color)))}}@media (any-pointer: coarse){.bslib-sidebar-layout{--_resize-handle-width: var(--bslib-sidebar-resize-handle-width, 26px)}}.bslib-sidebar-layout.transitioning{--_transition-duration: max(var(--bslib-sidebar-transition-duration, 300ms), 5ms)}@media (prefers-reduced-motion: reduce){.bslib-sidebar-layout{transition:none}}.bslib-sidebar-layout,.html-fill-container>.bslib-sidebar-layout.html-fill-item{min-height:var(--_toggle-collective-height)}.bslib-sidebar-layout[data-bslib-sidebar-border="false"]{border:none}.bslib-sidebar-layout[data-bslib-sidebar-border-radius="false"]{border-radius:initial}.bslib-sidebar-layout>.main,.bslib-sidebar-layout>.sidebar{grid-row:1 / 2;border-radius:inherit;overflow:auto}.bslib-sidebar-layout>.main{grid-column:2 / 3;border-top-left-radius:0;border-bottom-left-radius:0;padding:var(--_padding);transition:padding var(--_transition-easing-x) var(--_transition-duration);color:var(--_main-fg);background-color:var(--_main-bg)}.bslib-sidebar-layout>.sidebar{grid-column:1 / 2;width:100%;border-right:var(--_vert-border);border-top-right-radius:0;border-bottom-right-radius:0;color:var(--_sidebar-fg);background-color:var(--_sidebar-bg);position:relative}.bslib-sidebar-layout>.sidebar>.sidebar-content{display:flex;flex-direction:column;gap:var(--bslib-spacer, 1rem);padding:var(--_padding);padding-top:var(--_padding-icon)}.bslib-sidebar-layout>.sidebar>.sidebar-content>:last-child:not(.sidebar-title){margin-bottom:0}.bslib-sidebar-layout>.sidebar>.sidebar-content>.accordion{margin-left:calc(-1 * var(--_padding));margin-right:calc(-1 * var(--_padding))}.bslib-sidebar-layout>.sidebar>.sidebar-content>.accordion:last-child{margin-bottom:calc(-1 * var(--_padding))}.bslib-sidebar-layout>.sidebar>.sidebar-content>.accordion:not(:last-child){margin-bottom:1rem}.bslib-sidebar-layout>.sidebar>.sidebar-content>.accordion .accordion-body{display:flex;flex-direction:column}.bslib-sidebar-layout>.sidebar>.sidebar-content>.accordion:not(:first-child) .accordion-item:first-child{border-top:var(--bs-accordion-border-width) solid var(--bs-accordion-border-color)}.bslib-sidebar-layout>.sidebar>.sidebar-content>.accordion:not(:last-child) .accordion-item:last-child{border-bottom:var(--bs-accordion-border-width) solid var(--bs-accordion-border-color)}.bslib-sidebar-layout>.sidebar>.sidebar-content.has-accordion>.sidebar-title{border-bottom:none;padding-bottom:0}.bslib-sidebar-layout>.sidebar .shiny-input-container{width:100%}.bslib-sidebar-layout>.collapse-toggle{grid-row:1 / 2;grid-column:1 / 2;z-index:1000;display:inline-flex;align-items:center;position:absolute;right:calc(var(--_icon-size));top:calc(var(--_icon-size, 1rem) / 2);border:none;border-radius:var(--_toggle-border-radius);height:var(--_icon-button-size, 2rem);width:var(--_icon-button-size, 2rem);display:flex;align-items:center;justify-content:center;padding:0;color:var(--_sidebar-fg);background-color:unset;transition:color var(--_transition-easing-x) var(--_transition-duration),top var(--_transition-easing-x) var(--_transition-duration),right var(--_transition-easing-x) var(--_transition-duration),left var(--_transition-easing-x) var(--_transition-duration)}.bslib-sidebar-layout>.collapse-toggle:hover{background-color:var(--_toggle-bg)}.bslib-sidebar-layout>.collapse-toggle>.collapse-icon{opacity:0.8;width:var(--_icon-size);height:var(--_icon-size);transform:rotateY(var(--_toggle-transform));transition:transform var(--_toggle-transition-easing) var(--_transition-duration)}.bslib-sidebar-layout>.collapse-toggle:hover>.collapse-icon{opacity:1}.bslib-sidebar-layout .sidebar-title{font-size:1.25rem;line-height:1.25;margin-top:0;margin-bottom:1rem;padding-bottom:1rem;border-bottom:var(--_border)}.bslib-sidebar-layout.sidebar-right{grid-template-columns:var(--_column-main) Min(calc(100% - var(--_padding-icon)), var(--_sidebar-width))}.bslib-sidebar-layout.sidebar-right>.main{grid-column:1 / 2;border-top-right-radius:0;border-bottom-right-radius:0;border-top-left-radius:inherit;border-bottom-left-radius:inherit}.bslib-sidebar-layout.sidebar-right>.sidebar{grid-column:2 / 3;border-right:none;border-left:var(--_vert-border);border-top-left-radius:0;border-bottom-left-radius:0}.bslib-sidebar-layout.sidebar-right>.collapse-toggle{grid-column:2 / 3;left:var(--_icon-size);right:unset;border:var(--bslib-collapse-toggle-border)}.bslib-sidebar-layout.sidebar-right>.collapse-toggle>.collapse-icon{transform:rotateY(var(--_toggle-right-transform))}.bslib-sidebar-layout.transitioning>.sidebar>.sidebar-content{display:none}.bslib-sidebar-layout.sidebar-collapsed{--_toggle-transform: 180deg;--_toggle-right-transform: 0deg;--_vert-border: none;grid-template-columns:0 minmax(0, 1fr)}.bslib-sidebar-layout.sidebar-collapsed.sidebar-right{grid-template-columns:minmax(0, 1fr) 0}.bslib-sidebar-layout.sidebar-collapsed:not(.transitioning)>.sidebar>*{display:none}.bslib-sidebar-layout.sidebar-collapsed>.main{border-radius:inherit;padding-left:var(--_padding-icon);padding-right:var(--_padding-icon)}.bslib-sidebar-layout.sidebar-collapsed>.collapse-toggle{color:var(--_main-fg);top:var(--_toggle-position-y);right:var(--_toggle-position-x)}.bslib-sidebar-layout.sidebar-collapsed.sidebar-right>.collapse-toggle{left:var(--_toggle-position-x);right:unset}.bslib-sidebar-layout .bslib-sidebar-resize-handle{position:absolute;top:0;bottom:0;width:var(--_resize-handle-width);left:calc(calc(-1 * var(--_resize-handle-width)) - 2px);grid-column:2;pointer-events:none;user-select:none;z-index:calc(1000 + 1)}.bslib-sidebar-layout .bslib-sidebar-resize-handle::before{content:"";position:absolute;top:0;bottom:0;left:100%;width:5px;pointer-events:auto}.bslib-sidebar-layout .bslib-sidebar-resize-handle.handle-active{cursor:ew-resize}.bslib-sidebar-layout .bslib-sidebar-resize-handle.handle-active::before{left:calc(100% - 10px);width:24px}@media (any-pointer: coarse){.bslib-sidebar-layout .bslib-sidebar-resize-handle{pointer-events:auto;cursor:ew-resize}.bslib-sidebar-layout .bslib-sidebar-resize-handle::before{left:0;width:auto;right:calc(-1 * var(--_resize-handle-width) - 2px)}}.bslib-sidebar-layout .bslib-sidebar-resize-handle .resize-indicator{position:absolute;top:50%;right:2px;width:2px;height:30px;transform:translate(-50%, -50%);background-color:var(--_resize-indicator-color);opacity:0.1;border-radius:1px;transition:all 0.15s ease}.bslib-sidebar-layout .bslib-sidebar-resize-handle:hover .resize-indicator,.bslib-sidebar-layout .bslib-sidebar-resize-handle:focus .resize-indicator,.bslib-sidebar-layout .bslib-sidebar-resize-handle:active .resize-indicator,.bslib-sidebar-layout .bslib-sidebar-resize-handle.handle-active .resize-indicator{opacity:1}.bslib-sidebar-layout .bslib-sidebar-resize-handle:hover .resize-indicator,.bslib-sidebar-layout .bslib-sidebar-resize-handle:focus .resize-indicator,.bslib-sidebar-layout .bslib-sidebar-resize-handle.handle-active .resize-indicator{width:3px;height:40px}.bslib-sidebar-layout .bslib-sidebar-resize-handle:active .resize-indicator{background-color:var(--_resize-indicator-color-active);width:4px;height:50px}.bslib-sidebar-layout .bslib-sidebar-resize-handle:focus{outline:none}.bslib-sidebar-layout .bslib-sidebar-resize-handle:focus .resize-indicator{outline:2px solid var(--bs-focus-ring-color, rgba(13,110,253,0.25));outline-offset:2px}.bslib-sidebar-layout.sidebar-right>.bslib-sidebar-resize-handle{left:2px}.bslib-sidebar-layout.sidebar-right>.bslib-sidebar-resize-handle::before{left:auto;right:100%}.bslib-sidebar-layout.sidebar-right>.bslib-sidebar-resize-handle.handle-active::before{right:calc(100% - 10px)}.bslib-sidebar-layout>.sidebar:not([data-resizable])~.bslib-sidebar-resize-handle{display:none}.bslib-sidebar-layout.transitioning>.bslib-sidebar-resize-handle,.bslib-sidebar-layout.sidebar-collapsed>.bslib-sidebar-resize-handle{display:none}.bslib-sidebar-layout.sidebar-resizing{user-select:none}.bslib-sidebar-layout.sidebar-resizing>.bslib-sidebar-resize-handle .resize-indicator{background-color:var(--_resize-indicator-color-active);width:4px;height:50px}.bslib-sidebar-layout{--bslib-sidebar-js-window-size: desktop}@media (max-width: 575.98px){.bslib-sidebar-layout{--bslib-sidebar-js-window-size: mobile}.bslib-sidebar-layout .bslib-sidebar-resize-handle{display:none !important}}@media (min-width: 576px){.bslib-sidebar-layout[data-collapsible-desktop="false"]{--_padding-icon: var(--_padding)}.bslib-sidebar-layout[data-collapsible-desktop="false"]>.collapse-toggle{display:none}.bslib-sidebar-layout[data-collapsible-desktop="false"]>.sidebar[hidden]{display:block !important}.bslib-sidebar-layout[data-collapsible-desktop="false"]>.sidebar[hidden]>.sidebar-content{display:flex !important}}@media (max-width: 575.98px){.bslib-sidebar-layout>.sidebar,.bslib-sidebar-layout.sidebar-right>.sidebar{border:none}.bslib-sidebar-layout>.main,.bslib-sidebar-layout.sidebar-right>.main{grid-column:1 / 3}.bslib-sidebar-layout[data-collapsible-mobile="true"]{grid-template-rows:calc(var(--_icon-button-size) + var(--_padding)) 1fr;grid-template-columns:100% 0}.bslib-sidebar-layout[data-collapsible-mobile="true"]>.collapse-toggle{grid-row:1 / 2}.bslib-sidebar-layout[data-collapsible-mobile="true"]>.main{grid-row:2 / 3}.bslib-sidebar-layout[data-collapsible-mobile="true"]>.sidebar{grid-row:1 / 3}.bslib-sidebar-layout[data-collapsible-mobile="true"]:not(.sidebar-collapsed)>.sidebar,.bslib-sidebar-layout[data-collapsible-mobile="true"].transitioning>.sidebar{z-index:1045}.bslib-sidebar-layout[data-collapsible-mobile="true"]:not(.sidebar-collapsed)>.collapse-toggle,.bslib-sidebar-layout[data-collapsible-mobile="true"].transitioning>.collapse-toggle{z-index:1045}.bslib-sidebar-layout[data-collapsible-mobile="true"]>.collapse-toggle{top:unset;position:relative;align-self:center}.bslib-sidebar-layout[data-collapsible-mobile="true"]:not(.sidebar-right)>.collapse-toggle{left:var(--_icon-size);right:unset;justify-self:left}.bslib-sidebar-layout[data-collapsible-mobile="true"].sidebar-right>.collapse-toggle{right:var(--_icon-size);left:unset;justify-self:right}.bslib-sidebar-layout[data-collapsible-mobile="true"]>.sidebar{opacity:var(--_sidebar-mobile-opacity, 1);max-width:var(--_sidebar-mobile-max-width, 100%);box-shadow:var(--_sidebar-mobile-box-shadow)}.bslib-sidebar-layout[data-collapsible-mobile="true"]>.sidebar{margin:0;padding-top:var(--_padding-icon)}.bslib-sidebar-layout[data-collapsible-mobile="true"]>.sidebar>.sidebar-content{padding-top:0;height:100%;overflow-y:auto}.bslib-sidebar-layout[data-collapsible-mobile="true"]:not(.sidebar-right)>.sidebar{margin-right:auto}.bslib-sidebar-layout[data-collapsible-mobile="true"].sidebar-right>.sidebar{margin-left:auto}.bslib-sidebar-layout[data-collapsible-mobile="true"].sidebar-right{grid-template-columns:0 100%}.bslib-sidebar-layout[data-collapsible-mobile="true"].sidebar-collapsed{grid-template-columns:0 100%}.bslib-sidebar-layout[data-collapsible-mobile="true"].sidebar-collapsed.sidebar-right{grid-template-columns:100% 0}.bslib-sidebar-layout[data-collapsible-mobile="true"]>.main{padding-top:1px;padding-left:var(--_padding);padding-right:var(--_padding)}.bslib-sidebar-layout[data-collapsible-mobile="true"]>.main{opacity:var(--_main-mobile-expanded-opacity);transition:opacity var(--_transition-easing-x) var(--_transition-duration)}.bslib-sidebar-layout[data-collapsible-mobile="true"].sidebar-collapsed>.main{opacity:1}.bslib-sidebar-layout[data-collapsible-mobile="true"]>.main{background-color:none}.bslib-sidebar-layout[data-collapsible-mobile="true"],.bslib-sidebar-layout[data-collapsible-mobile="true"].sidebar-collapsed{background-color:var(--_main-bg)}}@media (max-width: 575.98px){.bslib-sidebar-layout[data-collapsible-mobile="false"]{display:block !important;--_padding-icon: var(--_padding);--_vert-border: var(--_border)}.bslib-sidebar-layout[data-collapsible-mobile="false"]>.sidebar[hidden]{display:block !important}.bslib-sidebar-layout[data-collapsible-mobile="false"]>.sidebar[hidden]>.sidebar-content{display:flex !important}.bslib-sidebar-layout[data-collapsible-mobile="false"]>.sidebar{max-height:var(--_mobile-max-height);overflow-y:auto}.bslib-sidebar-layout[data-collapsible-mobile="false"][data-open-mobile="always"]>.sidebar{border-top:var(--_vert-border)}.bslib-sidebar-layout[data-collapsible-mobile="false"][data-open-mobile="always-above"]>.sidebar{border-bottom:var(--_vert-border)}.bslib-sidebar-layout[data-collapsible-mobile="false"]>.collapse-toggle{display:none}}html[data-bslib-sidebar-resizing="true"]{cursor:ew-resize !important;user-select:none !important}.toast{--bslib-toast-shadow: var(--bs-box-shadow);box-shadow:var(--bslib-toast-shadow);position:relative;overflow:hidden}.toast-body:empty{display:none}.text-bg-primary.toast .toast-body .btn-close,.text-bg-secondary.toast .toast-body .btn-close,.text-bg-success.toast .toast-body .btn-close,.text-bg-info.toast .toast-body .btn-close,.text-bg-warning.toast .toast-body .btn-close,.text-bg-danger.toast .toast-body .btn-close,.text-bg-dark.toast .toast-body .btn-close,.text-white.toast .toast-body .btn-close,.text-light.toast .toast-body .btn-close{filter:var(--bs-btn-close-white-filter)}@keyframes bslib-toast-progress{from{transform:scaleX(0)}to{transform:scaleX(1)}}.bslib-toast-progress-bar{position:absolute;top:0;left:0;height:2px;width:100%;pointer-events:none;z-index:1;transform-origin:left;border-radius:inherit;pointer-events:none;background-color:currentColor}.bslib-toolbar{--_divider-height: var(--bslib-toolbar-divider-height, 1lh);--_divider-width: var(--bslib-toolbar-divider-width, 2px);--_divider-gap: var(--bslib-toolbar-divider-gap, 1rem);--_divider-color: var(--bslib-toolbar-divider-color, var(--bs-border-color-translucent, rgba(40, 70, 94, 0.1)));--_toolbar-btn-size: var(--bslib-toolbar-btn-size, 1.75rem);display:flex;flex-wrap:wrap;align-items:center;gap:0}.bslib-toolbar[data-align="left"]{margin-right:auto;justify-content:start}.bslib-toolbar[data-align="right"]{margin-left:auto;justify-content:end}.bslib-toolbar .bslib-toolbar-input-button,.bslib-toolbar .bslib-toolbar-download-button{align-items:center;justify-content:center;line-height:1;height:var(--_toolbar-btn-size);display:flex}.bslib-toolbar .bslib-toolbar-input-button .action-icon,.bslib-toolbar .bslib-toolbar-input-button .action-label,.bslib-toolbar .bslib-toolbar-download-button .action-icon,.bslib-toolbar .bslib-toolbar-download-button .action-label{margin:0}.bslib-toolbar .bslib-toolbar-input-button.btn-default:not(.btn-primary):not(.btn-secondary):not(.btn-success):not(.btn-danger):not(.btn-warning):not(.btn-info):not(.btn-light):not(.btn-dark),.bslib-toolbar .bslib-toolbar-download-button.btn-default:not(.btn-primary):not(.btn-secondary):not(.btn-success):not(.btn-danger):not(.btn-warning):not(.btn-info):not(.btn-light):not(.btn-dark){background-color:transparent !important;background-image:none !important;color:currentColor !important}.bslib-toolbar .bslib-toolbar-input-button.btn-default:not(.btn-primary):not(.btn-secondary):not(.btn-success):not(.btn-danger):not(.btn-warning):not(.btn-info):not(.btn-light):not(.btn-dark):hover,.bslib-toolbar .bslib-toolbar-download-button.btn-default:not(.btn-primary):not(.btn-secondary):not(.btn-success):not(.btn-danger):not(.btn-warning):not(.btn-info):not(.btn-light):not(.btn-dark):hover{background-color:rgba(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.08) !important;color:currentColor !important}.bslib-toolbar .bslib-toolbar-input-button.btn-default:not(.btn-primary):not(.btn-secondary):not(.btn-success):not(.btn-danger):not(.btn-warning):not(.btn-info):not(.btn-light):not(.btn-dark):active,.bslib-toolbar .bslib-toolbar-download-button.btn-default:not(.btn-primary):not(.btn-secondary):not(.btn-success):not(.btn-danger):not(.btn-warning):not(.btn-info):not(.btn-light):not(.btn-dark):active{background-color:rgba(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.16) !important;color:currentColor !important}.bslib-toolbar .bslib-toolbar-input-button.btn-default:not(.btn-primary):not(.btn-secondary):not(.btn-success):not(.btn-danger):not(.btn-warning):not(.btn-info):not(.btn-light):not(.btn-dark):focus-within,.bslib-toolbar .bslib-toolbar-download-button.btn-default:not(.btn-primary):not(.btn-secondary):not(.btn-success):not(.btn-danger):not(.btn-warning):not(.btn-info):not(.btn-light):not(.btn-dark):focus-within{background-color:rgba(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.12) !important;box-shadow:0 0 0 0.25rem rgba(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.25) !important;color:currentColor !important}.bslib-toolbar .bslib-toolbar-download-button{text-decoration:none}.bslib-toolbar .bslib-toolbar-download-button:hover,.bslib-toolbar .bslib-toolbar-download-button:active,.bslib-toolbar .bslib-toolbar-download-button:focus-within{text-decoration:none !important}.bslib-toolbar .bslib-toolbar-download-button.disabled{opacity:0.65;pointer-events:none}.bslib-toolbar .bslib-toolbar-icon.action-icon svg,.bslib-toolbar .bslib-toolbar-icon.action-icon img{margin:0 !important}.bslib-toolbar .bslib-toolbar-input-button[data-type="label"] .action-label,.bslib-toolbar .bslib-toolbar-download-button[data-type="label"] .action-label{padding-left:0}.bslib-toolbar .bslib-toolbar-input-button[data-type="both"] .action-icon,.bslib-toolbar .bslib-toolbar-download-button[data-type="both"] .action-icon{margin-right:0.35rem}.bslib-toolbar .bslib-toolbar-input-button[data-type="icon"],.bslib-toolbar .bslib-toolbar-download-button[data-type="icon"]{aspect-ratio:1;line-height:1 !important}.bslib-toolbar .bslib-toolbar-input-button[data-type="icon"] .action-label,.bslib-toolbar .bslib-toolbar-download-button[data-type="icon"] .action-label{padding-left:0}.bslib-toolbar .bslib-toolbar-input-button[data-type="icon"]>.action-icon,.bslib-toolbar .bslib-toolbar-download-button[data-type="icon"]>.action-icon{display:flex;align-items:center;justify-content:center;line-height:1;margin:0}.bslib-toolbar .bslib-toolbar-divider{align-self:center;height:var(--_divider-height);width:var(--_divider-gap)}.bslib-toolbar .bslib-toolbar-divider::before{content:"";display:block;width:var(--_divider-width);height:100%;background-color:var(--_divider-color);margin:0 auto}.bslib-toolbar .bslib-toolbar-spacer{margin-left:auto}.bslib-toolbar,.bslib-toolbar *{font-size:0.9rem}.bslib-toolbar>*{margin-bottom:0 !important;width:auto;align-self:center}label:has(>.bslib-toolbar){width:100%}label>.bslib-toolbar{width:100%}.bslib-toolbar-input-select{padding-inline:0.25rem;height:var(--_toolbar-btn-size, 1.75rem);display:inline-flex;align-items:center;width:auto !important;border-radius:var(--bs-border-radius-sm, 0.25rem);gap:0.05rem;transition:border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out}.bslib-toolbar-input-select select{appearance:auto;background-image:none;padding:0.1rem 0.5rem 0.1rem 0.1rem;border:none;background-color:transparent;color:currentColor;line-height:1;width:auto;min-width:fit-content;font-family:inherit}.bslib-toolbar-input-select select:focus{outline:none;box-shadow:none}.bslib-toolbar-input-select:hover{background-color:rgba(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.08)}.bslib-toolbar-input-select:active{background-color:rgba(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.16)}.bslib-toolbar-input-select:focus-within{background-color:rgba(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.12);box-shadow:0 0 0 0.25rem rgba(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.25)}.bslib-toolbar-input-select .bslib-toolbar-icon{display:inline-flex;align-items:center;color:var(--bs-secondary-color);margin-left:0.15rem}.bslib-toolbar-input-select .bslib-toolbar-icon:empty{display:none}.bslib-toolbar-input-select label,.bslib-toolbar-input-select label.control-label{font-weight:600;margin-bottom:0;display:inline-flex;align-items:center}.bslib-toolbar-input-select .bslib-toolbar-label{margin-left:0.15rem}.bslib-value-box{container-name:bslib-value-box;container-type:inline-size}.bslib-value-box.default{--bslib-value-box-bg-default: var(--bs-card-bg, #fff);--bslib-value-box-border-color-default: var(--bs-card-border-color, var(--bs-border-color-translucent));color:var(--bslib-value-box-color, var(--bs-body-color));background-color:var(--bslib-value-box-bg, var(--bslib-value-box-bg-default));border-color:var(--bslib-value-box-border-color, var(--bslib-value-box-border-color-default))}.bslib-value-box .value-box-grid{display:grid;grid-template-areas:"left right";align-items:center;overflow:hidden}.bslib-value-box .value-box-showcase{height:100%;max-height:var(---bslib-value-box-showcase-max-h, 100%)}.bslib-value-box .value-box-showcase,.bslib-value-box .value-box-showcase>.html-fill-item{width:100%}.bslib-value-box[data-full-screen="true"] .value-box-showcase{max-height:var(---bslib-value-box-showcase-max-h-fs, 100%)}@media screen and (min-width: 575.98px){@container bslib-value-box (max-width: 300px){.bslib-value-box:not(.showcase-bottom) .value-box-grid{grid-template-columns:1fr !important;grid-template-rows:auto auto;grid-template-areas:"top" "bottom"}.bslib-value-box:not(.showcase-bottom) .value-box-grid .value-box-showcase{grid-area:top !important}.bslib-value-box:not(.showcase-bottom) .value-box-grid .value-box-area{grid-area:bottom !important;justify-content:end}}}.bslib-value-box .value-box-area{justify-content:center;padding:1.5rem 1rem;font-size:.9rem;font-weight:500}.bslib-value-box .value-box-area *{margin-bottom:0;margin-top:0}.bslib-value-box .value-box-title{font-size:1rem;margin-top:0;margin-bottom:.5rem;font-weight:400;line-height:1.2}.bslib-value-box .value-box-title:empty::after{content:'\00a0 '}.bslib-value-box .value-box-value{font-size:calc(1.325rem + .9vw);margin-top:0;margin-bottom:.5rem;font-weight:400;line-height:1.2}@media (min-width: 1200px){.bslib-value-box .value-box-value{font-size:2rem}}.bslib-value-box .value-box-value:empty::after{content:'\00a0 '}.bslib-value-box .value-box-showcase{align-items:center;justify-content:center;margin-top:auto;margin-bottom:auto;padding:1rem}.bslib-value-box .value-box-showcase .bi,.bslib-value-box .value-box-showcase .fa,.bslib-value-box .value-box-showcase .fab,.bslib-value-box .value-box-showcase .fas,.bslib-value-box .value-box-showcase .far{opacity:.85;min-width:50px;max-width:125%}.bslib-value-box .value-box-showcase .bi,.bslib-value-box .value-box-showcase .fa,.bslib-value-box .value-box-showcase .fab,.bslib-value-box .value-box-showcase .fas,.bslib-value-box .value-box-showcase .far{font-size:4rem}.bslib-value-box.showcase-top-right .value-box-grid{grid-template-columns:1fr var(---bslib-value-box-showcase-w, 50%)}.bslib-value-box.showcase-top-right .value-box-grid .value-box-showcase{grid-area:right;margin-left:auto;align-self:start;align-items:end;padding-left:0;padding-bottom:0}.bslib-value-box.showcase-top-right .value-box-grid .value-box-area{grid-area:left;align-self:end}.bslib-value-box.showcase-top-right[data-full-screen="true"] .value-box-grid{grid-template-columns:auto var(---bslib-value-box-showcase-w-fs, 1fr)}.bslib-value-box.showcase-top-right[data-full-screen="true"] .value-box-grid>div{align-self:center}.bslib-value-box.showcase-top-right:not([data-full-screen="true"]) .value-box-showcase{margin-top:0}@container bslib-value-box (max-width: 300px){.bslib-value-box.showcase-top-right:not([data-full-screen="true"]) .value-box-grid .value-box-showcase{padding-left:1rem}}.bslib-value-box.showcase-left-center .value-box-grid{grid-template-columns:var(---bslib-value-box-showcase-w, 30%) auto}.bslib-value-box.showcase-left-center[data-full-screen="true"] .value-box-grid{grid-template-columns:var(---bslib-value-box-showcase-w-fs, 1fr) auto}.bslib-value-box.showcase-left-center:not([data-fill-screen="true"]) .value-box-grid .value-box-showcase{grid-area:left}.bslib-value-box.showcase-left-center:not([data-fill-screen="true"]) .value-box-grid .value-box-area{grid-area:right}.bslib-value-box.showcase-bottom .value-box-grid{grid-template-columns:1fr;grid-template-rows:1fr var(---bslib-value-box-showcase-h, auto);grid-template-areas:"top" "bottom";overflow:hidden}.bslib-value-box.showcase-bottom .value-box-grid .value-box-showcase{grid-area:bottom;padding:0;margin:0}.bslib-value-box.showcase-bottom .value-box-grid .value-box-area{grid-area:top}.bslib-value-box.showcase-bottom[data-full-screen="true"] .value-box-grid{grid-template-rows:1fr var(---bslib-value-box-showcase-h-fs, 2fr)}.bslib-value-box.showcase-bottom[data-full-screen="true"] .value-box-grid .value-box-showcase{padding:1rem} From 7f4c636f7ad8a30b75cb31a7389c1f90366a0956 Mon Sep 17 00:00:00 2001 From: E Nelson Date: Fri, 29 May 2026 17:37:32 -0400 Subject: [PATCH 43/49] test: clarify show_label test comment re: icon default --- tests/testthat/test-toolbar.R | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/testthat/test-toolbar.R b/tests/testthat/test-toolbar.R index d9192b30a..7bd58b61a 100644 --- a/tests/testthat/test-toolbar.R +++ b/tests/testthat/test-toolbar.R @@ -1074,6 +1074,8 @@ test_that("toolbar_download_button() basic structure", { }) test_that("toolbar_download_button() with show_label", { + # icon defaults to shiny::icon("download"), so show_label = TRUE produces + # data-type = "both" (icon + label). For data-type = "label", pass icon = NULL. btn <- toolbar_download_button( outputId = "dl_test", label = "Download CSV", From e2a889ef201c584f6f883bb6ba1f8d33d1102560 Mon Sep 17 00:00:00 2001 From: E Nelson Date: Fri, 29 May 2026 17:53:02 -0400 Subject: [PATCH 44/49] fix: update_toolbar_download_button() and test cleanup --- R/toolbar.R | 6 ++---- tests/testthat/test-toolbar.R | 34 ++++++++++++++-------------------- 2 files changed, 16 insertions(+), 24 deletions(-) diff --git a/R/toolbar.R b/R/toolbar.R index 62d1bb9b6..1520868c9 100644 --- a/R/toolbar.R +++ b/R/toolbar.R @@ -1023,8 +1023,7 @@ toolbar_download_button <- function( enabled = c("auto", TRUE, FALSE), border = FALSE ) { - # TODO-REENABLE-GATE: temporarily disabled for local testing on stable shiny. - # check_shiny_supports_download_button_enabled("toolbar_download_button()") + check_shiny_supports_download_button_enabled("toolbar_download_button()") # Normalize the match.arg vector default to "auto", then hard-error on # anything else invalid (mirrors shiny::downloadButton()'s approach). if (identical(enabled, c("auto", TRUE, FALSE))) enabled <- "auto" @@ -1143,8 +1142,7 @@ update_toolbar_download_button <- function( disabled = NULL, session = get_current_session() ) { - # TODO-REENABLE-GATE: temporarily disabled for local testing on stable shiny. - # check_shiny_supports_download_button_enabled("update_toolbar_download_button()") + check_shiny_supports_download_button_enabled("update_toolbar_download_button()") if (!is.null(label)) { label_text <- paste(unlist(find_characters(label)), collapse = " ") if (!nzchar(trimws(label_text))) { diff --git a/tests/testthat/test-toolbar.R b/tests/testthat/test-toolbar.R index 7bd58b61a..2353d2c88 100644 --- a/tests/testthat/test-toolbar.R +++ b/tests/testthat/test-toolbar.R @@ -1073,16 +1073,6 @@ test_that("toolbar_download_button() basic structure", { expect_match(htmltools::tagGetAttribute(btn_tag, "data-type"), "icon") }) -test_that("toolbar_download_button() with show_label", { - # icon defaults to shiny::icon("download"), so show_label = TRUE produces - # data-type = "both" (icon + label). For data-type = "label", pass icon = NULL. - btn <- toolbar_download_button( - outputId = "dl_test", - label = "Download CSV", - show_label = TRUE - ) - expect_match(htmltools::tagGetAttribute(btn, "data-type"), "both") -}) test_that("toolbar_download_button() enabled = 'auto' (default)", { btn <- toolbar_download_button(outputId = "dl_test", show_label = TRUE) @@ -1175,9 +1165,18 @@ test_that("toolbar_download_button() aborts when show_label = FALSE and no icon" ) }) -test_that("toolbar_download_button() label-only type (icon = NULL, show_label = TRUE)", { - btn <- toolbar_download_button("dl_label_only", icon = NULL, show_label = TRUE) - expect_match(htmltools::tagGetAttribute(btn, "data-type"), "label") +test_that("toolbar_download_button() data-type: label, both, icon", { + # "label": no icon, label visible + btn_label <- toolbar_download_button("dl_label_only", icon = NULL, show_label = TRUE) + expect_match(htmltools::tagGetAttribute(btn_label, "data-type"), "label") + + # "both": default icon + label visible + btn_both <- toolbar_download_button("dl_both", show_label = TRUE) + expect_match(htmltools::tagGetAttribute(btn_both, "data-type"), "both") + + # "icon": default icon only (show_label = FALSE is the default) + btn_icon_tag <- tagQuery(as.tags(toolbar_download_button("dl_icon")))$find("a")$selectedTags()[[1]] + expect_match(htmltools::tagGetAttribute(btn_icon_tag, "data-type"), "icon") }) test_that("toolbar_download_button() border = TRUE", { @@ -1262,15 +1261,10 @@ test_that("update_toolbar_download_button() sends only specified fields", { } ) - # Only specified fields appear — NULLs are dropped + # Only specified fields appear — NULLs are dropped; id always included update_toolbar_download_button("dl_target", disabled = TRUE, session = session) - expect_equal(sort(names(session$last_message)), c("disabled", "id")) - - # No args → id-only message on the correct channel - update_toolbar_download_button("dl_target", session = session) expect_equal(session$last_type, "bslib.toolbar-download-button") - expect_equal(names(session$last_message), "id") - expect_equal(session$last_message$id, "dl_target") + expect_equal(sort(names(session$last_message)), c("disabled", "id")) }) test_that("update_toolbar_download_button() warns on empty label", { From eecaa9e9b664e0acdd4bb85c2930f4ea52b7c05b Mon Sep 17 00:00:00 2001 From: E Nelson Date: Fri, 29 May 2026 17:58:15 -0400 Subject: [PATCH 45/49] docs: inheritParams for update_toolbar_download_button --- R/toolbar.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/R/toolbar.R b/R/toolbar.R index 1520868c9..cd058dc07 100644 --- a/R/toolbar.R +++ b/R/toolbar.R @@ -1127,10 +1127,10 @@ toolbar_download_button <- function( button } +#' @inheritParams toolbar_download_button +#' @inheritParams update_toolbar_input_button #' @param disabled If `TRUE`, disables the button; if `FALSE`, enables it. #' `NULL` (default) leaves the current state unchanged. -#' @param session A Shiny session object (the default should almost always be -#' used). #' #' @describeIn toolbar_download_button Update a toolbar download button. #' @export From 02457f1167c58d0c1d31ed594cdfe005eae9f055 Mon Sep 17 00:00:00 2001 From: elnelson575 Date: Fri, 29 May 2026 22:00:10 +0000 Subject: [PATCH 46/49] `air format` (GitHub Actions) --- R/toolbar.R | 14 ++++++-- tests/testthat/test-toolbar.R | 61 ++++++++++++++++++++++++++++------- 2 files changed, 60 insertions(+), 15 deletions(-) diff --git a/R/toolbar.R b/R/toolbar.R index cd058dc07..84e547934 100644 --- a/R/toolbar.R +++ b/R/toolbar.R @@ -1026,10 +1026,16 @@ toolbar_download_button <- function( check_shiny_supports_download_button_enabled("toolbar_download_button()") # Normalize the match.arg vector default to "auto", then hard-error on # anything else invalid (mirrors shiny::downloadButton()'s approach). - if (identical(enabled, c("auto", TRUE, FALSE))) enabled <- "auto" + if (identical(enabled, c("auto", TRUE, FALSE))) { + enabled <- "auto" + } if (!isTRUE(enabled) && !isFALSE(enabled) && !identical(enabled, "auto")) { rlang::abort( - paste0('`enabled` must be TRUE, FALSE, or "auto". Got ', deparse(enabled), ".") + paste0( + '`enabled` must be TRUE, FALSE, or "auto". Got ', + deparse(enabled), + "." + ) ) } @@ -1142,7 +1148,9 @@ update_toolbar_download_button <- function( disabled = NULL, session = get_current_session() ) { - check_shiny_supports_download_button_enabled("update_toolbar_download_button()") + check_shiny_supports_download_button_enabled( + "update_toolbar_download_button()" + ) if (!is.null(label)) { label_text <- paste(unlist(find_characters(label)), collapse = " ") if (!nzchar(trimws(label_text))) { diff --git a/tests/testthat/test-toolbar.R b/tests/testthat/test-toolbar.R index 2353d2c88..72451f2e3 100644 --- a/tests/testthat/test-toolbar.R +++ b/tests/testthat/test-toolbar.R @@ -536,7 +536,9 @@ test_that("toolbar_input_select() icon parameter", { ) # Icon wrapper is always rendered; when no icon is given its only child is NULL # (CSS hides the span via :empty when there's no rendered content) - icon_span <- tagQuery(select_no_icon)$find(".bslib-toolbar-icon")$selectedTags()[[1]] + icon_span <- tagQuery(select_no_icon)$find( + ".bslib-toolbar-icon" + )$selectedTags()[[1]] expect_null(icon_span$children[[1]]) # With icon @@ -1094,11 +1096,17 @@ test_that("toolbar_download_button() enabled = TRUE", { enabled = TRUE, show_label = TRUE ) - expect_false(grepl("\\bdisabled\\b", htmltools::tagGetAttribute(btn, "class") %||% "")) + expect_false(grepl( + "\\bdisabled\\b", + htmltools::tagGetAttribute(btn, "class") %||% "" + )) expect_null(htmltools::tagGetAttribute(btn, "aria-disabled")) expect_null(htmltools::tagGetAttribute(btn, "tabindex")) # data-shiny-disable-auto-enable present — Shiny must NOT override enabled state - expect_false(is.null(htmltools::tagGetAttribute(btn, "data-shiny-disable-auto-enable"))) + expect_false(is.null(htmltools::tagGetAttribute( + btn, + "data-shiny-disable-auto-enable" + ))) expect_snapshot_html( toolbar_download_button( @@ -1119,7 +1127,10 @@ test_that("toolbar_download_button() enabled = FALSE", { expect_equal(htmltools::tagGetAttribute(btn, "aria-disabled"), "true") expect_equal(htmltools::tagGetAttribute(btn, "tabindex"), "-1") # data-shiny-disable-auto-enable present — Shiny must NOT auto-enable - expect_false(is.null(htmltools::tagGetAttribute(btn, "data-shiny-disable-auto-enable"))) + expect_false(is.null(htmltools::tagGetAttribute( + btn, + "data-shiny-disable-auto-enable" + ))) expect_snapshot_html( toolbar_download_button( @@ -1167,7 +1178,11 @@ test_that("toolbar_download_button() aborts when show_label = FALSE and no icon" test_that("toolbar_download_button() data-type: label, both, icon", { # "label": no icon, label visible - btn_label <- toolbar_download_button("dl_label_only", icon = NULL, show_label = TRUE) + btn_label <- toolbar_download_button( + "dl_label_only", + icon = NULL, + show_label = TRUE + ) expect_match(htmltools::tagGetAttribute(btn_label, "data-type"), "label") # "both": default icon + label visible @@ -1175,13 +1190,20 @@ test_that("toolbar_download_button() data-type: label, both, icon", { expect_match(htmltools::tagGetAttribute(btn_both, "data-type"), "both") # "icon": default icon only (show_label = FALSE is the default) - btn_icon_tag <- tagQuery(as.tags(toolbar_download_button("dl_icon")))$find("a")$selectedTags()[[1]] + btn_icon_tag <- tagQuery(as.tags(toolbar_download_button("dl_icon")))$find( + "a" + )$selectedTags()[[1]] expect_match(htmltools::tagGetAttribute(btn_icon_tag, "data-type"), "icon") }) test_that("toolbar_download_button() border = TRUE", { expect_snapshot_html( - toolbar_download_button("dl_border", label = "Download", show_label = TRUE, border = TRUE) + toolbar_download_button( + "dl_border", + label = "Download", + show_label = TRUE, + border = TRUE + ) ) }) @@ -1197,7 +1219,11 @@ test_that("toolbar_download_button() errors on invalid enabled value", { }) test_that("toolbar_download_button() merges class arg into button class", { - btn <- toolbar_download_button("dl_cls", label = "Export", class = "btn-success") + btn <- toolbar_download_button( + "dl_cls", + label = "Export", + class = "btn-success" + ) btn_tag <- tagQuery(as.tags(btn))$find("a")$selectedTags()[[1]] cls <- htmltools::tagGetAttribute(btn_tag, "class") expect_match(cls, "bslib-toolbar-download-button") @@ -1213,7 +1239,6 @@ test_that("toolbar_download_button() warns on empty label", { # Tests for update_toolbar_download_button() # - test_that("update_toolbar_download_button() can disable and re-enable", { session <- list( sendCustomMessage = function(type, message) { @@ -1221,11 +1246,19 @@ test_that("update_toolbar_download_button() can disable and re-enable", { } ) - update_toolbar_download_button("dl_target", disabled = TRUE, session = session) + update_toolbar_download_button( + "dl_target", + disabled = TRUE, + session = session + ) expect_equal(session$last_message$id, "dl_target") expect_equal(session$last_message$disabled, TRUE) - update_toolbar_download_button("dl_target", disabled = FALSE, session = session) + update_toolbar_download_button( + "dl_target", + disabled = FALSE, + session = session + ) expect_equal(session$last_message$id, "dl_target") expect_equal(session$last_message$disabled, FALSE) }) @@ -1262,7 +1295,11 @@ test_that("update_toolbar_download_button() sends only specified fields", { ) # Only specified fields appear — NULLs are dropped; id always included - update_toolbar_download_button("dl_target", disabled = TRUE, session = session) + update_toolbar_download_button( + "dl_target", + disabled = TRUE, + session = session + ) expect_equal(session$last_type, "bslib.toolbar-download-button") expect_equal(sort(names(session$last_message)), c("disabled", "id")) }) From 0f4d60af024bd3367a6beaa56a232b37038494f0 Mon Sep 17 00:00:00 2001 From: elnelson575 Date: Fri, 29 May 2026 22:00:42 +0000 Subject: [PATCH 47/49] `devtools::document()` (GitHub Actions) --- man/toolbar.Rd | 10 +++++----- man/toolbar_divider.Rd | 10 +++++----- man/toolbar_input_button.Rd | 10 +++++----- man/toolbar_input_select.Rd | 10 +++++----- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/man/toolbar.Rd b/man/toolbar.Rd index 55eff7fa5..00483eb4d 100644 --- a/man/toolbar.Rd +++ b/man/toolbar.Rd @@ -251,10 +251,10 @@ shinyApp(ui, server) \seealso{ \code{\link[=card_header]{card_header()}} for using toolbars in card headers/footers -Other toolbar components: -\code{\link{toolbar_divider}()}, -\code{\link{toolbar_download_button}()}, -\code{\link{toolbar_input_button}()}, -\code{\link{toolbar_input_select}()} +Other toolbar components: +\code{\link[=toolbar_divider]{toolbar_divider()}}, +\code{\link[=toolbar_download_button]{toolbar_download_button()}}, +\code{\link[=toolbar_input_button]{toolbar_input_button()}}, +\code{\link[=toolbar_input_select]{toolbar_input_select()}} } \concept{toolbar components} diff --git a/man/toolbar_divider.Rd b/man/toolbar_divider.Rd index 897dd0d8e..97f7ea786 100644 --- a/man/toolbar_divider.Rd +++ b/man/toolbar_divider.Rd @@ -60,10 +60,10 @@ toolbar( \dontshow{\}) # examplesIf} } \seealso{ -Other toolbar components: -\code{\link{toolbar}()}, -\code{\link{toolbar_download_button}()}, -\code{\link{toolbar_input_button}()}, -\code{\link{toolbar_input_select}()} +Other toolbar components: +\code{\link[=toolbar]{toolbar()}}, +\code{\link[=toolbar_download_button]{toolbar_download_button()}}, +\code{\link[=toolbar_input_button]{toolbar_input_button()}}, +\code{\link[=toolbar_input_select]{toolbar_input_select()}} } \concept{toolbar components} diff --git a/man/toolbar_input_button.Rd b/man/toolbar_input_button.Rd index f90aa57cd..40acea03f 100644 --- a/man/toolbar_input_button.Rd +++ b/man/toolbar_input_button.Rd @@ -164,10 +164,10 @@ toolbar( \dontshow{\}) # examplesIf} } \seealso{ -Other toolbar components: -\code{\link{toolbar}()}, -\code{\link{toolbar_divider}()}, -\code{\link{toolbar_download_button}()}, -\code{\link{toolbar_input_select}()} +Other toolbar components: +\code{\link[=toolbar]{toolbar()}}, +\code{\link[=toolbar_divider]{toolbar_divider()}}, +\code{\link[=toolbar_download_button]{toolbar_download_button()}}, +\code{\link[=toolbar_input_select]{toolbar_input_select()}} } \concept{toolbar components} diff --git a/man/toolbar_input_select.Rd b/man/toolbar_input_select.Rd index 7700ac9d0..0a8a6fd40 100644 --- a/man/toolbar_input_select.Rd +++ b/man/toolbar_input_select.Rd @@ -164,10 +164,10 @@ toolbar( \dontshow{\}) # examplesIf} } \seealso{ -Other toolbar components: -\code{\link{toolbar}()}, -\code{\link{toolbar_divider}()}, -\code{\link{toolbar_download_button}()}, -\code{\link{toolbar_input_button}()} +Other toolbar components: +\code{\link[=toolbar]{toolbar()}}, +\code{\link[=toolbar_divider]{toolbar_divider()}}, +\code{\link[=toolbar_download_button]{toolbar_download_button()}}, +\code{\link[=toolbar_input_button]{toolbar_input_button()}} } \concept{toolbar components} From ad39e5625907b0df2f770904a4ffd01cebe4841f Mon Sep 17 00:00:00 2001 From: elnelson575 Date: Fri, 29 May 2026 22:01:36 +0000 Subject: [PATCH 48/49] `yarn build` (GitHub Actions) --- inst/components/dist/components.js.map | 4 ++-- inst/components/dist/components.min.js | 8 ++++---- inst/components/dist/components.min.js.map | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/inst/components/dist/components.js.map b/inst/components/dist/components.js.map index 8a1d59901..2c8452b76 100644 --- a/inst/components/dist/components.js.map +++ b/inst/components/dist/components.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../../../srcts/src/components/_utils.ts", "../../../srcts/src/components/accordion.ts", "../../../srcts/src/components/_shinyResizeObserver.ts", "../../../srcts/src/components/_shinyRemovedObserver.ts", "../../../srcts/src/components/card.ts", "../../../srcts/src/components/sidebar.ts", "../../../srcts/src/components/taskButton.ts", "../../../srcts/src/components/toolbarInputButton.ts", "../../../srcts/src/components/_shinyAddCustomMessageHandlers.ts", "../../../srcts/src/components/toolbarDownloadButton.ts", "../../../srcts/src/components/toolbarInputSelect.ts", "../../../srcts/src/components/submitTextArea.ts", "../../../srcts/src/components/toast.ts", "../../../srcts/src/components/index.ts"], - "sourcesContent": ["import type { HtmlDep } from \"rstudio-shiny/srcts/types/src/shiny/render\";\n\nimport type { InputBinding as InputBindingType } from \"rstudio-shiny/srcts/types/src/bindings/input\";\n\nimport type { ShinyClass } from \"rstudio-shiny/srcts/types/src\";\n\n// eslint-disable-next-line @typescript-eslint/naming-convention\nconst Shiny: ShinyClass | undefined = window.Shiny;\n\n// Exclude undefined from T\ntype NotUndefined = T extends undefined ? never : T;\n\n// eslint-disable-next-line @typescript-eslint/naming-convention\nconst InputBinding = (\n Shiny ? Shiny.InputBinding : class {}\n) as typeof InputBindingType;\n\nfunction registerBinding(\n inputBindingClass: new () => InputBindingType,\n name: string\n): void {\n if (Shiny) {\n Shiny.inputBindings.register(new inputBindingClass(), \"bslib.\" + name);\n }\n}\n\nfunction registerBslibGlobal(name: string, value: object): void {\n (window as any).bslib = (window as any).bslib || {};\n if (!(window as any).bslib[name]) {\n (window as any).bslib[name] = value;\n } else {\n console.error(\n `[bslib] Global window.bslib.${name} was already defined, using previous definition.`\n );\n }\n}\n\ntype ShinyClientMessage = {\n message: string;\n headline?: string;\n status?: \"error\" | \"info\" | \"warning\";\n};\n\nfunction showShinyClientMessage({\n headline = \"\",\n message,\n status = \"warning\",\n}: ShinyClientMessage): void {\n document.dispatchEvent(\n new CustomEvent(\"shiny:client-message\", {\n detail: { headline: headline, message: message, status: status },\n })\n );\n}\n\n// Return true if the key exists on the object and the value is not undefined.\n//\n// This method is mainly used in input bindings' `receiveMessage` method.\n// Since we know that the values are sent by Shiny via `{jsonlite}`,\n// then we know that there are no `undefined` values. `null` is possible, but not `undefined`.\nfunction hasDefinedProperty<\n Prop extends keyof X,\n X extends { [key: string]: any }\n>(\n obj: X,\n prop: Prop\n): obj is X & { [key in NonNullable]: NotUndefined } {\n return (\n Object.prototype.hasOwnProperty.call(obj, prop) && obj[prop] !== undefined\n );\n}\n\n// TODO: Shiny should trigger resize events when the output\n// https://github.com/rstudio/shiny/pull/3682\nfunction doWindowResizeOnElementResize(el: HTMLElement): void {\n if ($(el).data(\"window-resize-observer\")) {\n return;\n }\n const resizeEvent = new Event(\"resize\");\n const ro = new ResizeObserver(() => {\n window.dispatchEvent(resizeEvent);\n });\n ro.observe(el);\n $(el).data(\"window-resize-observer\", ro);\n}\n\nfunction getAllFocusableChildren(el: HTMLElement): HTMLElement[] {\n // Cross-referenced with https://allyjs.io/data-tables/focusable.html\n const base = [\n \"a[href]\",\n \"area[href]\",\n \"button\",\n \"details summary\",\n \"input\",\n \"iframe\",\n \"select\",\n \"textarea\",\n '[contentEditable=\"\"]',\n '[contentEditable=\"true\"]',\n '[contentEditable=\"TRUE\"]',\n \"[tabindex]\",\n ];\n const modifiers = [':not([tabindex=\"-1\"])', \":not([disabled])\"];\n const selectors = base.map((b) => b + modifiers.join(\"\"));\n const focusable = el.querySelectorAll(selectors.join(\", \"));\n return Array.from(focusable) as HTMLElement[];\n}\n\nasync function shinyRenderContent(\n ...args: Parameters\n): Promise {\n if (!Shiny) {\n throw new Error(\"This function must be called in a Shiny app.\");\n }\n if (Shiny.renderContentAsync) {\n return await Shiny.renderContentAsync.apply(null, args);\n } else {\n return await Shiny.renderContent.apply(null, args);\n }\n}\n\n// Copied from shiny utils\nasync function updateLabel(\n labelContent: string | { html: string; deps: HtmlDep[] } | undefined,\n labelNode: JQuery\n): Promise {\n // Only update if label was specified in the update method\n if (typeof labelContent === \"undefined\") return;\n if (labelNode.length !== 1) {\n throw new Error(\"labelNode must be of length 1\");\n }\n\n if (typeof labelContent === \"string\") {\n labelContent = {\n html: labelContent,\n deps: [],\n };\n }\n\n if (labelContent.html === \"\") {\n labelNode.addClass(\"shiny-label-null\");\n } else {\n await shinyRenderContent(labelNode, labelContent);\n labelNode.removeClass(\"shiny-label-null\");\n }\n}\n\nexport {\n InputBinding,\n registerBinding,\n registerBslibGlobal,\n hasDefinedProperty,\n doWindowResizeOnElementResize,\n getAllFocusableChildren,\n shinyRenderContent,\n showShinyClientMessage,\n Shiny,\n updateLabel,\n};\nexport type { HtmlDep, ShinyClientMessage };\n", "import type { HtmlDep } from \"./_utils\";\nimport {\n InputBinding,\n registerBinding,\n hasDefinedProperty,\n shinyRenderContent,\n} from \"./_utils\";\n\ntype AccordionItem = {\n item: HTMLElement;\n value: string;\n isOpen: () => boolean;\n show: () => void;\n hide: () => void;\n};\n\ntype HTMLContent = {\n html: string;\n deps?: HtmlDep[];\n};\n\ntype SetMessage = {\n method: \"set\";\n values: string[];\n};\n\ntype OpenMessage = {\n method: \"open\";\n values: string[] | true;\n};\n\ntype CloseMessage = {\n method: \"close\";\n values: string[] | true;\n};\n\ntype InsertMessage = {\n method: \"insert\";\n panel: HTMLContent;\n target: string;\n position: \"after\" | \"before\";\n};\n\ntype RemoveMessage = {\n method: \"remove\";\n target: string[];\n};\n\ntype UpdateMessage = {\n method: \"update\";\n target: string;\n value: string;\n body: HTMLContent;\n title: HTMLContent;\n icon: HTMLContent;\n};\n\ntype MessageData =\n | CloseMessage\n | InsertMessage\n | OpenMessage\n | RemoveMessage\n | SetMessage\n | UpdateMessage;\n\nclass AccordionInputBinding extends InputBinding {\n find(scope: HTMLElement) {\n return $(scope).find(\".accordion.bslib-accordion-input\");\n }\n\n getValue(el: HTMLElement): string[] | null {\n const items = this._getItemInfo(el);\n const selected = items.filter((x) => x.isOpen()).map((x) => x.value);\n return selected.length === 0 ? null : selected;\n }\n\n subscribe(el: HTMLElement, callback: (x: boolean) => void) {\n $(el).on(\n \"shown.bs.collapse.accordionInputBinding hidden.bs.collapse.accordionInputBinding\",\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n function (event) {\n callback(true);\n }\n );\n }\n\n unsubscribe(el: HTMLElement) {\n $(el).off(\".accordionInputBinding\");\n }\n\n async receiveMessage(el: HTMLElement, data: MessageData) {\n const method = data.method;\n if (method === \"set\") {\n this._setItems(el, data);\n } else if (method === \"open\") {\n this._openItems(el, data);\n } else if (method === \"close\") {\n this._closeItems(el, data);\n } else if (method === \"remove\") {\n this._removeItem(el, data);\n } else if (method === \"insert\") {\n await this._insertItem(el, data);\n } else if (method === \"update\") {\n await this._updateItem(el, data);\n } else {\n throw new Error(`Method not yet implemented: ${method}`);\n }\n }\n\n protected _setItems(el: HTMLElement, data: SetMessage) {\n const items = this._getItemInfo(el);\n const vals = this._getValues(el, items, data.values);\n items.forEach((x) => {\n vals.indexOf(x.value) > -1 ? x.show() : x.hide();\n });\n }\n\n protected _openItems(el: HTMLElement, data: OpenMessage) {\n const items = this._getItemInfo(el);\n const vals = this._getValues(el, items, data.values);\n items.forEach((x) => {\n if (vals.indexOf(x.value) > -1) x.show();\n });\n }\n\n protected _closeItems(el: HTMLElement, data: CloseMessage) {\n const items = this._getItemInfo(el);\n const vals = this._getValues(el, items, data.values);\n items.forEach((x) => {\n if (vals.indexOf(x.value) > -1) x.hide();\n });\n }\n\n protected async _insertItem(el: HTMLElement, data: InsertMessage) {\n let targetItem = this._findItem(el, data.target);\n\n // If no target was specified, or the target was not found, then default\n // to the first or last item, depending on the position\n if (!targetItem) {\n targetItem = (\n data.position === \"before\" ? el.firstElementChild : el.lastElementChild\n ) as HTMLElement;\n }\n\n const panel = data.panel;\n\n // If there is still no targetItem, then there are no items in the accordion\n if (targetItem) {\n await shinyRenderContent(\n targetItem,\n panel,\n data.position === \"before\" ? \"beforeBegin\" : \"afterEnd\"\n );\n } else {\n await shinyRenderContent(el, panel);\n }\n\n // Need to add a reference to the parent id that makes autoclose to work\n if (this._isAutoClosing(el)) {\n const val = $(panel.html).attr(\"data-value\");\n $(el)\n .find(`[data-value=\"${val}\"] .accordion-collapse`)\n .attr(\"data-bs-parent\", \"#\" + el.id);\n }\n }\n\n protected _removeItem(el: HTMLElement, data: RemoveMessage) {\n const targetItems = this._getItemInfo(el).filter(\n (x) => data.target.indexOf(x.value) > -1\n );\n\n const unbindAll = window.Shiny?.unbindAll;\n\n targetItems.forEach((x) => {\n if (unbindAll) unbindAll(x.item);\n x.item.remove();\n });\n }\n\n protected async _updateItem(el: HTMLElement, data: UpdateMessage) {\n const target = this._findItem(el, data.target);\n\n if (!target) {\n throw new Error(\n `Unable to find an accordion_panel() with a value of ${data.target}`\n );\n }\n\n if (hasDefinedProperty(data, \"value\")) {\n target.dataset.value = data.value;\n }\n\n if (hasDefinedProperty(data, \"body\")) {\n const body = target.querySelector(\".accordion-body\") as HTMLElement; // always exists\n await shinyRenderContent(body, data.body);\n }\n\n const header = target.querySelector(\".accordion-header\") as HTMLElement; // always exists\n\n if (hasDefinedProperty(data, \"title\")) {\n const title = header.querySelector(\".accordion-title\") as HTMLElement; // always exists\n await shinyRenderContent(title, data.title);\n }\n\n if (hasDefinedProperty(data, \"icon\")) {\n const icon = header.querySelector(\n \".accordion-button > .accordion-icon\"\n ) as HTMLElement; // always exists\n await shinyRenderContent(icon, data.icon);\n }\n }\n\n protected _getItemInfo(el: HTMLElement): AccordionItem[] {\n const items = Array.from(\n el.querySelectorAll(\":scope > .accordion-item\")\n ) as HTMLElement[];\n return items.map((x) => this._getSingleItemInfo(x));\n }\n\n protected _getSingleItemInfo(x: HTMLElement): AccordionItem {\n const collapse = x.querySelector(\".accordion-collapse\") as HTMLElement;\n const isOpen = () => $(collapse).hasClass(\"show\");\n return {\n item: x,\n value: x.dataset.value as string,\n isOpen: isOpen,\n show: () => {\n if (!isOpen()) $(collapse).collapse(\"show\");\n },\n hide: () => {\n if (isOpen()) $(collapse).collapse(\"hide\");\n },\n };\n }\n\n protected _getValues(\n el: HTMLElement,\n items: AccordionItem[],\n values: string[] | true\n ): string[] {\n let vals = values !== true ? values : items.map((x) => x.value);\n const autoclose = this._isAutoClosing(el);\n if (autoclose) {\n vals = vals.slice(vals.length - 1, vals.length);\n }\n return vals;\n }\n\n protected _findItem(el: HTMLElement, value: string): HTMLElement | null {\n return el.querySelector(`[data-value=\"${value}\"]`);\n }\n\n protected _isAutoClosing(el: HTMLElement): boolean {\n return el.classList.contains(\"autoclose\");\n }\n}\n\nregisterBinding(AccordionInputBinding, \"accordion\");\n", "/**\n * A resize observer that ensures Shiny outputs resize during or just after\n * their parent container size changes. Useful, in particular, for sidebar\n * transitions or for full-screen card transitions.\n *\n * @class ShinyResizeObserver\n * @typedef {ShinyResizeObserver}\n */\nclass ShinyResizeObserver {\n /**\n * The actual ResizeObserver instance.\n * @private\n * @type {ResizeObserver}\n */\n private resizeObserver: ResizeObserver;\n /**\n * An array of elements that are currently being watched by the Resize\n * Observer.\n *\n * @details\n * We don't currently have lifecycle hooks that allow us to unobserve elements\n * when they are removed from the DOM. As a result, we need to manually check\n * that the elements we're watching still exist in the DOM. This array keeps\n * track of the elements we're watching so that we can check them later.\n * @private\n * @type {HTMLElement[]}\n */\n private resizeObserverEntries: HTMLElement[];\n\n /**\n * Watch containers for size changes and ensure that Shiny outputs and\n * htmlwidgets within resize appropriately.\n *\n * @details\n * The ShinyResizeObserver is used to watch the containers, such as Sidebars\n * and Cards for size changes, in particular when the sidebar state is toggled\n * or the card body is expanded full screen. It performs two primary tasks:\n *\n * 1. Dispatches a `resize` event on the window object. This is necessary to\n * ensure that Shiny outputs resize appropriately. In general, the window\n * resizing is throttled and the output update occurs when the transition\n * is complete.\n * 2. If an output with a resize method on the output binding is detected, we\n * directly call the `.onResize()` method of the binding. This ensures that\n * htmlwidgets transition smoothly. In static mode, htmlwidgets does this\n * already.\n *\n * @note\n * This resize observer also handles race conditions in some complex\n * fill-based layouts with multiple outputs (e.g., plotly), where shiny\n * initializes with the correct sizing, but in-between the 1st and last\n * renderValue(), the size of the output containers can change, meaning every\n * output but the 1st gets initialized with the wrong size during their\n * renderValue(). Then, after the render phase, shiny won't know to trigger a\n * resize since all the widgets will return to their original size (and thus,\n * Shiny thinks there isn't any resizing to do). The resize observer works\n * around this by ensuring that the output is resized whenever its container\n * size changes.\n * @constructor\n */\n constructor() {\n this.resizeObserverEntries = [];\n this.resizeObserver = new ResizeObserver((entries) => {\n const resizeEvent = new Event(\"resize\");\n window.dispatchEvent(resizeEvent);\n\n // the rest of this callback is only relevant in Shiny apps\n if (!window.Shiny) return;\n\n const resized = [] as HTMLElement[];\n\n for (const entry of entries) {\n if (!(entry.target instanceof HTMLElement)) continue;\n if (!entry.target.querySelector(\".shiny-bound-output\")) continue;\n\n entry.target\n .querySelectorAll(\".shiny-bound-output\")\n .forEach((el) => {\n if (resized.includes(el)) return;\n\n const { binding, onResize } = $(el).data(\"shinyOutputBinding\");\n if (!binding || !binding.resize) return;\n\n // if this output is owned by another observer, skip it\n const owner = (el as any).shinyResizeObserver;\n if (owner && owner !== this) return;\n // mark this output as owned by this shinyResizeObserver instance\n if (!owner) (el as any).shinyResizeObserver = this;\n\n // trigger immediate resizing of outputs with a resize method\n onResize(el);\n // only once per output and resize event\n resized.push(el);\n\n // set plot images to 100% width temporarily during the transition\n if (!el.classList.contains(\"shiny-plot-output\")) return;\n const img = el.querySelector(\n 'img:not([width=\"100%\"])'\n );\n if (img) img.setAttribute(\"width\", \"100%\");\n });\n }\n });\n }\n\n /**\n * Observe an element for size changes.\n * @param {HTMLElement} el - The element to observe.\n */\n observe(el: HTMLElement): void {\n this.resizeObserver.observe(el);\n this.resizeObserverEntries.push(el);\n }\n\n /**\n * Stop observing an element for size changes.\n * @param {HTMLElement} el - The element to stop observing.\n */\n unobserve(el: HTMLElement): void {\n const idxEl = this.resizeObserverEntries.indexOf(el);\n if (idxEl < 0) return;\n\n this.resizeObserver.unobserve(el);\n this.resizeObserverEntries.splice(idxEl, 1);\n }\n\n /**\n * This method checks that we're not continuing to watch elements that no\n * longer exist in the DOM. If any are found, we stop observing them and\n * remove them from our array of observed elements.\n *\n * @private\n * @static\n */\n flush(): void {\n this.resizeObserverEntries.forEach((el) => {\n if (!document.body.contains(el)) this.unobserve(el);\n });\n }\n}\n\nexport { ShinyResizeObserver };\n", "type Callback = (el: T) => void;\n\n/**\n * Watch for the removal of specific elements from regions of the page.\n */\nexport class ShinyRemovedObserver {\n private observer: MutationObserver;\n private watching: Set;\n\n /**\n * Creates a new instance of the `ShinyRemovedObserver` class to watch for the\n * removal of specific elements from part of the DOM.\n *\n * @param selector A CSS selector to identify elements to watch for removal.\n * @param callback The function to be called on a matching element when it\n * is removed.\n */\n constructor(selector: string, callback: Callback) {\n this.watching = new Set();\n this.observer = new MutationObserver((mutations) => {\n const found = new Set();\n for (const { type, removedNodes } of mutations) {\n if (type !== \"childList\") continue;\n if (removedNodes.length === 0) continue;\n\n for (const node of removedNodes) {\n if (!(node instanceof HTMLElement)) continue;\n if (node.matches(selector)) {\n found.add(node);\n }\n if (node.querySelector(selector)) {\n node\n .querySelectorAll(selector)\n .forEach((el) => found.add(el));\n }\n }\n }\n if (found.size === 0) return;\n for (const el of found) {\n try {\n callback(el);\n } catch (e) {\n console.error(e);\n }\n }\n });\n }\n\n /**\n * Starts observing the specified element for removal of its children. If the\n * element is already being observed, no change is made to the mutation\n * observer.\n * @param el The element to observe.\n */\n observe(el: HTMLElement): void {\n const changed = this._flush();\n if (this.watching.has(el)) {\n if (!changed) return;\n } else {\n this.watching.add(el);\n }\n\n if (changed) {\n this._restartObserver();\n } else {\n this.observer.observe(el, { childList: true, subtree: true });\n }\n }\n\n /**\n * Stops observing the specified element for removal.\n * @param el The element to unobserve.\n */\n unobserve(el: HTMLElement): void {\n if (!this.watching.has(el)) return;\n // MutationObserver doesn't have an \"unobserve\" method, so we have to\n // disconnect and re-observe all elements that are still being watched.\n this.watching.delete(el);\n this._flush();\n this._restartObserver();\n }\n\n /**\n * Restarts the mutation observer, observing all elements in the `watching`\n * and implicitly unobserving any elements that are no longer in the\n * watchlist.\n * @private\n */\n private _restartObserver(): void {\n this.observer.disconnect();\n for (const el of this.watching) {\n this.observer.observe(el, { childList: true, subtree: true });\n }\n }\n\n /**\n * Flushes the set of watched elements, removing any elements that are no\n * longer in the DOM, but it does not modify the mutation observer.\n * @private\n * @returns A boolean indicating whether the watched elements have changed.\n */\n private _flush(): boolean {\n let watchedChanged = false;\n const watched = Array.from(this.watching);\n for (const el of watched) {\n if (document.body.contains(el)) continue;\n this.watching.delete(el);\n watchedChanged = true;\n }\n return watchedChanged;\n }\n}\n", "import { getAllFocusableChildren, registerBslibGlobal, Shiny } from \"./_utils\";\nimport { ShinyResizeObserver } from \"./_shinyResizeObserver\";\nimport { ShinyRemovedObserver } from \"./_shinyRemovedObserver\";\n\n/**\n * The overlay element that is placed behind the card when expanded full screen.\n *\n * @interface CardFullScreenOverlay\n * @typedef {CardFullScreenOverlay}\n */\ninterface CardFullScreenOverlay {\n /**\n * The full screen overlay container.\n * @type {HTMLDivElement}\n */\n container: HTMLDivElement;\n /**\n * The anchor element used to close the full screen overlay.\n * @type {HTMLAnchorElement}\n */\n anchor: HTMLAnchorElement;\n}\n\n/**\n * The bslib card component class.\n *\n * @class Card\n * @typedef {Card}\n */\nclass Card {\n /**\n * The card container element.\n * @private\n * @type {HTMLElement}\n */\n private card: HTMLElement;\n /**\n * The card's full screen overlay element. We create this element once and add\n * and remove it from the DOM as needed (this simplifies focus management\n * while in full screen mode).\n * @private\n * @type {CardFullScreenOverlay}\n */\n private overlay: CardFullScreenOverlay;\n\n /**\n * Key bslib-specific classes and attributes used by the card component.\n * @private\n * @static\n */\n private static attr = {\n // eslint-disable-next-line @typescript-eslint/naming-convention\n ATTR_INIT: \"data-bslib-card-init\",\n // eslint-disable-next-line @typescript-eslint/naming-convention\n CLASS_CARD: \"bslib-card\",\n // eslint-disable-next-line @typescript-eslint/naming-convention\n ATTR_FULL_SCREEN: \"data-full-screen\",\n // eslint-disable-next-line @typescript-eslint/naming-convention\n CLASS_HAS_FULL_SCREEN: \"bslib-has-full-screen\",\n // eslint-disable-next-line @typescript-eslint/naming-convention\n CLASS_FULL_SCREEN_ENTER: \"bslib-full-screen-enter\",\n // eslint-disable-next-line @typescript-eslint/naming-convention\n CLASS_FULL_SCREEN_EXIT: \"bslib-full-screen-exit\",\n // eslint-disable-next-line @typescript-eslint/naming-convention\n ID_FULL_SCREEN_OVERLAY: \"bslib-full-screen-overlay\",\n // eslint-disable-next-line @typescript-eslint/naming-convention\n CLASS_SHINY_INPUT: \"bslib-card-input\",\n };\n\n /**\n * A Shiny-specific resize observer that ensures Shiny outputs in within the\n * card resize appropriately.\n * @private\n * @type {ShinyResizeObserver}\n * @static\n */\n private static shinyResizeObserver = new ShinyResizeObserver();\n\n /**\n * Watch card parent containers for removal and exit full screen mode if a\n * full screen card is removed from the DOM.\n *\n * @private\n * @type {ShinyRemovedObserver}\n * @static\n */\n private static cardRemovedObserver = new ShinyRemovedObserver(\n `.${Card.attr.CLASS_CARD}`,\n (el) => {\n const card = Card.getInstance(el);\n if (!card) return;\n if (card.card.getAttribute(Card.attr.ATTR_FULL_SCREEN) === \"true\") {\n card.exitFullScreen();\n }\n }\n );\n\n /**\n * Creates an instance of a bslib Card component.\n *\n * @constructor\n * @param {HTMLElement} card\n */\n constructor(card: HTMLElement) {\n // remove initialization attribute and script\n card.removeAttribute(Card.attr.ATTR_INIT);\n card\n .querySelector(`script[${Card.attr.ATTR_INIT}]`)\n ?.remove();\n\n this.card = card;\n Card.instanceMap.set(card, this);\n\n // Let Shiny know to trigger resize when the card size changes\n // TODO: shiny could/should do this itself (rstudio/shiny#3682)\n Card.shinyResizeObserver.observe(this.card);\n Card.cardRemovedObserver.observe(document.body);\n\n this._addEventListeners();\n this.overlay = this._createOverlay();\n this._setShinyInput();\n\n // bind event handler methods to this card instance\n this._exitFullScreenOnEscape = this._exitFullScreenOnEscape.bind(this);\n this._trapFocusExit = this._trapFocusExit.bind(this);\n }\n\n /**\n * Enter the card's full screen mode, either programmatically or via an event\n * handler. Full screen mode is activated by adding a class to the card that\n * positions it absolutely and expands it to fill the viewport. In addition,\n * we add a full screen overlay element behind the card and we trap focus in\n * the expanded card while in full screen mode.\n *\n * @param {?Event} [event]\n */\n enterFullScreen(event?: Event): void {\n if (event) event.preventDefault();\n\n // Update close anchor to control current expanded card\n if (this.card.id) {\n this.overlay.anchor.setAttribute(\"aria-controls\", this.card.id);\n }\n\n document.addEventListener(\"keydown\", this._exitFullScreenOnEscape, false);\n\n // trap focus in the fullscreen container, listening for Tab key on the\n // capture phase so we have the best chance of preventing other handlers\n document.addEventListener(\"keydown\", this._trapFocusExit, true);\n\n this.card.setAttribute(Card.attr.ATTR_FULL_SCREEN, \"true\");\n document.body.classList.add(Card.attr.CLASS_HAS_FULL_SCREEN);\n this.card.insertAdjacentElement(\"beforebegin\", this.overlay.container);\n\n // Set initial focus on the card, if not already\n if (\n !this.card.contains(document.activeElement) ||\n document.activeElement?.classList.contains(\n Card.attr.CLASS_FULL_SCREEN_ENTER\n )\n ) {\n this.card.setAttribute(\"tabindex\", \"-1\");\n this.card.focus();\n }\n\n this._emitFullScreenEvent(true);\n this._setShinyInput();\n }\n\n /**\n * Exit full screen mode. This removes the full screen overlay element,\n * removes the full screen class from the card, and removes the keyboard event\n * listeners that were added when entering full screen mode.\n */\n exitFullScreen(): void {\n document.removeEventListener(\n \"keydown\",\n this._exitFullScreenOnEscape,\n false\n );\n document.removeEventListener(\"keydown\", this._trapFocusExit, true);\n\n // Remove overlay and remove full screen classes from card\n this.overlay.container.remove();\n this.card.setAttribute(Card.attr.ATTR_FULL_SCREEN, \"false\");\n this.card.removeAttribute(\"tabindex\");\n document.body.classList.remove(Card.attr.CLASS_HAS_FULL_SCREEN);\n\n this._emitFullScreenEvent(false);\n this._setShinyInput();\n }\n\n private _setShinyInput(): void {\n if (!this.card.classList.contains(Card.attr.CLASS_SHINY_INPUT)) return;\n if (!Shiny) return;\n if (!Shiny.setInputValue) {\n // Shiny isn't ready yet, so we'll try to set the input value again later,\n // (but it might not be ready then either, so we'll keep trying).\n setTimeout(() => this._setShinyInput(), 0);\n return;\n }\n const fsAttr = this.card.getAttribute(Card.attr.ATTR_FULL_SCREEN);\n Shiny.setInputValue(this.card.id + \"_full_screen\", fsAttr === \"true\");\n }\n\n /**\n * Emits a custom event to communicate the card's full screen state change.\n * @private\n * @param {boolean} fullScreen\n */\n private _emitFullScreenEvent(fullScreen: boolean): void {\n const event = new CustomEvent(\"bslib.card\", {\n bubbles: true,\n detail: { fullScreen },\n });\n this.card.dispatchEvent(event);\n }\n\n /**\n * Adds general card-specific event listeners.\n * @private\n */\n private _addEventListeners(): void {\n const btnFullScreen = this.card.querySelector(\n `:scope > * > .${Card.attr.CLASS_FULL_SCREEN_ENTER}`\n );\n if (!btnFullScreen) return;\n btnFullScreen.addEventListener(\"click\", (ev) => this.enterFullScreen(ev));\n }\n\n /**\n * An event handler to exit full screen mode when the Escape key is pressed.\n * @private\n * @param {KeyboardEvent} event\n */\n private _exitFullScreenOnEscape(event: KeyboardEvent): void {\n if (!(event.target instanceof HTMLElement)) return;\n // If the user is in the middle of a select input choice, don't exit\n const selOpenSelectInput = [\"select[open]\", \"input[aria-expanded='true']\"];\n if (event.target.matches(selOpenSelectInput.join(\", \"))) return;\n\n if (event.key === \"Escape\") {\n this.exitFullScreen();\n }\n }\n\n /**\n * An event handler to trap focus within the card when in full screen mode.\n *\n * @description\n * This keyboard event handler ensures that tab focus stays within the card\n * when in full screen mode. When the card is first expanded,\n * we move focus to the card element itself. If focus somehow leaves the card,\n * we returns focus to the card container.\n *\n * Within the card, we handle only tabbing from the close anchor or the last\n * focusable element and only when tab focus would have otherwise left the\n * card. In those cases, we cycle focus to the last focusable element or back\n * to the anchor. If the card doesn't have any focusable elements, we move\n * focus to the close anchor.\n *\n * @note\n * Because the card contents may change, we check for focusable elements\n * every time the handler is called.\n *\n * @private\n * @param {KeyboardEvent} event\n */\n private _trapFocusExit(event: KeyboardEvent): void {\n if (!(event instanceof KeyboardEvent)) return;\n if (event.key !== \"Tab\") return;\n\n const isFocusedContainer = event.target === this.card;\n const isFocusedAnchor = event.target === this.overlay.anchor;\n const isFocusedWithin = this.card.contains(event.target as Node);\n\n const stopEvent = () => {\n event.preventDefault();\n event.stopImmediatePropagation();\n };\n\n if (!(isFocusedWithin || isFocusedContainer || isFocusedAnchor)) {\n // If focus is outside the card, return to the card\n stopEvent();\n this.card.focus();\n return;\n }\n\n // Check focusables every time because the card contents may have changed\n // but exclude the full screen enter button from this list of elements\n const focusableElements = getAllFocusableChildren(this.card).filter(\n (el) => !el.classList.contains(Card.attr.CLASS_FULL_SCREEN_ENTER)\n );\n const hasFocusableElements = focusableElements.length > 0;\n\n // We need to handle five cases:\n // 1. The card has no focusable elements --> focus the anchor\n // 2. Focus is on the card container (do nothing, natural tab order)\n // 3. Focus is on the anchor and the user pressed Tab + Shift (backwards)\n // -> Move to the last focusable element (end of card)\n // 4. Focus is on the last focusable element and the user pressed Tab\n // (forwards) -> Move to the anchor (top of card)\n // 5. otherwise we don't interfere\n\n if (!hasFocusableElements) {\n // case 1\n stopEvent();\n this.overlay.anchor.focus();\n return;\n }\n\n // case 2\n if (isFocusedContainer) return;\n\n const lastFocusable = focusableElements[focusableElements.length - 1];\n const isFocusedLast = event.target === lastFocusable;\n\n if (isFocusedAnchor && event.shiftKey) {\n stopEvent();\n lastFocusable.focus();\n return;\n }\n\n if (isFocusedLast && !event.shiftKey) {\n stopEvent();\n this.overlay.anchor.focus();\n return;\n }\n }\n\n /**\n * Creates the full screen overlay.\n * @private\n * @returns {CardFullScreenOverlay}\n */\n private _createOverlay(): CardFullScreenOverlay {\n const container = document.createElement(\"div\");\n container.id = Card.attr.ID_FULL_SCREEN_OVERLAY;\n container.onclick = this.exitFullScreen.bind(this);\n\n const anchor = this._createOverlayCloseAnchor();\n container.appendChild(anchor);\n\n return { container, anchor };\n }\n\n /**\n * Creates the anchor element used to exit the full screen mode.\n * @private\n * @returns {CardFullScreenOverlay[\"anchor\"]}\n */\n private _createOverlayCloseAnchor(): CardFullScreenOverlay[\"anchor\"] {\n const anchor = document.createElement(\"a\");\n anchor.classList.add(Card.attr.CLASS_FULL_SCREEN_EXIT);\n anchor.tabIndex = 0;\n anchor.setAttribute(\"aria-expanded\", \"true\");\n anchor.setAttribute(\"aria-label\", \"Close card\");\n anchor.setAttribute(\"role\", \"button\");\n anchor.onclick = (ev) => {\n this.exitFullScreen();\n ev.stopPropagation();\n };\n anchor.onkeydown = (ev) => {\n if (ev.key === \"Enter\" || ev.key === \" \") {\n this.exitFullScreen();\n }\n };\n anchor.innerHTML = this._overlayCloseHtml();\n\n return anchor;\n }\n\n /**\n * Returns the HTML for the close icon.\n * @private\n * @returns {string}\n */\n private _overlayCloseHtml(): string {\n return (\n \"Close \" +\n \"\" +\n \"\"\n );\n }\n\n /**\n * The registry of card instances and their associated DOM elements.\n * @private\n * @static\n * @type {WeakMap}\n */\n private static instanceMap: WeakMap = new WeakMap();\n\n /**\n * Returns the card instance associated with the given element, if any.\n * @public\n * @static\n * @param {HTMLElement} el\n * @returns {(Card | undefined)}\n */\n public static getInstance(el: HTMLElement): Card | undefined {\n return Card.instanceMap.get(el);\n }\n\n /**\n * If cards are initialized before the DOM is ready, we re-schedule the\n * initialization to occur on DOMContentLoaded.\n * @private\n * @static\n * @type {boolean}\n */\n private static onReadyScheduled = false;\n\n /**\n * Initializes all cards that require initialization on the page, or schedules\n * initialization if the DOM is not yet ready.\n * @public\n * @static\n * @param {boolean} [flushResizeObserver=true]\n */\n public static initializeAllCards(flushResizeObserver = true): void {\n if (document.readyState === \"loading\") {\n if (!Card.onReadyScheduled) {\n Card.onReadyScheduled = true;\n document.addEventListener(\"DOMContentLoaded\", () => {\n Card.initializeAllCards(false);\n });\n }\n return;\n }\n\n if (flushResizeObserver) {\n // Trigger a recheck of observed cards to unobserve non-existent cards\n Card.shinyResizeObserver.flush();\n }\n\n const initSelector = `.${Card.attr.CLASS_CARD}[${Card.attr.ATTR_INIT}]`;\n if (!document.querySelector(initSelector)) {\n // no cards to initialize\n return;\n }\n\n const cards = document.querySelectorAll(initSelector);\n cards.forEach((card) => new Card(card as HTMLElement));\n }\n}\n\n// attach Sidebar class to window for global usage\nregisterBslibGlobal(\"Card\", Card);\n\nexport { Card };\n", "import { InputBinding, registerBinding, registerBslibGlobal } from \"./_utils\";\nimport { ShinyResizeObserver } from \"./_shinyResizeObserver\";\n\n/**\n * Methods for programmatically toggling the state of the sidebar. These methods\n * describe the desired state of the sidebar: `\"close\"` and `\"open\"` transition\n * the sidebar to the desired state, unless the sidebar is already in that\n * state. `\"toggle\"` transitions the sidebar to the state opposite of its\n * current state.\n * @typedef {SidebarToggleMethod}\n */\ntype SidebarToggleMethod = \"close\" | \"closed\" | \"open\" | \"toggle\";\n\n/**\n * Data received by the input binding's `receiveMessage` method.\n * @typedef {SidebarMessageData}\n */\ntype SidebarMessageData = {\n method: SidebarToggleMethod;\n};\n\n/**\n * Represents the size of the sidebar window either: \"desktop\" or \"mobile\".\n */\ntype SidebarWindowSize = \"desktop\" | \"mobile\";\n\n/**\n * The DOM elements that make up the sidebar. `main`, `sidebar`, and `toggle`\n * are all direct children of `container` (in that order).\n * @interface SidebarComponents\n * @typedef {SidebarComponents}\n */\ninterface SidebarComponents {\n /**\n * The `layout_sidebar()` parent container, with class\n * `Sidebar.classes.LAYOUT`.\n * @type {HTMLElement}\n */\n container: HTMLElement;\n /**\n * The main content area of the sidebar layout.\n * @type {HTMLElement}\n */\n main: HTMLElement;\n /**\n * The sidebar container of the sidebar layout.\n * @type {HTMLElement}\n */\n sidebar: HTMLElement;\n /**\n * The toggle button that is used to toggle the sidebar state.\n * @type {HTMLElement}\n */\n toggle: HTMLElement;\n /**\n * The resize handle for resizing the sidebar (optional).\n * @type {HTMLElement | null}\n */\n resizeHandle?: HTMLElement | null;\n}\n\n/**\n * The bslib sidebar component class. This class is only used for collapsible\n * sidebars.\n *\n * @class Sidebar\n * @typedef {Sidebar}\n */\nclass Sidebar {\n /**\n * The DOM elements that make up the sidebar, see `SidebarComponents`.\n * @private\n * @type {SidebarComponents}\n */\n private layout: SidebarComponents;\n\n /**\n * A Shiny-specific resize observer that ensures Shiny outputs in the main\n * content areas of the sidebar resize appropriately.\n * @private\n * @type {ShinyResizeObserver}\n * @static\n */\n private static shinyResizeObserver = new ShinyResizeObserver();\n\n /**\n * Resize state tracking\n * @private\n */\n private resizeState = {\n isResizing: false,\n startX: 0,\n startWidth: 0,\n minWidth: 150,\n maxWidth: () => window.innerWidth - 50,\n constrainedWidth: (width: number): number => {\n return Math.max(\n this.resizeState.minWidth,\n Math.min(this.resizeState.maxWidth(), width)\n );\n },\n };\n\n /**\n * Creates an instance of a collapsible bslib Sidebar.\n * @constructor\n * @param {HTMLElement} container\n */\n constructor(container: HTMLElement) {\n Sidebar.instanceMap.set(container, this);\n this.layout = {\n container,\n main: container.querySelector(\":scope > .main\") as HTMLElement,\n sidebar: container.querySelector(\":scope > .sidebar\") as HTMLElement,\n toggle: container.querySelector(\n \":scope > .collapse-toggle\"\n ) as HTMLElement,\n } as SidebarComponents;\n\n const sideAccordion = this.layout.sidebar.querySelector(\n \":scope > .sidebar-content > .accordion\"\n );\n if (sideAccordion) {\n // Add `.has-accordion` class to `.sidebar-content` container\n sideAccordion?.parentElement?.classList.add(\"has-accordion\");\n sideAccordion.classList.add(\"accordion-flush\");\n }\n\n this._initSidebarCounters();\n this._initSidebarState();\n\n if (this._isCollapsible(\"desktop\") || this._isCollapsible(\"mobile\")) {\n this._initEventListeners();\n }\n\n // Initialize resize functionality\n this._initResizeHandle();\n\n // Start watching the main content area for size changes to ensure Shiny\n // outputs resize appropriately during sidebar transitions.\n Sidebar.shinyResizeObserver.observe(this.layout.main);\n\n container.removeAttribute(\"data-bslib-sidebar-init\");\n const initScript = container.querySelector(\n \":scope > script[data-bslib-sidebar-init]\"\n );\n if (initScript) {\n container.removeChild(initScript);\n }\n }\n\n /**\n * Read the current state of the sidebar. Note that, when calling this method,\n * the sidebar may be transitioning into the state returned by this method.\n *\n * @description\n * The sidebar state works as follows, starting from the open state. When the\n * sidebar is closed:\n * 1. We add both the `COLLAPSE` and `TRANSITIONING` classes to the sidebar.\n * 2. The sidebar collapse begins to animate. In general, where it is\n * supported, we transition the `grid-template-columns` property of the\n * sidebar layout. We also rotate the collapse icon and we use this\n * rotation to determine when the transition is complete.\n * 3. If another sidebar state toggle is requested while closing the sidebar,\n * we remove the `COLLAPSE` class and the animation immediately starts to\n * reverse.\n * 4. When the `transition` is complete, we remove the `TRANSITIONING` class.\n * @readonly\n * @type {boolean}\n */\n get isClosed(): boolean {\n return this.layout.container.classList.contains(Sidebar.classes.COLLAPSE);\n }\n\n /**\n * Static classes related to the sidebar layout or state.\n * @public\n * @static\n * @readonly\n * @type {{ LAYOUT: string; COLLAPSE: string; TRANSITIONING: string; }}\n */\n public static readonly classes = {\n // eslint-disable-next-line @typescript-eslint/naming-convention\n LAYOUT: \"bslib-sidebar-layout\",\n // eslint-disable-next-line @typescript-eslint/naming-convention\n COLLAPSE: \"sidebar-collapsed\",\n // eslint-disable-next-line @typescript-eslint/naming-convention\n TRANSITIONING: \"transitioning\",\n // eslint-disable-next-line @typescript-eslint/naming-convention\n RESIZE_HANDLE: \"bslib-sidebar-resize-handle\",\n // eslint-disable-next-line @typescript-eslint/naming-convention\n RESIZING: \"sidebar-resizing\",\n };\n\n /**\n * If sidebars are initialized before the DOM is ready, we re-schedule the\n * initialization to occur on DOMContentLoaded.\n * @private\n * @static\n * @type {boolean}\n */\n private static onReadyScheduled = false;\n /**\n * A map of initialized sidebars to their respective Sidebar instances.\n * @private\n * @static\n * @type {WeakMap}\n */\n private static instanceMap: WeakMap = new WeakMap();\n\n /**\n * Given a sidebar container, return the Sidebar instance associated with it.\n * @public\n * @static\n * @param {HTMLElement} el\n * @returns {(Sidebar | undefined)}\n */\n public static getInstance(el: HTMLElement): Sidebar | undefined {\n return Sidebar.instanceMap.get(el);\n }\n\n /**\n * Determine whether the sidebar is collapsible at a given screen size.\n * @private\n * @param {SidebarWindowSize} [size=\"desktop\"]\n * @returns {boolean}\n */\n private _isCollapsible(size: SidebarWindowSize = \"desktop\"): boolean {\n const { container } = this.layout;\n\n const attr =\n size === \"desktop\" ? \"collapsibleDesktop\" : \"collapsibleMobile\";\n\n const isCollapsible = container.dataset[attr];\n\n if (isCollapsible === undefined) {\n return true;\n }\n\n return isCollapsible.trim().toLowerCase() !== \"false\";\n }\n\n /**\n * Initialize all collapsible sidebars on the page.\n * @public\n * @static\n * @param {boolean} [flushResizeObserver=true] When `true`, we remove\n * non-existent elements from the ResizeObserver. This is required\n * periodically to prevent memory leaks. To avoid over-checking, we only flush\n * the ResizeObserver when initializing sidebars after page load.\n */\n public static initCollapsibleAll(flushResizeObserver = true): void {\n if (document.readyState === \"loading\") {\n if (!Sidebar.onReadyScheduled) {\n Sidebar.onReadyScheduled = true;\n document.addEventListener(\"DOMContentLoaded\", () => {\n Sidebar.initCollapsibleAll(false);\n });\n }\n return;\n }\n\n const initSelector = `.${Sidebar.classes.LAYOUT}[data-bslib-sidebar-init]`;\n if (!document.querySelector(initSelector)) {\n // no sidebars to initialize\n return;\n }\n\n if (flushResizeObserver) Sidebar.shinyResizeObserver.flush();\n\n const containers = document.querySelectorAll(initSelector);\n containers.forEach((container) => new Sidebar(container as HTMLElement));\n }\n\n /**\n * Initialize sidebar resize functionality.\n * @private\n */\n private _initResizeHandle(): void {\n if (!this.layout.resizeHandle) {\n const handle = this._createResizeHandle();\n // Insert handle into the layout container\n this.layout.container.appendChild(handle);\n this.layout.resizeHandle = handle;\n\n this._attachResizeEventListeners(handle);\n }\n this._updateResizeAvailability();\n }\n\n /**\n * Create the resize handle element.\n * @private\n */\n private _createResizeHandle(): HTMLDivElement {\n const handle = document.createElement(\"div\");\n handle.className = Sidebar.classes.RESIZE_HANDLE;\n handle.setAttribute(\"role\", \"separator\");\n handle.setAttribute(\"aria-orientation\", \"vertical\");\n handle.setAttribute(\"aria-label\", \"Resize sidebar\");\n handle.setAttribute(\"tabindex\", \"0\");\n handle.setAttribute(\"aria-keyshortcuts\", \"ArrowLeft ArrowRight Home End\");\n handle.title = \"Drag to resize sidebar\";\n\n const indicator = document.createElement(\"div\");\n indicator.className = \"resize-indicator\";\n handle.appendChild(indicator);\n\n const instructions = document.createElement(\"div\");\n instructions.className = \"visually-hidden\";\n instructions.textContent =\n \"Use arrow keys to resize the sidebar, Shift for larger steps, Home/End for min/max width.\";\n handle.appendChild(instructions);\n\n return handle;\n }\n\n /**\n * Attach event listeners for resize functionality.\n * @private\n */\n private _attachResizeEventListeners(handle: HTMLDivElement): void {\n // Mouse events\n handle.addEventListener(\"mousedown\", this._onResizeStart.bind(this));\n document.addEventListener(\"mousemove\", this._onResizeMove.bind(this));\n document.addEventListener(\"mouseup\", this._onResizeEnd.bind(this));\n\n // Touch events for mobile devices\n handle.addEventListener(\"touchstart\", this._onResizeStart.bind(this), {\n passive: false,\n });\n document.addEventListener(\"touchmove\", this._onResizeMove.bind(this), {\n passive: false,\n });\n document.addEventListener(\"touchend\", this._onResizeEnd.bind(this));\n\n // Keyboard events for accessibility\n handle.addEventListener(\"keydown\", this._onResizeKeyDown.bind(this));\n\n window.addEventListener(\n \"resize\",\n whenChangedCallback(\n () => this._getWindowSize(),\n () => this._updateResizeAvailability()\n )\n );\n }\n\n /**\n * Check if the sidebar should be resizable in the current state.\n * @private\n * @returns {boolean}\n */\n private _shouldEnableResize(): boolean {\n const isDesktop = this._getWindowSize() === \"desktop\";\n const notTransitioning = !this.layout.container.classList.contains(\n Sidebar.classes.TRANSITIONING\n );\n const notClosed = !this.isClosed;\n\n return (\n // Allow resizing only when the sidebar...\n isDesktop && notTransitioning && notClosed\n );\n }\n\n /**\n * Handle resize start (mouse/touch down).\n * @private\n * @param {MouseEvent | TouchEvent} event\n */\n private _onResizeStart(event: MouseEvent | TouchEvent): void {\n if (!this._shouldEnableResize()) return;\n\n event.preventDefault();\n\n const clientX =\n \"touches\" in event ? event.touches[0].clientX : event.clientX;\n\n this.resizeState.isResizing = true;\n this.resizeState.startX = clientX;\n this.resizeState.startWidth = this._getCurrentSidebarWidth();\n\n // Disable transitions during resize for smooth interaction\n this.layout.container.style.setProperty(\"--_transition-duration\", \"0ms\");\n this.layout.container.classList.add(Sidebar.classes.RESIZING);\n\n document.documentElement.setAttribute(\n `data-bslib-${Sidebar.classes.RESIZING}`,\n \"true\"\n );\n\n this._dispatchResizeEvent(\"start\", this.resizeState.startWidth);\n }\n\n /**\n * Handle resize move (mouse/touch move).\n * @private\n * @param {MouseEvent | TouchEvent} event\n */\n private _onResizeMove(event: MouseEvent | TouchEvent): void {\n if (!this.resizeState.isResizing) return;\n\n event.preventDefault();\n\n const clientX =\n \"touches\" in event ? event.touches[0].clientX : event.clientX;\n const deltaX = clientX - this.resizeState.startX;\n\n // Calculate new width based on sidebar position\n const isRight = this._isRightSidebar();\n const newWidth = isRight\n ? this.resizeState.startWidth - deltaX\n : this.resizeState.startWidth + deltaX;\n\n // Constrain within bounds\n const constrainedWidth = this.resizeState.constrainedWidth(newWidth);\n\n this._updateSidebarWidth(constrainedWidth);\n this._dispatchResizeEvent(\"move\", constrainedWidth);\n }\n\n /**\n * Handle resize end (mouse/touch up).\n * @private\n */\n private _onResizeEnd(): void {\n if (!this.resizeState.isResizing) return;\n\n this.resizeState.isResizing = false;\n\n // Re-enable transitions\n this.layout.container.style.removeProperty(\"--_transition-duration\");\n this.layout.container.classList.remove(Sidebar.classes.RESIZING);\n\n // Reset cursor and text selection resizing changes\n document.documentElement.removeAttribute(\n `data-bslib-${Sidebar.classes.RESIZING}`\n );\n\n // Dispatch resize end event\n Sidebar.shinyResizeObserver.flush();\n this._dispatchResizeEvent(\"end\", this._getCurrentSidebarWidth());\n }\n\n /**\n * Handle keyboard events for resize accessibility.\n * @private\n * @param {KeyboardEvent} event\n */\n private _onResizeKeyDown(event: KeyboardEvent): void {\n if (!this._shouldEnableResize()) return;\n\n const step = event.shiftKey ? 50 : 10; // Larger steps with Shift\n let newWidth = this._getCurrentSidebarWidth();\n\n switch (event.key) {\n case \"ArrowLeft\":\n newWidth = this._isRightSidebar() ? newWidth + step : newWidth - step;\n break;\n case \"ArrowRight\":\n newWidth = this._isRightSidebar() ? newWidth - step : newWidth + step;\n break;\n case \"Home\":\n newWidth = this.resizeState.minWidth;\n break;\n case \"End\":\n newWidth = this.resizeState.maxWidth();\n break;\n default:\n return; // Don't prevent default for other keys\n }\n\n event.preventDefault();\n\n // Constrain within bounds\n newWidth = this.resizeState.constrainedWidth(newWidth);\n\n this._updateSidebarWidth(newWidth);\n Sidebar.shinyResizeObserver.flush();\n this._dispatchResizeEvent(\"keyboard\", newWidth);\n }\n\n /**\n * Get the current sidebar width in pixels.\n * @private\n * @returns {number}\n */\n private _getCurrentSidebarWidth(): number {\n const sidebarWidth = this.layout.sidebar.getBoundingClientRect().width;\n return sidebarWidth || 250;\n }\n\n /**\n * Update the sidebar width.\n * @private\n * @param {number} newWidth\n */\n private _updateSidebarWidth(newWidth: number): void {\n const { container, resizeHandle } = this.layout;\n\n container.style.setProperty(\"--_sidebar-width\", `${newWidth}px`);\n\n // Update min, max and current width attributes on the resize handle\n if (resizeHandle) {\n resizeHandle.setAttribute(\"aria-valuenow\", newWidth.toString());\n resizeHandle.setAttribute(\n \"aria-valuemin\",\n this.resizeState.minWidth.toString()\n );\n resizeHandle.setAttribute(\n \"aria-valuemax\",\n this.resizeState.maxWidth().toString()\n );\n }\n }\n\n /**\n * Check if this is a right-aligned sidebar.\n * @private\n * @returns {boolean}\n */\n private _isRightSidebar(): boolean {\n return this.layout.container.classList.contains(\"sidebar-right\");\n }\n\n /**\n * Update resize handle availability based on current state.\n * @private\n */\n private _updateResizeAvailability(): void {\n if (!this.layout.resizeHandle) return;\n\n const shouldEnable = this._shouldEnableResize();\n\n this.layout.resizeHandle.style.display = shouldEnable ? \"\" : \"none\";\n this.layout.resizeHandle.setAttribute(\n \"aria-hidden\",\n shouldEnable ? \"false\" : \"true\"\n );\n\n if (shouldEnable) {\n this.layout.resizeHandle.setAttribute(\"tabindex\", \"0\");\n } else {\n this.layout.resizeHandle.removeAttribute(\"tabindex\");\n }\n }\n\n /**\n * Dispatch a custom resize event.\n * @private\n * @param {string} phase The phase of the resize event lifecycle, e.g.\n * \"start\", \"move\", \"end\", or \"keyboard\".\n * @param {number} width The new width of the sidebar in pixels.\n */\n private _dispatchResizeEvent(phase: string, width: number): void {\n const event = new CustomEvent(\"bslib.sidebar.resize\", {\n bubbles: true,\n detail: { phase, width, sidebar: this },\n });\n this.layout.sidebar.dispatchEvent(event);\n }\n\n /**\n * Initialize event listeners for the sidebar toggle button.\n * @private\n */\n private _initEventListeners(): void {\n const { toggle } = this.layout;\n\n toggle.addEventListener(\"click\", (ev) => {\n ev.preventDefault();\n this.toggle(\"toggle\");\n });\n\n // Remove the transitioning class when the transition ends. We watch the\n // collapse toggle icon because it's guaranteed to transition, whereas not\n // all browsers support animating grid-template-columns.\n toggle\n .querySelector(\".collapse-icon\")\n ?.addEventListener(\"transitionend\", () => {\n this._finalizeState();\n });\n\n if (this._isCollapsible(\"desktop\") && this._isCollapsible(\"mobile\")) {\n return;\n }\n\n // The sidebar is *sometimes* collapsible, so we need to handle window\n // resize events to ensure visibility and expected behavior.\n window.addEventListener(\n \"resize\",\n whenChangedCallback(\n () => this._getWindowSize(),\n () => this._initSidebarState()\n )\n );\n }\n\n /**\n * Initialize nested sidebar counters.\n *\n * @description\n * This function walks up the DOM tree, adding CSS variables to each direct\n * parent sidebar layout that count the layout's position in the stack of\n * nested layouts. We use these counters to keep the collapse toggles from\n * overlapping. Note that always-open sidebars that don't have collapse\n * toggles break the chain of nesting.\n * @private\n */\n private _initSidebarCounters(): void {\n const { container } = this.layout;\n\n const selectorChildLayouts =\n `.${Sidebar.classes.LAYOUT}` +\n \"> .main > \" +\n `.${Sidebar.classes.LAYOUT}:not([data-bslib-sidebar-open=\"always\"])`;\n\n const isInnermostLayout =\n container.querySelector(selectorChildLayouts) === null;\n\n if (!isInnermostLayout) {\n // There are sidebar layouts nested within this layout; defer to children\n return;\n }\n\n function nextSidebarParent(el: HTMLElement | null): HTMLElement | null {\n el = el ? el.parentElement : null;\n if (el && el.classList.contains(\"main\")) {\n // .bslib-sidebar-layout > .main > .bslib-sidebar-layout\n el = el.parentElement;\n }\n if (el && el.classList.contains(Sidebar.classes.LAYOUT)) {\n return el;\n }\n return null;\n }\n\n const layouts = [container];\n let parent = nextSidebarParent(container);\n\n while (parent) {\n // Add parent to front of layouts array, so we sort outer -> inner\n layouts.unshift(parent);\n parent = nextSidebarParent(parent);\n }\n\n const count = { left: 0, right: 0 };\n layouts.forEach(function (x: HTMLElement): void {\n const isRight = x.classList.contains(\"sidebar-right\");\n const thisCount = isRight ? count.right++ : count.left++;\n x.style.setProperty(\"--_js-toggle-count-this-side\", thisCount.toString());\n x.style.setProperty(\n \"--_js-toggle-count-max-side\",\n Math.max(count.right, count.left).toString()\n );\n });\n }\n\n /**\n * Retrieves the current window size by reading a CSS variable whose value is\n * toggled via media queries.\n * @returns The window size as `\"desktop\"` or `\"mobile\"`, or `\"\"` if not\n * available.\n */\n private _getWindowSize(): SidebarWindowSize | \"\" {\n const { container } = this.layout;\n\n return window\n .getComputedStyle(container)\n .getPropertyValue(\"--bslib-sidebar-js-window-size\")\n .trim() as SidebarWindowSize | \"\";\n }\n\n /**\n * Determine the initial toggle state of the sidebar at a current screen size.\n * It always returns whether we should `\"open\"` or `\"close\"` the sidebar.\n *\n * @private\n * @returns {(\"close\" | \"open\")}\n */\n private _initialToggleState(): \"close\" | \"open\" {\n const { container } = this.layout;\n\n const attr = this.windowSize === \"desktop\" ? \"openDesktop\" : \"openMobile\";\n\n const initState = container.dataset[attr]?.trim()?.toLowerCase();\n\n if (initState === undefined) {\n return \"open\";\n }\n\n if ([\"open\", \"always\"].includes(initState)) {\n return \"open\";\n }\n\n if ([\"close\", \"closed\"].includes(initState)) {\n return \"close\";\n }\n\n return \"open\";\n }\n\n /**\n * Initialize the sidebar's initial state when `open = \"desktop\"`.\n * @private\n */\n private _initSidebarState(): void {\n // Check the CSS variable to find out which mode we're in right now\n this.windowSize = this._getWindowSize();\n\n const initState = this._initialToggleState();\n this.toggle(initState, true);\n }\n\n /**\n * The current window size, either `\"desktop\"` or `\"mobile\"`.\n * @private\n * @type {SidebarWindowSize | \"\"}\n */\n private windowSize: SidebarWindowSize | \"\" = \"\";\n\n /**\n * Toggle the sidebar's open/closed state.\n * @public\n * @param {SidebarToggleMethod | undefined} method Whether to `\"open\"`,\n * `\"close\"` or `\"toggle\"` the sidebar. If `.toggle()` is called without an\n * argument, it will toggle the sidebar's state.\n * @param {boolean} [immediate=false] If `true`, the sidebar state will be\n * set immediately, without a transition. This is primarily used when the\n * sidebar is initialized.\n */\n public toggle(\n method: SidebarToggleMethod | undefined,\n immediate = false\n ): void {\n if (typeof method === \"undefined\") {\n method = \"toggle\";\n } else if (method === \"closed\") {\n method = \"close\";\n }\n\n const { container, sidebar } = this.layout;\n const isClosed = this.isClosed;\n\n if ([\"open\", \"close\", \"toggle\"].indexOf(method) === -1) {\n throw new Error(`Unknown method ${method}`);\n }\n\n if (method === \"toggle\") {\n method = isClosed ? \"open\" : \"close\";\n }\n\n if ((isClosed && method === \"close\") || (!isClosed && method === \"open\")) {\n // nothing to do, sidebar is already in the desired state\n if (immediate) this._finalizeState();\n return;\n }\n\n if (method === \"open\") {\n // unhide sidebar immediately when opening,\n // otherwise the sidebar is hidden on transitionend\n sidebar.hidden = false;\n }\n\n // If not immediate, add the .transitioning class to the sidebar for smooth\n // transitions. This class is removed when the transition ends.\n container.classList.toggle(Sidebar.classes.TRANSITIONING, !immediate);\n container.classList.toggle(Sidebar.classes.COLLAPSE);\n\n if (immediate) {\n // When transitioning, state is finalized on transitionend, otherwise we\n // need to manually and immediately finalize the state.\n this._finalizeState();\n }\n }\n\n /**\n * When the sidebar open/close transition ends, finalize the sidebar's state.\n * @private\n */\n private _finalizeState(): void {\n const { container, sidebar, toggle } = this.layout;\n container.classList.remove(Sidebar.classes.TRANSITIONING);\n sidebar.hidden = this.isClosed;\n toggle.setAttribute(\"aria-expanded\", this.isClosed ? \"false\" : \"true\");\n\n // Update resize handle availability\n this._updateResizeAvailability();\n\n // Send browser-native event with updated sidebar state\n const event = new CustomEvent(\"bslib.sidebar\", {\n bubbles: true,\n detail: { open: !this.isClosed },\n });\n sidebar.dispatchEvent(event);\n\n // Trigger Shiny input and output binding events\n $(sidebar).trigger(\"toggleCollapse.sidebarInputBinding\");\n $(sidebar).trigger(this.isClosed ? \"hidden\" : \"shown\");\n }\n}\n\nfunction whenChangedCallback(\n watchFn: () => unknown,\n callback: () => void\n): () => void {\n let lastValue = watchFn();\n\n return () => {\n const currentValue = watchFn();\n\n if (currentValue !== lastValue) {\n callback();\n }\n\n lastValue = currentValue;\n };\n}\n\n/**\n * A Shiny input binding for a sidebar.\n * @class SidebarInputBinding\n * @typedef {SidebarInputBinding}\n * @extends {InputBinding}\n */\nclass SidebarInputBinding extends InputBinding {\n find(scope: HTMLElement) {\n return $(scope).find(`.${Sidebar.classes.LAYOUT} > .bslib-sidebar-input`);\n }\n\n getValue(el: HTMLElement): boolean {\n const sb = Sidebar.getInstance(el.parentElement as HTMLElement);\n if (!sb) return false;\n return !sb.isClosed;\n }\n\n setValue(el: HTMLElement, value: boolean): void {\n const method = value ? \"open\" : \"close\";\n this.receiveMessage(el, { method });\n }\n\n subscribe(el: HTMLElement, callback: (x: boolean) => void) {\n $(el).on(\n \"toggleCollapse.sidebarInputBinding\",\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n function (event) {\n callback(true);\n }\n );\n }\n\n unsubscribe(el: HTMLElement) {\n $(el).off(\".sidebarInputBinding\");\n }\n\n receiveMessage(el: HTMLElement, data: SidebarMessageData) {\n const sb = Sidebar.getInstance(el.parentElement as HTMLElement);\n if (sb) sb.toggle(data.method);\n }\n}\n\nregisterBinding(SidebarInputBinding, \"sidebar\");\n// attach Sidebar class to window for global usage\nregisterBslibGlobal(\"Sidebar\", Sidebar);\n", "import { InputBinding, registerBinding } from \"./_utils\";\nimport type { BslibSwitchInline } from \"./webcomponents/switch\";\n\ntype TaskButtonMessage = {\n state: string;\n};\n\n/**\n * This is a Shiny input binding for `bslib::input_task_button()`. It is not a\n * web component, though one of its children is . The\n * reason it is not a web component is because it is primarily a button, and I\n * wanted to use the native