From 5cbe8d0a0c0ab21bfe8dd6bda2134d5641b9c64d Mon Sep 17 00:00:00 2001 From: E Nelson Date: Fri, 22 May 2026 17:04:23 -0400 Subject: [PATCH 01/22] docs: add toolbar_badge() design spec (issue #1316) --- .../specs/2026-05-22-toolbar-badge-design.md | 177 ++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 .claude/specs/2026-05-22-toolbar-badge-design.md diff --git a/.claude/specs/2026-05-22-toolbar-badge-design.md b/.claude/specs/2026-05-22-toolbar-badge-design.md new file mode 100644 index 000000000..4a310070a --- /dev/null +++ b/.claude/specs/2026-05-22-toolbar-badge-design.md @@ -0,0 +1,177 @@ +# Design: `toolbar_badge()` (issue #1316) + +**Date:** 2026-05-22 +**Status:** Approved + +## Overview + +Add `toolbar_badge()` and `update_toolbar_badge()` to the bslib toolbar family. Badges are pure display elements for showing status text (and optionally an icon) inside a `toolbar()`. They are server-updatable but do not register as Shiny inputs. + +## Function Signatures + +```r +toolbar_badge( + label, + ..., + id = NULL, + icon = NULL, + show_label = is.null(icon), + tooltip = !show_label, + color = "secondary", + pill = FALSE +) + +update_toolbar_badge( + id, + label = NULL, + icon = NULL, + show_label = NULL, + color = NULL, + pill = NULL, + session = get_current_session() +) +``` + +### Parameter notes + +- `label` — required; display text or tag object. Always present in the DOM for accessibility, hidden visually when `show_label = FALSE`. +- `id` — optional; required only if `update_toolbar_badge()` will be called. +- `icon` — optional decorative icon, wrapped in `aria-hidden` span. +- `show_label` — defaults to `is.null(icon)`: label shown for text-only badges, hidden for icon badges. +- `tooltip` — defaults to `!show_label`. Accepts `TRUE` (use label text), `FALSE` (none), a character string, or a tag object. When an `id` is provided, the tooltip gets `id = "{id}_tooltip"` for `update_tooltip()` compatibility. `update_toolbar_badge()` does not include a `tooltip` argument — tooltip content is updated separately via `update_tooltip()`, consistent with the rest of the toolbar family. +- `color` — Bootstrap contextual color string: `"primary"`, `"secondary"`, `"success"`, `"danger"`, `"warning"`, `"info"`, `"light"`, `"dark"`. Defaults to `"secondary"`. +- `pill` — `TRUE` adds `rounded-pill` for fully-rounded ends. +- `...` — additional HTML attributes passed to the outer ``. + +## HTML Structure + +```html + + + + + + + + + + [label] + + + +... +``` + +- Uses Bootstrap 5's `text-bg-{color}` utility (sets background and foreground for contrast). +- Label span always has a randomly generated ID (`badge-label-XXXX` via `p_randomInt()`); outer span uses `aria-labelledby` pointing to it — matching the `toolbar_input_button` pattern. This avoids screen readers picking up icon `aria-label` text from within an `aria-hidden` element. +- When `show_label = FALSE`, the label span has the `hidden` HTML attribute (not `visually-hidden`); `aria-labelledby` still resolves it for screen readers. +- When `tooltip` is non-`FALSE`, the `` is wrapped in `bslib::tooltip(placement = "bottom")`. +- No `data-shiny-no-bind-input` needed — the element is never wired as a Shiny input. + +## Update Mechanism + +`update_toolbar_badge()` uses `sendCustomMessage()` (same as `toast`): + +```r +session$sendCustomMessage("bslib.update-toolbar-badge", dropNulls(list( + id = id, + label = processDeps(label, session), + icon = processDeps(validateIcon(icon), session), + showLabel = show_label, + color = color, + pill = pill +))) +``` + +The JS handler (`srcts/src/components/toolbarBadge.ts`) registers a custom message handler: + +```ts +Shiny.addCustomMessageHandler("bslib.update-toolbar-badge", (message) => { + const el = document.getElementById(message.id); + if (!el) return; + if (message.label) // update .bslib-toolbar-label innerHTML + if (message.icon) // update .bslib-toolbar-icon innerHTML + if (message.showLabel !== undefined) // toggle visually-hidden on label span + if (message.color) // swap text-bg-* class + if (message.pill !== undefined) // toggle rounded-pill class +}); +``` + +Only fields present in the message are updated; `NULL` values are dropped by `dropNulls()` before sending. + +## SCSS + +A minimal rule added to `inst/components/scss/toolbar.scss`: + +```scss +.bslib-toolbar-badge { + display: inline-flex; + align-items: center; + gap: 0.25em; // em so it scales with badge font size + + .bslib-toolbar-icon { + display: inline-flex; + align-items: center; + } +} +``` + +Bootstrap's `.badge` and `text-bg-{color}` handle all other styling. Existing `.bslib-toolbar > *` rules (`align-self: center`, `margin-bottom: 0`, `font-size: 0.9rem`) apply automatically. + +## Files to Create / Modify + +| File | Change | +|------|--------| +| `R/toolbar.R` | Add `toolbar_badge()` and `update_toolbar_badge()` | +| `srcts/src/components/toolbarBadge.ts` | New file: custom message handler | +| `inst/components/scss/toolbar.scss` | Add `.bslib-toolbar-badge` rule | +| `tests/testthat/test-toolbar.R` | Add tests (see below) | +| `inst/examples-shiny/toolbar/app.R` | Add badge to example app | +| `NAMESPACE` | Export `toolbar_badge`, `update_toolbar_badge` | +| `NEWS.md` | Add entry under current dev version | + +## Testing Plan + +### Snapshot tests (HTML structure) + +- Default: `toolbar_badge("Active")` — label shown, `text-bg-secondary`, no pill +- Icon-only: icon provided, `show_label = FALSE` — label visually hidden, tooltip present +- Icon + label: both shown +- Pill variant: `pill = TRUE` adds `rounded-pill` +- Spot-check color variants (e.g., `"success"`, `"danger"`) +- Badge nested inside `toolbar()` + +### Unit tests (attribute/class assertions) + +- `color` maps to `text-bg-{color}` class +- `pill = TRUE` adds `rounded-pill` +- `show_label = FALSE` adds `visually-hidden` to label span +- `tooltip = TRUE` wraps in tooltip with `id = "{id}_tooltip"` when `id` is provided +- `tooltip = FALSE` skips the wrapper +- Custom tooltip string/tag passes through correctly + +### `update_toolbar_badge()` tests (mock session) + +```r +session <- list( + sendCustomMessage = function(type, message) { + session$last_type <<- type + session$last_message <<- message + } +) +``` + +- Message type is always `"bslib.update-toolbar-badge"` +- `label`, `color`, `pill`, `show_label`, `icon` appear in message when provided +- `NULL` fields are dropped (not sent) via `dropNulls()` +- Invalid `color` value errors +- Blank/whitespace `label` warns (same guard as `update_toolbar_input_button`) +- Updating one field (e.g., only `color`) does not include other fields in the message From 4a86f2631cd3d66002d77b2aac89759e93b0ec2b Mon Sep 17 00:00:00 2001 From: E Nelson Date: Fri, 22 May 2026 17:16:18 -0400 Subject: [PATCH 02/22] docs: add toolbar_badge() implementation plan --- .claude/plans/2026-05-22-toolbar-badge.md | 782 ++++++++++++++++++++++ 1 file changed, 782 insertions(+) create mode 100644 .claude/plans/2026-05-22-toolbar-badge.md diff --git a/.claude/plans/2026-05-22-toolbar-badge.md b/.claude/plans/2026-05-22-toolbar-badge.md new file mode 100644 index 000000000..41c6bf360 --- /dev/null +++ b/.claude/plans/2026-05-22-toolbar-badge.md @@ -0,0 +1,782 @@ +# toolbar_badge() Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add `toolbar_badge()` and `update_toolbar_badge()` to bslib's toolbar family as a pure display element (no Shiny input binding) that can be updated from the server via `sendCustomMessage`. + +**Architecture:** `toolbar_badge()` renders a Bootstrap `badge` `` with `text-bg-{color}` styling, following the same hidden-label + `aria-labelledby` accessibility pattern as `toolbar_input_button()`. Server updates go through a `sendCustomMessage("bslib.update-toolbar-badge", ...)` custom message handler in a new TypeScript file that is imported into the existing components bundle. + +**Tech Stack:** R (htmltools, rlang, shiny), TypeScript (esbuild bundle), SCSS (Bootstrap 5 utilities) + +--- + +## File Map + +| File | Action | +|------|--------| +| `R/toolbar.R` | Append `toolbar_badge()` and `update_toolbar_badge()` | +| `srcts/src/components/toolbarBadge.ts` | Create: custom message handler | +| `srcts/src/components/index.ts` | Modify: add `import "./toolbarBadge"` | +| `inst/components/scss/toolbar.scss` | Modify: add `.bslib-toolbar-badge` rule | +| `tests/testthat/test-toolbar.R` | Modify: append badge tests | +| `inst/examples-shiny/toolbar/app.R` | Modify: add badge usage example | +| `NAMESPACE` | Auto-generated by `devtools::document()` | +| `NEWS.md` | Modify: add entry under dev version | + +--- + +### Task 1: Add SCSS rule for `.bslib-toolbar-badge` + +**Files:** +- Modify: `inst/components/scss/toolbar.scss` + +- [ ] **Step 1: Add the SCSS rule** + +Append to the end of `inst/components/scss/toolbar.scss`, before the final blank line: + +```scss +.bslib-toolbar-badge { + display: inline-flex; + align-items: center; + gap: 0.25em; + + .bslib-toolbar-icon { + display: inline-flex; + align-items: center; + } +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add inst/components/scss/toolbar.scss +git commit -m "style: add .bslib-toolbar-badge SCSS rule" +``` + +--- + +### Task 2: Write failing tests for `toolbar_badge()` core HTML + +**Files:** +- Modify: `tests/testthat/test-toolbar.R` + +- [ ] **Step 1: Append tests to `tests/testthat/test-toolbar.R`** + +```r +# Tests for toolbar_badge() ---- + +test_that("toolbar_badge() snapshot tests", { + # Default: text-only badge, show_label = TRUE, no tooltip + expect_snapshot_html( + toolbar_badge("Active") + ) + # Icon-only badge (show_label defaults to FALSE): label hidden, tooltip present + expect_snapshot_html( + toolbar_badge("Status", icon = shiny::icon("circle"), id = "badge1") + ) + # Icon + label shown + expect_snapshot_html( + toolbar_badge("Status", icon = shiny::icon("circle"), show_label = TRUE, tooltip = FALSE, id = "badge2") + ) + # Pill variant + expect_snapshot_html( + toolbar_badge("New", pill = TRUE) + ) + # Color variants + expect_snapshot_html(toolbar_badge("OK", color = "success")) + expect_snapshot_html(toolbar_badge("Error", color = "danger")) + # Nested inside toolbar() + expect_snapshot_html( + toolbar( + toolbar_badge("Active"), + toolbar_badge("3 errors", color = "danger") + ) + ) +}) + +test_that("toolbar_badge() applies text-bg-{color} class", { + badge <- toolbar_badge("Test", color = "success", tooltip = FALSE) + expect_match(htmltools::tagGetAttribute(badge, "class"), "text-bg-success") + badge2 <- toolbar_badge("Test", tooltip = FALSE) + expect_match(htmltools::tagGetAttribute(badge2, "class"), "text-bg-secondary") +}) + +test_that("toolbar_badge() pill = TRUE adds rounded-pill class", { + badge <- toolbar_badge("Test", pill = TRUE, tooltip = FALSE) + expect_match(htmltools::tagGetAttribute(badge, "class"), "rounded-pill") + + badge2 <- toolbar_badge("Test", pill = FALSE, tooltip = FALSE) + expect_false(grepl("rounded-pill", htmltools::tagGetAttribute(badge2, "class") %||% "")) +}) + +test_that("toolbar_badge() show_label = FALSE sets hidden on label span", { + badge <- toolbar_badge("Hidden", icon = shiny::icon("circle"), id = "b1", tooltip = FALSE) + label_el <- tagQuery(as.tags(badge))$find(".bslib-toolbar-label")$selectedTags()[[1]] + expect_false(is.null(htmltools::tagGetAttribute(label_el, "hidden"))) +}) + +test_that("toolbar_badge() show_label = TRUE omits hidden on label span", { + badge <- toolbar_badge("Visible", show_label = TRUE) + label_el <- tagQuery(as.tags(badge))$find(".bslib-toolbar-label")$selectedTags()[[1]] + expect_null(htmltools::tagGetAttribute(label_el, "hidden")) +}) + +test_that("toolbar_badge() outer span has aria-labelledby pointing to label span", { + badge <- toolbar_badge("Status", icon = shiny::icon("circle"), tooltip = FALSE) + label_el <- tagQuery(as.tags(badge))$find(".bslib-toolbar-label")$selectedTags()[[1]] + label_id <- htmltools::tagGetAttribute(label_el, "id") + expect_false(is.null(label_id)) + expect_equal(htmltools::tagGetAttribute(badge, "aria-labelledby"), label_id) +}) + +test_that("toolbar_badge() invalid color aborts", { + expect_error(toolbar_badge("Test", color = "purple"), "`color` must be one of") + expect_error(toolbar_badge("Test", color = "red"), "`color` must be one of") +}) + +test_that("toolbar_badge() unnamed ... args abort", { + expect_error(toolbar_badge("Test", span("child")), "must be named") +}) + +test_that("toolbar_badge() named ... args pass as HTML attributes", { + badge <- toolbar_badge("Test", `data-foo` = "bar", tooltip = FALSE) + expect_equal(htmltools::tagGetAttribute(badge, "data-foo"), "bar") +}) +``` + +- [ ] **Step 2: Run tests to confirm they fail with "function not found"** + +```r +devtools::test(filter = "toolbar") +``` + +Expected: errors with `could not find function "toolbar_badge"` + +--- + +### Task 3: Implement `toolbar_badge()` + +**Files:** +- Modify: `R/toolbar.R` + +- [ ] **Step 1: Append `toolbar_badge()` to `R/toolbar.R`** + +```r +#' Toolbar Badge +#' +#' @description +#' A display badge for use in a [toolbar()]. Badges are non-interactive status +#' indicators. Use [update_toolbar_badge()] to update the badge from the server. +#' +#' @param label The badge label text or tag. Required for accessibility even +#' when hidden. +#' @param ... Additional named HTML attributes passed to the outer ``. +#' @param id An optional ID. Required when using [update_toolbar_badge()]. When +#' a tooltip is shown, the tooltip gets `id = "{id}_tooltip"`. +#' @param icon An optional decorative icon. +#' @param show_label Whether to show the label. Defaults to `TRUE` when no icon +#' is provided, `FALSE` when an icon is provided (icon-only mode). +#' @param tooltip Tooltip shown on hover. Defaults to `!show_label`. Accepts +#' `TRUE` (use label text), `FALSE` (no tooltip), a character string, or a +#' tag object. Tooltip content can be updated via +#' [update_tooltip()][bslib::update_tooltip()] using id `"{id}_tooltip"`. +#' @param color Bootstrap contextual color. One of `"primary"`, +#' `"secondary"`, `"success"`, `"danger"`, `"warning"`, `"info"`, +#' `"light"`, or `"dark"`. Defaults to `"secondary"`. +#' @param pill If `TRUE`, renders with fully-rounded ends. +#' +#' @return A badge element for use in a [toolbar()]. +#' @family toolbar components +#' @describeIn toolbar_badge Create a toolbar badge. +#' @export +toolbar_badge <- function( + label, + ..., + id = NULL, + icon = NULL, + show_label = is.null(icon), + tooltip = !show_label, + color = "secondary", + pill = FALSE +) { + .toolbar_badge_valid_colors <- c( + "primary", "secondary", "success", "danger", + "warning", "info", "light", "dark" + ) + if (!color %in% .toolbar_badge_valid_colors) { + rlang::abort(sprintf( + '`color` must be one of %s, not "%s".', + paste0('"', .toolbar_badge_valid_colors, '"', collapse = ", "), + color + )) + } + + dots <- separate_arguments(...) + if (length(dots$children) > 0) { + rlang::abort("All arguments in `...` must be named.") + } + + label_text <- paste(unlist(find_characters(label)), collapse = " ") + if (!nzchar(trimws(label_text))) { + warning("Consider providing a non-empty string label for accessibility.") + } + + label_id <- paste0("badge-label-", p_randomInt(1000, 10000)) + + icon_elem <- if (!is.null(icon)) { + tags$span( + class = "bslib-toolbar-icon", + `aria-hidden` = "true", + style = "pointer-events: none", + icon + ) + } + + label_elem <- tags$span( + id = label_id, + class = "bslib-toolbar-label", + hidden = if (!show_label) NA else NULL, + label + ) + + badge <- tags$span( + id = id, + class = paste( + c( + "bslib-toolbar-badge badge", + paste0("text-bg-", color), + if (isTRUE(pill)) "rounded-pill" + ), + collapse = " " + ), + `aria-labelledby` = label_id, + !!!dots$attribs, + icon_elem, + label_elem + ) + + if (isTRUE(tooltip)) { + tooltip <- label + } + if (isFALSE(tooltip)) { + tooltip <- NULL + } + if (!is.null(tooltip)) { + badge <- bslib::tooltip( + badge, + tooltip, + id = if (!is.null(id)) sprintf("%s_tooltip", id) else NULL, + placement = "bottom" + ) + } + + badge +} +``` + +- [ ] **Step 2: Update NAMESPACE** + +```bash +Rscript -e 'devtools::document()' +``` + +Expected: `Writing NAMESPACE` line in output. Confirm `export(toolbar_badge)` appears in `NAMESPACE`. + +- [ ] **Step 3: Run tests** + +```r +devtools::test(filter = "toolbar") +``` + +Expected: snapshot tests pass (snapshots created on first run). All new unit tests pass. If any snapshot test fails after first creation, run again to confirm stable. + +- [ ] **Step 4: Commit** + +```bash +git add R/toolbar.R NAMESPACE tests/testthat/test-toolbar.R tests/testthat/_snaps/toolbar.md +git commit -m "feat: add toolbar_badge() R function" +``` + +--- + +### Task 4: Write failing tests for `toolbar_badge()` tooltip behavior + +**Files:** +- Modify: `tests/testthat/test-toolbar.R` + +- [ ] **Step 1: Append tooltip tests to `tests/testthat/test-toolbar.R`** + +```r +test_that("toolbar_badge() tooltip defaults to TRUE when show_label = FALSE", { + badge <- toolbar_badge("Status", icon = shiny::icon("circle"), id = "b1") + tooltip_els <- tagQuery(as.tags(badge))$find("bslib-tooltip")$selectedTags() + expect_length(tooltip_els, 1) +}) + +test_that("toolbar_badge() tooltip id is {id}_tooltip when id is provided", { + badge <- toolbar_badge("Status", icon = shiny::icon("circle"), id = "mybadge") + tooltip_el <- tagQuery(as.tags(badge))$find("bslib-tooltip")$selectedTags()[[1]] + expect_equal(htmltools::tagGetAttribute(tooltip_el, "id"), "mybadge_tooltip") +}) + +test_that("toolbar_badge() tooltip has no id when badge id is NULL", { + badge <- toolbar_badge("Status", icon = shiny::icon("circle")) + tooltip_el <- tagQuery(as.tags(badge))$find("bslib-tooltip")$selectedTags()[[1]] + expect_null(htmltools::tagGetAttribute(tooltip_el, "id")) +}) + +test_that("toolbar_badge() tooltip = FALSE skips tooltip wrapper", { + badge <- toolbar_badge("Status", icon = shiny::icon("circle"), tooltip = FALSE) + tooltip_els <- tagQuery(as.tags(badge))$find("bslib-tooltip")$selectedTags() + expect_length(tooltip_els, 0) +}) + +test_that("toolbar_badge() tooltip = TRUE uses label as tooltip content", { + expect_snapshot_html( + toolbar_badge("Active status", icon = shiny::icon("circle"), id = "b2") + ) +}) + +test_that("toolbar_badge() custom string tooltip passes through", { + expect_snapshot_html( + toolbar_badge("Status", icon = shiny::icon("circle"), tooltip = "Custom tip", id = "b3") + ) +}) + +test_that("toolbar_badge() text-only badge has no tooltip by default", { + badge <- toolbar_badge("Active") + tooltip_els <- tagQuery(as.tags(badge))$find("bslib-tooltip")$selectedTags() + expect_length(tooltip_els, 0) +}) +``` + +- [ ] **Step 2: Run tests to confirm they all pass** (tooltip behavior already implemented in Task 3) + +```r +devtools::test(filter = "toolbar") +``` + +Expected: all pass. Update snapshots if needed with `testthat::snapshot_accept("toolbar")`. + +- [ ] **Step 3: Commit** + +```bash +git add tests/testthat/test-toolbar.R tests/testthat/_snaps/toolbar.md +git commit -m "test: add toolbar_badge() tooltip tests" +``` + +--- + +### Task 5: Write failing tests and implement `update_toolbar_badge()` + +**Files:** +- Modify: `tests/testthat/test-toolbar.R` +- Modify: `R/toolbar.R` + +- [ ] **Step 1: Append `update_toolbar_badge()` tests to `tests/testthat/test-toolbar.R`** + +```r +# Tests for update_toolbar_badge() ---- + +test_that("update_toolbar_badge() sends correct custom message type", { + session <- list( + sendCustomMessage = function(type, message) { + session$last_type <<- type + session$last_message <<- message + } + ) + + update_toolbar_badge("my_badge", label = "Updated", session = session) + expect_equal(session$last_type, "bslib.update-toolbar-badge") +}) + +test_that("update_toolbar_badge() includes provided fields in message", { + session <- list( + sendCustomMessage = function(type, message) { + session$last_message <<- message + } + ) + + update_toolbar_badge( + "my_badge", + label = "New", + color = "success", + pill = TRUE, + show_label = TRUE, + session = session + ) + + expect_equal(session$last_message$id, "my_badge") + expect_equal(session$last_message$color, "success") + expect_true(session$last_message$pill) + expect_true(session$last_message$showLabel) +}) + +test_that("update_toolbar_badge() drops NULL fields from message", { + session <- list( + sendCustomMessage = function(type, message) { + session$last_message <<- message + } + ) + + update_toolbar_badge("my_badge", color = "danger", session = session) + + expect_null(session$last_message$label) + expect_null(session$last_message$icon) + expect_null(session$last_message$showLabel) + expect_null(session$last_message$pill) + expect_equal(session$last_message$color, "danger") +}) + +test_that("update_toolbar_badge() errors on invalid color", { + session <- list( + sendCustomMessage = function(type, message) invisible(NULL) + ) + expect_error( + update_toolbar_badge("my_badge", color = "purple", session = session), + "`color` must be one of" + ) +}) + +test_that("update_toolbar_badge() warns on blank label", { + expect_warning( + expect_error(update_toolbar_badge("my_badge", label = "")), + "non-empty string label" + ) + expect_warning( + expect_error(update_toolbar_badge("my_badge", label = " ")), + "non-empty string label" + ) +}) +``` + +- [ ] **Step 2: Run tests to confirm `update_toolbar_badge` tests fail** + +```r +devtools::test(filter = "toolbar") +``` + +Expected: errors with `could not find function "update_toolbar_badge"` for the new tests. + +- [ ] **Step 3: Append `update_toolbar_badge()` to `R/toolbar.R`** + +Add immediately after `toolbar_badge()`: + +```r +#' @param session A Shiny session object (the default should almost always be +#' used). +#' @describeIn toolbar_badge Update a toolbar badge from the server. +#' @export +update_toolbar_badge <- function( + id, + label = NULL, + icon = NULL, + show_label = NULL, + color = NULL, + pill = NULL, + session = get_current_session() +) { + 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.") + } + } + + if (!is.null(color)) { + .toolbar_badge_valid_colors <- c( + "primary", "secondary", "success", "danger", + "warning", "info", "light", "dark" + ) + if (!color %in% .toolbar_badge_valid_colors) { + rlang::abort(sprintf( + '`color` must be one of %s, not "%s".', + paste0('"', .toolbar_badge_valid_colors, '"', collapse = ", "), + color + )) + } + } + + icon <- validateIcon(icon) + icon_processed <- if (!is.null(icon)) processDeps(icon, session) + label_processed <- if (!is.null(label)) processDeps(label, session) + + message <- dropNulls(list( + id = id, + label = label_processed, + icon = icon_processed, + showLabel = show_label, + color = color, + pill = pill + )) + + session$sendCustomMessage("bslib.update-toolbar-badge", message) +} +``` + +- [ ] **Step 4: Update NAMESPACE** + +```bash +Rscript -e 'devtools::document()' +``` + +Expected: `export(update_toolbar_badge)` added to `NAMESPACE`. + +- [ ] **Step 5: Run tests** + +```r +devtools::test(filter = "toolbar") +``` + +Expected: all tests pass. + +- [ ] **Step 6: Commit** + +```bash +git add R/toolbar.R NAMESPACE tests/testthat/test-toolbar.R +git commit -m "feat: add update_toolbar_badge() R function" +``` + +--- + +### Task 6: Create TypeScript message handler + +**Files:** +- Create: `srcts/src/components/toolbarBadge.ts` +- Modify: `srcts/src/components/index.ts` + +- [ ] **Step 1: Create `srcts/src/components/toolbarBadge.ts`** + +```typescript +import { shinyAddCustomMessageHandlers } from "./_shinyAddCustomMessageHandlers"; +import { shinyRenderContent, hasDefinedProperty } from "./_utils"; +import type { HtmlDep } from "./_utils"; + +interface UpdateToolbarBadgeMessage { + id: string; + label?: string | { html: string; deps: HtmlDep[] }; + icon?: string | { html: string; deps: HtmlDep[] }; + showLabel?: boolean; + color?: string; + pill?: boolean; +} + +const badgeColorClasses = [ + "text-bg-primary", + "text-bg-secondary", + "text-bg-success", + "text-bg-danger", + "text-bg-warning", + "text-bg-info", + "text-bg-light", + "text-bg-dark", +]; + +async function updateToolbarBadge( + message: UpdateToolbarBadgeMessage +): Promise { + const el = document.getElementById(message.id); + if (!el) return; + + if (hasDefinedProperty(message, "label") && message.label !== undefined) { + const labelEl = el.querySelector(".bslib-toolbar-label") as HTMLElement; + if (labelEl) await shinyRenderContent(labelEl, message.label); + } + + if (hasDefinedProperty(message, "icon") && message.icon !== undefined) { + const iconEl = el.querySelector(".bslib-toolbar-icon") as HTMLElement; + if (iconEl) await shinyRenderContent(iconEl, message.icon); + } + + if (hasDefinedProperty(message, "showLabel")) { + const labelEl = el.querySelector(".bslib-toolbar-label") as HTMLElement; + if (labelEl) { + if (message.showLabel === false) { + labelEl.setAttribute("hidden", ""); + } else { + labelEl.removeAttribute("hidden"); + } + } + } + + if (hasDefinedProperty(message, "color") && message.color !== undefined) { + el.classList.remove(...badgeColorClasses); + el.classList.add(`text-bg-${message.color}`); + } + + if (hasDefinedProperty(message, "pill")) { + if (message.pill) { + el.classList.add("rounded-pill"); + } else { + el.classList.remove("rounded-pill"); + } + } +} + +shinyAddCustomMessageHandlers({ + // eslint-disable-next-line @typescript-eslint/naming-convention + "bslib.update-toolbar-badge": updateToolbarBadge, +}); +``` + +- [ ] **Step 2: Register in `srcts/src/components/index.ts`** + +Add after the `"./toolbarInputSelect"` import line: + +```typescript +import "./toolbarBadge"; +``` + +The import block should look like: + +```typescript +import "./accordion"; +import "./card"; +import "./sidebar"; +import "./taskButton"; +import "./toolbarInputButton"; +import "./toolbarInputSelect"; +import "./toolbarBadge"; +import "./submitTextArea"; +import "./toast"; +``` + +- [ ] **Step 3: Build the TypeScript bundle** + +```bash +yarn build_bslib +``` + +Expected: output includes: +``` +Building components.min.js +Building components.js +... +√ - components.min.js +√ - components.js +``` + +If linting errors appear, run `yarn build` to see them and fix before proceeding. + +- [ ] **Step 4: Commit** + +```bash +git add srcts/src/components/toolbarBadge.ts srcts/src/components/index.ts inst/components/dist/components.min.js inst/components/dist/components.js +git commit -m "feat: add toolbarBadge.ts custom message handler" +``` + +--- + +### Task 7: Update example app + +**Files:** +- Modify: `inst/examples-shiny/toolbar/app.R` + +- [ ] **Step 1: Add badge usage to the example app** + +In `inst/examples-shiny/toolbar/app.R`, find the `card_footer` of the main "Sales Data" card (around the toolbar with `"update_filter_btn"` and `"update_export_btn"`). Add a badge showing a record count before that toolbar: + +```r +card_footer( + toolbar( + align = "left", + toolbar_badge("247 records", id = "record_count", color = "secondary"), + toolbar_badge("", id = "status_badge", color = "success", + icon = icon("circle-check"), tooltip = "Data is up to date") + ), + toolbar( + align = "right", + toolbar_input_button( + id = "update_filter_btn", + # ... existing code unchanged +``` + +In the server, add an example update: + +```r +observeEvent(input$refresh, { + update_toolbar_badge("status_badge", color = "warning", + icon = icon("arrows-rotate"), tooltip = "Refreshing...") + Sys.sleep(0.5) + update_toolbar_badge("status_badge", color = "success", + icon = icon("circle-check"), tooltip = "Data is up to date") +}) +``` + +- [ ] **Step 2: Verify the app runs without error** + +```r +shiny::runApp("inst/examples-shiny/toolbar") +``` + +Check that: +- Badges render in the card footer +- Clicking "Refresh" cycles the status badge from success → warning → success + +- [ ] **Step 3: Commit** + +```bash +git add inst/examples-shiny/toolbar/app.R +git commit -m "feat: add toolbar_badge() to toolbar example app" +``` + +--- + +### Task 8: Update NEWS.md + +**Files:** +- Modify: `NEWS.md` + +- [ ] **Step 1: Add entry under the development version heading** + +In `NEWS.md`, find `# bslib (development version)` at the top. Add under the `## New features` section (create it if not present): + +```markdown +* Added `toolbar_badge()` for displaying status text and icons in a `toolbar()`. + Use `update_toolbar_badge()` to update the badge label, icon, color, and pill + style from the server. (#1316) +``` + +- [ ] **Step 2: Commit** + +```bash +git add NEWS.md +git commit -m "news: document toolbar_badge() addition (#1316)" +``` + +--- + +## Self-Review + +**Spec coverage check:** + +| Spec requirement | Covered by | +|-----------------|------------| +| `toolbar_badge()` function signature | Task 3 | +| `update_toolbar_badge()` function signature | Task 5 | +| `label`, `id`, `icon`, `show_label`, `tooltip`, `color`, `pill` params | Task 3 | +| `...` as named HTML attrs | Task 3 | +| Bootstrap `text-bg-{color}` class | Task 3 | +| `rounded-pill` for `pill = TRUE` | Task 3 | +| `hidden` attr on label when `show_label = FALSE` | Task 3 | +| `aria-labelledby` pointing to random label_id | Task 3 | +| `tooltip = !show_label` default | Task 3 | +| `{id}_tooltip` tooltip ID | Task 3, tested Task 4 | +| `update_toolbar_badge()` uses `sendCustomMessage` | Task 5 | +| `dropNulls` — only changed fields in message | Task 5 | +| Invalid color errors | Task 3, Task 5 | +| Blank label warns | Task 3, Task 5 | +| TypeScript `updateToolbarBadge` handler | Task 6 | +| Handler updates label, icon, showLabel, color, pill | Task 6 | +| SCSS `.bslib-toolbar-badge` rule | Task 1 | +| NAMESPACE exports | Tasks 3, 5 (via `devtools::document()`) | +| Snapshot tests | Task 2 | +| Unit tests (color, pill, hidden, aria) | Task 2 | +| Update function mock-session tests | Task 5 | +| Example app | Task 7 | +| NEWS.md | Task 8 | + +**Placeholder scan:** No TBDs, TODOs, or "implement later" phrases. All code blocks are complete. + +**Type consistency:** `UpdateToolbarBadgeMessage` interface field names (`id`, `label`, `icon`, `showLabel`, `color`, `pill`) match `dropNulls(list(...))` keys in `update_toolbar_badge()`. `shinyRenderContent`, `hasDefinedProperty`, `HtmlDep` imported from `./_utils` — same pattern as `toolbarInputButton.ts`. From fc2ea155e22c7783674340231dc3feac1cf6644c Mon Sep 17 00:00:00 2001 From: E Nelson Date: Fri, 22 May 2026 17:17:51 -0400 Subject: [PATCH 03/22] style: add .bslib-toolbar-badge SCSS rule --- inst/components/scss/toolbar.scss | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/inst/components/scss/toolbar.scss b/inst/components/scss/toolbar.scss index 5ed370b53..b67cab2e3 100644 --- a/inst/components/scss/toolbar.scss +++ b/inst/components/scss/toolbar.scss @@ -225,3 +225,14 @@ label > .bslib-toolbar { margin-left: 0.15rem; } } + +.bslib-toolbar-badge { + display: inline-flex; + align-items: center; + gap: 0.25em; + + .bslib-toolbar-icon { + display: inline-flex; + align-items: center; + } +} From 4517097b484c765d00e7f0abe643725f39e8268b Mon Sep 17 00:00:00 2001 From: E Nelson Date: Fri, 22 May 2026 17:21:23 -0400 Subject: [PATCH 04/22] test: add failing tests for toolbar_badge() --- tests/testthat/test-toolbar.R | 80 +++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/tests/testthat/test-toolbar.R b/tests/testthat/test-toolbar.R index 7e7f45cbb..b2604bd6c 100644 --- a/tests/testthat/test-toolbar.R +++ b/tests/testthat/test-toolbar.R @@ -1022,3 +1022,83 @@ test_that("update_toolbar_input_button() can disable and reenable button", { expect_true(!is.null(session$last_message$label)) expect_true(!is.null(session$last_message$icon)) }) + +# Tests for toolbar_badge() ---- + +test_that("toolbar_badge() snapshot tests", { + # Default: text-only badge, show_label = TRUE, no tooltip + expect_snapshot_html( + toolbar_badge("Active") + ) + # Icon-only badge (show_label defaults to FALSE): label hidden, tooltip present + expect_snapshot_html( + toolbar_badge("Status", icon = shiny::icon("circle"), id = "badge1") + ) + # Icon + label shown + expect_snapshot_html( + toolbar_badge("Status", icon = shiny::icon("circle"), show_label = TRUE, tooltip = FALSE, id = "badge2") + ) + # Pill variant + expect_snapshot_html( + toolbar_badge("New", pill = TRUE) + ) + # Color variants + expect_snapshot_html(toolbar_badge("OK", color = "success")) + expect_snapshot_html(toolbar_badge("Error", color = "danger")) + # Nested inside toolbar() + expect_snapshot_html( + toolbar( + toolbar_badge("Active"), + toolbar_badge("3 errors", color = "danger") + ) + ) +}) + +test_that("toolbar_badge() applies text-bg-{color} class", { + badge <- toolbar_badge("Test", color = "success", tooltip = FALSE) + expect_match(htmltools::tagGetAttribute(badge, "class"), "text-bg-success") + badge2 <- toolbar_badge("Test", tooltip = FALSE) + expect_match(htmltools::tagGetAttribute(badge2, "class"), "text-bg-secondary") +}) + +test_that("toolbar_badge() pill = TRUE adds rounded-pill class", { + badge <- toolbar_badge("Test", pill = TRUE, tooltip = FALSE) + expect_match(htmltools::tagGetAttribute(badge, "class"), "rounded-pill") + + badge2 <- toolbar_badge("Test", pill = FALSE, tooltip = FALSE) + expect_false(grepl("rounded-pill", htmltools::tagGetAttribute(badge2, "class") %||% "")) +}) + +test_that("toolbar_badge() show_label = FALSE sets hidden on label span", { + badge <- toolbar_badge("Hidden", icon = shiny::icon("circle"), id = "b1", tooltip = FALSE) + label_el <- tagQuery(as.tags(badge))$find(".bslib-toolbar-label")$selectedTags()[[1]] + expect_false(is.null(htmltools::tagGetAttribute(label_el, "hidden"))) +}) + +test_that("toolbar_badge() show_label = TRUE omits hidden on label span", { + badge <- toolbar_badge("Visible", show_label = TRUE) + label_el <- tagQuery(as.tags(badge))$find(".bslib-toolbar-label")$selectedTags()[[1]] + expect_null(htmltools::tagGetAttribute(label_el, "hidden")) +}) + +test_that("toolbar_badge() outer span has aria-labelledby pointing to label span", { + badge <- toolbar_badge("Status", icon = shiny::icon("circle"), tooltip = FALSE) + label_el <- tagQuery(as.tags(badge))$find(".bslib-toolbar-label")$selectedTags()[[1]] + label_id <- htmltools::tagGetAttribute(label_el, "id") + expect_false(is.null(label_id)) + expect_equal(htmltools::tagGetAttribute(badge, "aria-labelledby"), label_id) +}) + +test_that("toolbar_badge() invalid color aborts", { + expect_error(toolbar_badge("Test", color = "purple"), "`color` must be one of") + expect_error(toolbar_badge("Test", color = "red"), "`color` must be one of") +}) + +test_that("toolbar_badge() unnamed ... args abort", { + expect_error(toolbar_badge("Test", span("child")), "must be named") +}) + +test_that("toolbar_badge() named ... args pass as HTML attributes", { + badge <- toolbar_badge("Test", `data-foo` = "bar", tooltip = FALSE) + expect_equal(htmltools::tagGetAttribute(badge, "data-foo"), "bar") +}) From 1f5be088b66262d7b8564221bcb6f8c66148c02c Mon Sep 17 00:00:00 2001 From: E Nelson Date: Tue, 26 May 2026 11:20:00 -0400 Subject: [PATCH 05/22] feat: add toolbar_badge() R function --- NAMESPACE | 1 + R/toolbar.R | 111 +++++++++++++++++++++++++++++++ tests/testthat/_snaps/toolbar.md | 78 ++++++++++++++++++++++ 3 files changed, 190 insertions(+) diff --git a/NAMESPACE b/NAMESPACE index 5a9b78e3d..59442ed42 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -164,6 +164,7 @@ export(toggle_sidebar) export(toggle_switch) export(toggle_tooltip) export(toolbar) +export(toolbar_badge) export(toolbar_divider) export(toolbar_input_button) export(toolbar_input_select) diff --git a/R/toolbar.R b/R/toolbar.R index d36243ca5..695720f04 100644 --- a/R/toolbar.R +++ b/R/toolbar.R @@ -960,3 +960,114 @@ toolbar_divider <- function(..., width = NULL, gap = NULL) { toolbar_spacer <- function() { div(class = "bslib-toolbar-spacer") } + +#' Toolbar Badge +#' +#' @description +#' A display badge for use in a [toolbar()]. Badges are non-interactive status +#' indicators. Use [update_toolbar_badge()] to update the badge from the server. +#' +#' @param label The badge label text or tag. Required for accessibility even +#' when hidden. +#' @param ... Additional named HTML attributes passed to the outer ``. +#' @param id An optional ID. Required when using [update_toolbar_badge()]. When +#' a tooltip is shown, the tooltip gets `id = "{id}_tooltip"`. +#' @param icon An optional decorative icon. +#' @param show_label Whether to show the label. Defaults to `TRUE` when no icon +#' is provided, `FALSE` when an icon is provided (icon-only mode). +#' @param tooltip Tooltip shown on hover. Defaults to `!show_label`. Accepts +#' `TRUE` (use label text), `FALSE` (no tooltip), a character string, or a +#' tag object. Tooltip content can be updated via +#' [update_tooltip()][bslib::update_tooltip()] using id `"{id}_tooltip"`. +#' @param color Bootstrap contextual color. One of `"primary"`, +#' `"secondary"`, `"success"`, `"danger"`, `"warning"`, `"info"`, +#' `"light"`, or `"dark"`. Defaults to `"secondary"`. +#' @param pill If `TRUE`, renders with fully-rounded ends. +#' +#' @return A badge element for use in a [toolbar()]. +#' @family toolbar components +#' @describeIn toolbar_badge Create a toolbar badge. +#' @export +toolbar_badge <- function( + label, + ..., + id = NULL, + icon = NULL, + show_label = is.null(icon), + tooltip = !show_label, + color = "secondary", + pill = FALSE +) { + .toolbar_badge_valid_colors <- c( + "primary", "secondary", "success", "danger", + "warning", "info", "light", "dark" + ) + if (!color %in% .toolbar_badge_valid_colors) { + rlang::abort(sprintf( + '`color` must be one of %s, not "%s".', + paste0('"', .toolbar_badge_valid_colors, '"', collapse = ", "), + color + )) + } + + dots <- separate_arguments(...) + if (length(dots$children) > 0) { + rlang::abort("All arguments in `...` must be named.") + } + + label_text <- paste(unlist(find_characters(label)), collapse = " ") + if (!nzchar(trimws(label_text))) { + warning("Consider providing a non-empty string label for accessibility.") + } + + label_id <- paste0("badge-label-", p_randomInt(1000, 10000)) + + icon_elem <- if (!is.null(icon)) { + tags$span( + class = "bslib-toolbar-icon", + `aria-hidden` = "true", + style = "pointer-events: none", + icon + ) + } + + label_elem <- tags$span( + id = label_id, + class = "bslib-toolbar-label", + hidden = if (!show_label) NA else NULL, + label + ) + + badge <- tags$span( + id = id, + class = paste( + c( + "bslib-toolbar-badge badge", + paste0("text-bg-", color), + if (isTRUE(pill)) "rounded-pill" + ), + collapse = " " + ), + `aria-labelledby` = label_id, + !!!dots$attribs, + icon_elem, + label_elem + ) + + if (isTRUE(tooltip)) { + tooltip <- label + } + if (isFALSE(tooltip)) { + tooltip <- NULL + } + if (!is.null(tooltip)) { + badge <- bslib::tooltip( + badge, + tooltip, + id = if (!is.null(id)) sprintf("%s_tooltip", id) else NULL, + placement = "bottom" + ) + } + + badge +} diff --git a/tests/testthat/_snaps/toolbar.md b/tests/testthat/_snaps/toolbar.md index 98c547131..7e4090cfb 100644 --- a/tests/testthat/_snaps/toolbar.md +++ b/tests/testthat/_snaps/toolbar.md @@ -384,3 +384,81 @@ Warning: `selected` value 'D' is not in `choices`. +# toolbar_badge() snapshot tests + + Code + show_raw_html(toolbar_badge("Active")) + Output + + Active + + +--- + + Code + show_raw_html(toolbar_badge("Status", icon = shiny::icon("circle"), id = "badge1")) + Output + + + + + + + + +--- + + Code + show_raw_html(toolbar_badge("Status", icon = shiny::icon("circle"), show_label = TRUE, + tooltip = FALSE, id = "badge2")) + Output + + + Status + + +--- + + Code + show_raw_html(toolbar_badge("New", pill = TRUE)) + Output + + New + + +--- + + Code + show_raw_html(toolbar_badge("OK", color = "success")) + Output + + OK + + +--- + + Code + show_raw_html(toolbar_badge("Error", color = "danger")) + Output + + Error + + +--- + + Code + show_raw_html(toolbar(toolbar_badge("Active"), toolbar_badge("3 errors", color = "danger"))) + Output +
+ + Active + + + 3 errors + +
+ From 48be8f9fb1a79354d871ab266d73cc0e24cfa594 Mon Sep 17 00:00:00 2001 From: E Nelson Date: Tue, 26 May 2026 11:23:53 -0400 Subject: [PATCH 06/22] fix: clean up toolbar_badge() docs and valid_colors naming --- R/toolbar.R | 14 +++++++--- man/toolbar_badge.Rd | 66 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 man/toolbar_badge.Rd diff --git a/R/toolbar.R b/R/toolbar.R index 695720f04..c78f03613 100644 --- a/R/toolbar.R +++ b/R/toolbar.R @@ -984,9 +984,15 @@ toolbar_spacer <- function() { #' `"light"`, or `"dark"`. Defaults to `"secondary"`. #' @param pill If `TRUE`, renders with fully-rounded ends. #' +#' @examplesIf rlang::is_interactive() +#' toolbar( +#' toolbar_badge("Active", color = "success"), +#' toolbar_badge("3 errors", color = "danger"), +#' toolbar_badge("Loading", icon = shiny::icon("spinner"), id = "status") +#' ) +#' #' @return A badge element for use in a [toolbar()]. #' @family toolbar components -#' @describeIn toolbar_badge Create a toolbar badge. #' @export toolbar_badge <- function( label, @@ -998,14 +1004,14 @@ toolbar_badge <- function( color = "secondary", pill = FALSE ) { - .toolbar_badge_valid_colors <- c( + valid_colors <- c( "primary", "secondary", "success", "danger", "warning", "info", "light", "dark" ) - if (!color %in% .toolbar_badge_valid_colors) { + if (!color %in% valid_colors) { rlang::abort(sprintf( '`color` must be one of %s, not "%s".', - paste0('"', .toolbar_badge_valid_colors, '"', collapse = ", "), + paste0('"', valid_colors, '"', collapse = ", "), color )) } diff --git a/man/toolbar_badge.Rd b/man/toolbar_badge.Rd new file mode 100644 index 000000000..a481007c4 --- /dev/null +++ b/man/toolbar_badge.Rd @@ -0,0 +1,66 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/toolbar.R +\name{toolbar_badge} +\alias{toolbar_badge} +\title{Toolbar Badge} +\usage{ +toolbar_badge( + label, + ..., + id = NULL, + icon = NULL, + show_label = is.null(icon), + tooltip = !show_label, + color = "secondary", + pill = FALSE +) +} +\arguments{ +\item{label}{The badge label text or tag. Required for accessibility even +when hidden.} + +\item{...}{Additional named HTML attributes passed to the outer \verb{}.} + +\item{id}{An optional ID. Required when using \code{\link[=update_toolbar_badge]{update_toolbar_badge()}}. When +a tooltip is shown, the tooltip gets \code{id = "{id}_tooltip"}.} + +\item{icon}{An optional decorative icon.} + +\item{show_label}{Whether to show the label. Defaults to \code{TRUE} when no icon +is provided, \code{FALSE} when an icon is provided (icon-only mode).} + +\item{tooltip}{Tooltip shown on hover. Defaults to \code{!show_label}. Accepts +\code{TRUE} (use label text), \code{FALSE} (no tooltip), a character string, or a +tag object. Tooltip content can be updated via +\link[=update_tooltip]{update_tooltip()} using id \code{"{id}_tooltip"}.} + +\item{color}{Bootstrap contextual color. One of \code{"primary"}, +\code{"secondary"}, \code{"success"}, \code{"danger"}, \code{"warning"}, \code{"info"}, +\code{"light"}, or \code{"dark"}. Defaults to \code{"secondary"}.} + +\item{pill}{If \code{TRUE}, renders with fully-rounded ends.} +} +\value{ +A badge element for use in a \code{\link[=toolbar]{toolbar()}}. +} +\description{ +A display badge for use in a \code{\link[=toolbar]{toolbar()}}. Badges are non-interactive status +indicators. Use \code{\link[=update_toolbar_badge]{update_toolbar_badge()}} to update the badge from the server. +} +\examples{ +\dontshow{if (rlang::is_interactive()) withAutoprint(\{ # examplesIf} +toolbar( + toolbar_badge("Active", color = "success"), + toolbar_badge("3 errors", color = "danger"), + toolbar_badge("Loading", icon = shiny::icon("spinner"), id = "status") +) +\dontshow{\}) # examplesIf} +} +\seealso{ +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 b12bb2143a4df3f4a2d39def85ed2fc98dab2a13 Mon Sep 17 00:00:00 2001 From: E Nelson Date: Tue, 26 May 2026 11:32:01 -0400 Subject: [PATCH 07/22] test: add toolbar_badge() tooltip behavior tests --- tests/testthat/_snaps/toolbar.md | 31 ++++++++++++++++++++++ tests/testthat/test-toolbar.R | 44 ++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/tests/testthat/_snaps/toolbar.md b/tests/testthat/_snaps/toolbar.md index 7e4090cfb..f96f5a696 100644 --- a/tests/testthat/_snaps/toolbar.md +++ b/tests/testthat/_snaps/toolbar.md @@ -462,3 +462,34 @@ +# toolbar_badge() tooltip = TRUE snapshot (uses label as tooltip) + + Code + show_raw_html(toolbar_badge("Active status", icon = shiny::icon("circle"), id = "b2")) + Output + + + + + + + + +# toolbar_badge() custom string tooltip snapshot + + Code + show_raw_html(toolbar_badge("Status", icon = shiny::icon("circle"), tooltip = "Custom tip", + id = "b3")) + Output + + + + + + + + diff --git a/tests/testthat/test-toolbar.R b/tests/testthat/test-toolbar.R index b2604bd6c..3c7dc308a 100644 --- a/tests/testthat/test-toolbar.R +++ b/tests/testthat/test-toolbar.R @@ -1102,3 +1102,47 @@ test_that("toolbar_badge() named ... args pass as HTML attributes", { badge <- toolbar_badge("Test", `data-foo` = "bar", tooltip = FALSE) expect_equal(htmltools::tagGetAttribute(badge, "data-foo"), "bar") }) + +# Tests for toolbar_badge() tooltip behavior ---- + +test_that("toolbar_badge() tooltip wraps when show_label = FALSE (default for icon)", { + badge <- toolbar_badge("Status", icon = shiny::icon("circle"), id = "b1") + tooltip_els <- tagQuery(as.tags(badge))$filter("bslib-tooltip")$selectedTags() + expect_length(tooltip_els, 1) +}) + +test_that("toolbar_badge() tooltip id is {id}_tooltip when id is provided", { + badge <- toolbar_badge("Status", icon = shiny::icon("circle"), id = "mybadge") + tooltip_el <- tagQuery(as.tags(badge))$filter("bslib-tooltip")$selectedTags()[[1]] + expect_equal(htmltools::tagGetAttribute(tooltip_el, "id"), "mybadge_tooltip") +}) + +test_that("toolbar_badge() tooltip has no id when badge id is NULL", { + badge <- toolbar_badge("Status", icon = shiny::icon("circle")) + tooltip_el <- tagQuery(as.tags(badge))$filter("bslib-tooltip")$selectedTags()[[1]] + expect_null(htmltools::tagGetAttribute(tooltip_el, "id")) +}) + +test_that("toolbar_badge() tooltip = FALSE skips tooltip wrapper", { + badge <- toolbar_badge("Status", icon = shiny::icon("circle"), tooltip = FALSE) + tooltip_els <- tagQuery(as.tags(badge))$filter("bslib-tooltip")$selectedTags() + expect_length(tooltip_els, 0) +}) + +test_that("toolbar_badge() tooltip = TRUE snapshot (uses label as tooltip)", { + expect_snapshot_html( + toolbar_badge("Active status", icon = shiny::icon("circle"), id = "b2") + ) +}) + +test_that("toolbar_badge() custom string tooltip snapshot", { + expect_snapshot_html( + toolbar_badge("Status", icon = shiny::icon("circle"), tooltip = "Custom tip", id = "b3") + ) +}) + +test_that("toolbar_badge() text-only badge has no tooltip by default", { + badge <- toolbar_badge("Active") + tooltip_els <- tagQuery(as.tags(badge))$filter("bslib-tooltip")$selectedTags() + expect_length(tooltip_els, 0) +}) From 4ac645343455983c377b1604be4066ba1f8466a0 Mon Sep 17 00:00:00 2001 From: E Nelson Date: Tue, 26 May 2026 12:19:15 -0400 Subject: [PATCH 08/22] feat: add update_toolbar_badge() R function --- NAMESPACE | 1 + R/toolbar.R | 51 ++++++++++++++++++++++++ man/toolbar_badge.Rd | 21 ++++++++++ tests/testthat/test-toolbar.R | 73 +++++++++++++++++++++++++++++++++++ 4 files changed, 146 insertions(+) diff --git a/NAMESPACE b/NAMESPACE index 59442ed42..c16a6decc 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -175,6 +175,7 @@ export(update_popover) export(update_submit_textarea) export(update_switch) export(update_task_button) +export(update_toolbar_badge) export(update_toolbar_input_button) export(update_toolbar_input_select) export(update_tooltip) diff --git a/R/toolbar.R b/R/toolbar.R index c78f03613..67355d132 100644 --- a/R/toolbar.R +++ b/R/toolbar.R @@ -993,6 +993,7 @@ toolbar_spacer <- function() { #' #' @return A badge element for use in a [toolbar()]. #' @family toolbar components +#' @describeIn toolbar_badge Create a toolbar badge. #' @export toolbar_badge <- function( label, @@ -1077,3 +1078,53 @@ toolbar_badge <- function( badge } + +#' @param session A Shiny session object (the default should almost always be +#' used). +#' @describeIn toolbar_badge Update a toolbar badge from the server. +#' @export +update_toolbar_badge <- function( + id, + label = NULL, + icon = NULL, + show_label = NULL, + color = NULL, + pill = NULL, + session = get_current_session() +) { + 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.") + } + } + + if (!is.null(color)) { + valid_colors <- c( + "primary", "secondary", "success", "danger", + "warning", "info", "light", "dark" + ) + if (!color %in% valid_colors) { + rlang::abort(sprintf( + '`color` must be one of %s, not "%s".', + paste0('"', valid_colors, '"', collapse = ", "), + color + )) + } + } + + icon <- validateIcon(icon) + icon_processed <- if (!is.null(icon)) processDeps(icon, session) + label_processed <- if (!is.null(label)) processDeps(label, session) + + message <- dropNulls(list( + id = id, + label = label_processed, + icon = icon_processed, + showLabel = show_label, + color = color, + pill = pill + )) + + session$sendCustomMessage("bslib.update-toolbar-badge", message) +} diff --git a/man/toolbar_badge.Rd b/man/toolbar_badge.Rd index a481007c4..799977f7f 100644 --- a/man/toolbar_badge.Rd +++ b/man/toolbar_badge.Rd @@ -2,6 +2,7 @@ % Please edit documentation in R/toolbar.R \name{toolbar_badge} \alias{toolbar_badge} +\alias{update_toolbar_badge} \title{Toolbar Badge} \usage{ toolbar_badge( @@ -14,6 +15,16 @@ toolbar_badge( color = "secondary", pill = FALSE ) + +update_toolbar_badge( + id, + label = NULL, + icon = NULL, + show_label = NULL, + color = NULL, + pill = NULL, + session = get_current_session() +) } \arguments{ \item{label}{The badge label text or tag. Required for accessibility even @@ -39,6 +50,9 @@ tag object. Tooltip content can be updated via \code{"light"}, or \code{"dark"}. Defaults to \code{"secondary"}.} \item{pill}{If \code{TRUE}, renders with fully-rounded ends.} + +\item{session}{A Shiny session object (the default should almost always be +used).} } \value{ A badge element for use in a \code{\link[=toolbar]{toolbar()}}. @@ -47,6 +61,13 @@ A badge element for use in a \code{\link[=toolbar]{toolbar()}}. A display badge for use in a \code{\link[=toolbar]{toolbar()}}. Badges are non-interactive status indicators. Use \code{\link[=update_toolbar_badge]{update_toolbar_badge()}} to update the badge from the server. } +\section{Functions}{ +\itemize{ +\item \code{toolbar_badge()}: Create a toolbar badge. + +\item \code{update_toolbar_badge()}: Update a toolbar badge from the server. + +}} \examples{ \dontshow{if (rlang::is_interactive()) withAutoprint(\{ # examplesIf} toolbar( diff --git a/tests/testthat/test-toolbar.R b/tests/testthat/test-toolbar.R index 3c7dc308a..118bdcdc9 100644 --- a/tests/testthat/test-toolbar.R +++ b/tests/testthat/test-toolbar.R @@ -1146,3 +1146,76 @@ test_that("toolbar_badge() text-only badge has no tooltip by default", { tooltip_els <- tagQuery(as.tags(badge))$filter("bslib-tooltip")$selectedTags() expect_length(tooltip_els, 0) }) + +# Tests for update_toolbar_badge() ---- + +test_that("update_toolbar_badge() sends correct custom message type", { + session <- list( + sendCustomMessage = function(type, message) { + session$last_type <<- type + session$last_message <<- message + } + ) + + update_toolbar_badge("my_badge", label = "Updated", session = session) + expect_equal(session$last_type, "bslib.update-toolbar-badge") +}) + +test_that("update_toolbar_badge() includes provided fields in message", { + session <- list( + sendCustomMessage = function(type, message) { + session$last_message <<- message + } + ) + + update_toolbar_badge( + "my_badge", + label = "New", + color = "success", + pill = TRUE, + show_label = TRUE, + session = session + ) + + expect_equal(session$last_message$id, "my_badge") + expect_equal(session$last_message$color, "success") + expect_true(session$last_message$pill) + expect_true(session$last_message$showLabel) +}) + +test_that("update_toolbar_badge() drops NULL fields from message", { + session <- list( + sendCustomMessage = function(type, message) { + session$last_message <<- message + } + ) + + update_toolbar_badge("my_badge", color = "danger", session = session) + + expect_null(session$last_message$label) + expect_null(session$last_message$icon) + expect_null(session$last_message$showLabel) + expect_null(session$last_message$pill) + expect_equal(session$last_message$color, "danger") +}) + +test_that("update_toolbar_badge() errors on invalid color", { + session <- list( + sendCustomMessage = function(type, message) invisible(NULL) + ) + expect_error( + update_toolbar_badge("my_badge", color = "purple", session = session), + "`color` must be one of" + ) +}) + +test_that("update_toolbar_badge() warns on blank label", { + expect_warning( + expect_error(update_toolbar_badge("my_badge", label = "")), + "non-empty string label" + ) + expect_warning( + expect_error(update_toolbar_badge("my_badge", label = " ")), + "non-empty string label" + ) +}) From 7405b05578bb90ce1efd47d6ad38cacb9bcb642d Mon Sep 17 00:00:00 2001 From: E Nelson Date: Tue, 26 May 2026 12:47:14 -0400 Subject: [PATCH 09/22] feat: add toolbarBadge.ts custom message handler --- inst/components/dist/components.js | 95 +++++++++++++++++++++----- inst/components/dist/components.min.js | 10 +-- srcts/src/components/index.ts | 1 + srcts/src/components/toolbarBadge.ts | 69 +++++++++++++++++++ 4 files changed, 154 insertions(+), 21 deletions(-) create mode 100644 srcts/src/components/toolbarBadge.ts diff --git a/inst/components/dist/components.js b/inst/components/dist/components.js index d4fa003d2..3354e92fb 100644 --- a/inst/components/dist/components.js +++ b/inst/components/dist/components.js @@ -1,4 +1,4 @@ -/*! bslib 0.11.0 | (c) 2012-2026 RStudio, PBC. | License: MIT + file LICENSE */ +/*! bslib 0.11.0.9000 | (c) 2012-2026 RStudio, PBC. | License: MIT + file LICENSE */ "use strict"; (() => { var __getOwnPropNames = Object.getOwnPropertyNames; @@ -1829,6 +1829,83 @@ } }); + // srcts/src/components/_shinyAddCustomMessageHandlers.ts + function shinyAddCustomMessageHandlers(handlers) { + if (!window.Shiny) { + return; + } + for (const [name, handler] of Object.entries(handlers)) { + window.Shiny.addCustomMessageHandler(name, handler); + } + } + var init_shinyAddCustomMessageHandlers = __esm({ + "srcts/src/components/_shinyAddCustomMessageHandlers.ts"() { + "use strict"; + } + }); + + // srcts/src/components/toolbarBadge.ts + function updateToolbarBadge(message) { + return __async(this, null, function* () { + const el = document.getElementById(message.id); + if (!el) + return; + if (hasDefinedProperty(message, "label") && message.label !== void 0) { + const labelEl = el.querySelector(".bslib-toolbar-label"); + if (labelEl) + yield shinyRenderContent(labelEl, message.label); + } + if (hasDefinedProperty(message, "icon") && message.icon !== void 0) { + const iconEl = el.querySelector(".bslib-toolbar-icon"); + if (iconEl) + yield shinyRenderContent(iconEl, message.icon); + } + if (hasDefinedProperty(message, "showLabel")) { + const labelEl = el.querySelector(".bslib-toolbar-label"); + if (labelEl) { + if (message.showLabel === false) { + labelEl.setAttribute("hidden", ""); + } else { + labelEl.removeAttribute("hidden"); + } + } + } + if (hasDefinedProperty(message, "color") && message.color !== void 0) { + el.classList.remove(...badgeColorClasses); + el.classList.add(`text-bg-${message.color}`); + } + if (hasDefinedProperty(message, "pill")) { + if (message.pill) { + el.classList.add("rounded-pill"); + } else { + el.classList.remove("rounded-pill"); + } + } + }); + } + var badgeColorClasses; + var init_toolbarBadge = __esm({ + "srcts/src/components/toolbarBadge.ts"() { + "use strict"; + init_shinyAddCustomMessageHandlers(); + init_utils(); + badgeColorClasses = [ + "text-bg-primary", + "text-bg-secondary", + "text-bg-success", + "text-bg-danger", + "text-bg-warning", + "text-bg-info", + "text-bg-light", + "text-bg-dark" + ]; + shinyAddCustomMessageHandlers({ + // eslint-disable-next-line @typescript-eslint/naming-convention + "bslib.update-toolbar-badge": updateToolbarBadge + }); + } + }); + // srcts/src/components/submitTextArea.ts function updateDisabledState(el) { const btn = findSubmitButton(el); @@ -2023,21 +2100,6 @@ } }); - // srcts/src/components/_shinyAddCustomMessageHandlers.ts - function shinyAddCustomMessageHandlers(handlers) { - if (!window.Shiny) { - return; - } - for (const [name, handler] of Object.entries(handlers)) { - window.Shiny.addCustomMessageHandler(name, handler); - } - } - var init_shinyAddCustomMessageHandlers = __esm({ - "srcts/src/components/_shinyAddCustomMessageHandlers.ts"() { - "use strict"; - } - }); - // srcts/src/components/toast.ts function showToast(message) { return __async(this, null, function* () { @@ -2346,6 +2408,7 @@ init_taskButton(); init_toolbarInputButton(); init_toolbarInputSelect(); + init_toolbarBadge(); init_submitTextArea(); init_toast(); init_utils(); diff --git a/inst/components/dist/components.min.js b/inst/components/dist/components.min.js index cf48601f9..d58feecbb 100644 --- a/inst/components/dist/components.min.js +++ b/inst/components/dist/components.min.js @@ -1,9 +1,9 @@ -/*! bslib 0.11.0 | (c) 2012-2026 RStudio, PBC. | License: MIT + file LICENSE */ -"use strict";(()=>{var v=(n,e)=>()=>(n&&(e=n(n=0)),e);var Me=(n,e)=>()=>(e||n((e={exports:{}}).exports,e),e.exports);var re=(n,e,t)=>{if(!e.has(n))throw TypeError("Cannot "+t)};var m=(n,e,t)=>(re(n,e,"read from private field"),t?t.call(n):e.get(n)),y=(n,e,t)=>{if(e.has(n))throw TypeError("Cannot add the same private member more than once");e instanceof WeakSet?e.add(n):e.set(n,t)};var _=(n,e,t)=>(re(n,e,"access private method"),t);var u=(n,e,t)=>new Promise((i,s)=>{var r=a=>{try{c(t.next(a))}catch(h){s(h)}},o=a=>{try{c(t.throw(a))}catch(h){s(h)}},c=a=>a.done?i(a.value):Promise.resolve(a.value).then(r,o);c((t=t.apply(n,e)).next())});function E(n,e){g&&g.inputBindings.register(new n,"bslib."+e)}function N(n,e){window.bslib=window.bslib||{},window.bslib[n]?console.error(`[bslib] Global window.bslib.${n} was already defined, using previous definition.`):window.bslib[n]=e}function B({headline:n="",message:e,status:t="warning"}){document.dispatchEvent(new CustomEvent("shiny:client-message",{detail:{headline:n,message:e,status:t}}))}function b(n,e){return Object.prototype.hasOwnProperty.call(n,e)&&n[e]!==void 0}function oe(n){let e=["a[href]","area[href]","button","details summary","input","iframe","select","textarea",'[contentEditable=""]','[contentEditable="true"]','[contentEditable="TRUE"]',"[tabindex]"],t=[':not([tabindex="-1"])',":not([disabled])"],i=e.map(r=>r+t.join("")),s=n.querySelectorAll(i.join(", "));return Array.from(s)}function p(...n){return u(this,null,function*(){if(!g)throw new Error("This function must be called in a Shiny app.");return g.renderContentAsync?yield g.renderContentAsync.apply(null,n):yield g.renderContent.apply(null,n)})}function ae(n,e){return u(this,null,function*(){if(typeof n!="undefined"){if(e.length!==1)throw new Error("labelNode must be of length 1");typeof n=="string"&&(n={html:n,deps:[]}),n.html===""?e.addClass("shiny-label-null"):(yield p(e,n),e.removeClass("shiny-label-null"))}})}var g,f,L=v(()=>{"use strict";g=window.Shiny,f=g?g.InputBinding:class{}});var X,le=v(()=>{"use strict";L();X=class extends f{find(e){return $(e).find(".accordion.bslib-accordion-input")}getValue(e){let i=this._getItemInfo(e).filter(s=>s.isOpen()).map(s=>s.value);return i.length===0?null:i}subscribe(e,t){$(e).on("shown.bs.collapse.accordionInputBinding hidden.bs.collapse.accordionInputBinding",function(i){t(!0)})}unsubscribe(e){$(e).off(".accordionInputBinding")}receiveMessage(e,t){return u(this,null,function*(){let i=t.method;if(i==="set")this._setItems(e,t);else if(i==="open")this._openItems(e,t);else if(i==="close")this._closeItems(e,t);else if(i==="remove")this._removeItem(e,t);else if(i==="insert")yield this._insertItem(e,t);else if(i==="update")yield this._updateItem(e,t);else throw new Error(`Method not yet implemented: ${i}`)})}_setItems(e,t){let i=this._getItemInfo(e),s=this._getValues(e,i,t.values);i.forEach(r=>{s.indexOf(r.value)>-1?r.show():r.hide()})}_openItems(e,t){let i=this._getItemInfo(e),s=this._getValues(e,i,t.values);i.forEach(r=>{s.indexOf(r.value)>-1&&r.show()})}_closeItems(e,t){let i=this._getItemInfo(e),s=this._getValues(e,i,t.values);i.forEach(r=>{s.indexOf(r.value)>-1&&r.hide()})}_insertItem(e,t){return u(this,null,function*(){let i=this._findItem(e,t.target);i||(i=t.position==="before"?e.firstElementChild:e.lastElementChild);let s=t.panel;if(i?yield p(i,s,t.position==="before"?"beforeBegin":"afterEnd"):yield p(e,s),this._isAutoClosing(e)){let r=$(s.html).attr("data-value");$(e).find(`[data-value="${r}"] .accordion-collapse`).attr("data-bs-parent","#"+e.id)}})}_removeItem(e,t){var r;let i=this._getItemInfo(e).filter(o=>t.target.indexOf(o.value)>-1),s=(r=window.Shiny)==null?void 0:r.unbindAll;i.forEach(o=>{s&&s(o.item),o.item.remove()})}_updateItem(e,t){return u(this,null,function*(){let i=this._findItem(e,t.target);if(!i)throw new Error(`Unable to find an accordion_panel() with a value of ${t.target}`);if(b(t,"value")&&(i.dataset.value=t.value),b(t,"body")){let r=i.querySelector(".accordion-body");yield p(r,t.body)}let s=i.querySelector(".accordion-header");if(b(t,"title")){let r=s.querySelector(".accordion-title");yield p(r,t.title)}if(b(t,"icon")){let r=s.querySelector(".accordion-button > .accordion-icon");yield p(r,t.icon)}})}_getItemInfo(e){return Array.from(e.querySelectorAll(":scope > .accordion-item")).map(i=>this._getSingleItemInfo(i))}_getSingleItemInfo(e){let t=e.querySelector(".accordion-collapse"),i=()=>$(t).hasClass("show");return{item:e,value:e.dataset.value,isOpen:i,show:()=>{i()||$(t).collapse("show")},hide:()=>{i()&&$(t).collapse("hide")}}}_getValues(e,t,i){let s=i!==!0?i:t.map(o=>o.value);return this._isAutoClosing(e)&&(s=s.slice(s.length-1,s.length)),s}_findItem(e,t){return e.querySelector(`[data-value="${t}"]`)}_isAutoClosing(e){return e.classList.contains("autoclose")}};E(X,"accordion")});var z,K=v(()=>{"use strict";z=class{constructor(){this.resizeObserverEntries=[],this.resizeObserver=new ResizeObserver(e=>{let t=new Event("resize");if(window.dispatchEvent(t),!window.Shiny)return;let i=[];for(let s of e)s.target instanceof HTMLElement&&s.target.querySelector(".shiny-bound-output")&&s.target.querySelectorAll(".shiny-bound-output").forEach(r=>{if(i.includes(r))return;let{binding:o,onResize:c}=$(r).data("shinyOutputBinding");if(!o||!o.resize)return;let a=r.shinyResizeObserver;if(a&&a!==this||(a||(r.shinyResizeObserver=this),c(r),i.push(r),!r.classList.contains("shiny-plot-output")))return;let h=r.querySelector('img:not([width="100%"])');h&&h.setAttribute("width","100%")})})}observe(e){this.resizeObserver.observe(e),this.resizeObserverEntries.push(e)}unobserve(e){let t=this.resizeObserverEntries.indexOf(e);t<0||(this.resizeObserver.unobserve(e),this.resizeObserverEntries.splice(t,1))}flush(){this.resizeObserverEntries.forEach(e=>{document.body.contains(e)||this.unobserve(e)})}}});var F,de=v(()=>{"use strict";F=class{constructor(e,t){this.watching=new Set,this.observer=new MutationObserver(i=>{let s=new Set;for(let{type:r,removedNodes:o}of i)if(r==="childList"&&o.length!==0)for(let c of o)c instanceof HTMLElement&&(c.matches(e)&&s.add(c),c.querySelector(e)&&c.querySelectorAll(e).forEach(a=>s.add(a)));if(s.size!==0)for(let r of s)try{t(r)}catch(o){console.error(o)}})}observe(e){let t=this._flush();if(this.watching.has(e)){if(!t)return}else this.watching.add(e);t?this._restartObserver():this.observer.observe(e,{childList:!0,subtree:!0})}unobserve(e){this.watching.has(e)&&(this.watching.delete(e),this._flush(),this._restartObserver())}_restartObserver(){this.observer.disconnect();for(let e of this.watching)this.observer.observe(e,{childList:!0,subtree:!0})}_flush(){let e=!1,t=Array.from(this.watching);for(let i of t)document.body.contains(i)||(this.watching.delete(i),e=!0);return e}}});var l,T,ce=v(()=>{"use strict";L();K();de();l=class{constructor(e){var t;e.removeAttribute(l.attr.ATTR_INIT),(t=e.querySelector(`script[${l.attr.ATTR_INIT}]`))==null||t.remove(),this.card=e,l.instanceMap.set(e,this),l.shinyResizeObserver.observe(this.card),l.cardRemovedObserver.observe(document.body),this._addEventListeners(),this.overlay=this._createOverlay(),this._setShinyInput(),this._exitFullScreenOnEscape=this._exitFullScreenOnEscape.bind(this),this._trapFocusExit=this._trapFocusExit.bind(this)}enterFullScreen(e){var t;e&&e.preventDefault(),this.card.id&&this.overlay.anchor.setAttribute("aria-controls",this.card.id),document.addEventListener("keydown",this._exitFullScreenOnEscape,!1),document.addEventListener("keydown",this._trapFocusExit,!0),this.card.setAttribute(l.attr.ATTR_FULL_SCREEN,"true"),document.body.classList.add(l.attr.CLASS_HAS_FULL_SCREEN),this.card.insertAdjacentElement("beforebegin",this.overlay.container),(!this.card.contains(document.activeElement)||(t=document.activeElement)!=null&&t.classList.contains(l.attr.CLASS_FULL_SCREEN_ENTER))&&(this.card.setAttribute("tabindex","-1"),this.card.focus()),this._emitFullScreenEvent(!0),this._setShinyInput()}exitFullScreen(){document.removeEventListener("keydown",this._exitFullScreenOnEscape,!1),document.removeEventListener("keydown",this._trapFocusExit,!0),this.overlay.container.remove(),this.card.setAttribute(l.attr.ATTR_FULL_SCREEN,"false"),this.card.removeAttribute("tabindex"),document.body.classList.remove(l.attr.CLASS_HAS_FULL_SCREEN),this._emitFullScreenEvent(!1),this._setShinyInput()}_setShinyInput(){if(!this.card.classList.contains(l.attr.CLASS_SHINY_INPUT)||!g)return;if(!g.setInputValue){setTimeout(()=>this._setShinyInput(),0);return}let e=this.card.getAttribute(l.attr.ATTR_FULL_SCREEN);g.setInputValue(this.card.id+"_full_screen",e==="true")}_emitFullScreenEvent(e){let t=new CustomEvent("bslib.card",{bubbles:!0,detail:{fullScreen:e}});this.card.dispatchEvent(t)}_addEventListeners(){let e=this.card.querySelector(`:scope > * > .${l.attr.CLASS_FULL_SCREEN_ENTER}`);e&&e.addEventListener("click",t=>this.enterFullScreen(t))}_exitFullScreenOnEscape(e){if(!(e.target instanceof HTMLElement))return;let t=["select[open]","input[aria-expanded='true']"];e.target.matches(t.join(", "))||e.key==="Escape"&&this.exitFullScreen()}_trapFocusExit(e){if(!(e instanceof KeyboardEvent)||e.key!=="Tab")return;let t=e.target===this.card,i=e.target===this.overlay.anchor,s=this.card.contains(e.target),r=()=>{e.preventDefault(),e.stopImmediatePropagation()};if(!(s||t||i)){r(),this.card.focus();return}let o=oe(this.card).filter(C=>!C.classList.contains(l.attr.CLASS_FULL_SCREEN_ENTER));if(!(o.length>0)){r(),this.overlay.anchor.focus();return}if(t)return;let a=o[o.length-1],h=e.target===a;if(i&&e.shiftKey){r(),a.focus();return}if(h&&!e.shiftKey){r(),this.overlay.anchor.focus();return}}_createOverlay(){let e=document.createElement("div");e.id=l.attr.ID_FULL_SCREEN_OVERLAY,e.onclick=this.exitFullScreen.bind(this);let t=this._createOverlayCloseAnchor();return e.appendChild(t),{container:e,anchor:t}}_createOverlayCloseAnchor(){let e=document.createElement("a");return e.classList.add(l.attr.CLASS_FULL_SCREEN_EXIT),e.tabIndex=0,e.setAttribute("aria-expanded","true"),e.setAttribute("aria-label","Close card"),e.setAttribute("role","button"),e.onclick=t=>{this.exitFullScreen(),t.stopPropagation()},e.onkeydown=t=>{(t.key==="Enter"||t.key===" ")&&this.exitFullScreen()},e.innerHTML=this._overlayCloseHtml(),e}_overlayCloseHtml(){return"Close "}static getInstance(e){return l.instanceMap.get(e)}static initializeAllCards(e=!0){if(document.readyState==="loading"){l.onReadyScheduled||(l.onReadyScheduled=!0,document.addEventListener("DOMContentLoaded",()=>{l.initializeAllCards(!1)}));return}e&&l.shinyResizeObserver.flush();let t=`.${l.attr.CLASS_CARD}[${l.attr.ATTR_INIT}]`;if(!document.querySelector(t))return;document.querySelectorAll(t).forEach(s=>new l(s))}},T=l;T.attr={ATTR_INIT:"data-bslib-card-init",CLASS_CARD:"bslib-card",ATTR_FULL_SCREEN:"data-full-screen",CLASS_HAS_FULL_SCREEN:"bslib-has-full-screen",CLASS_FULL_SCREEN_ENTER:"bslib-full-screen-enter",CLASS_FULL_SCREEN_EXIT:"bslib-full-screen-exit",ID_FULL_SCREEN_OVERLAY:"bslib-full-screen-overlay",CLASS_SHINY_INPUT:"bslib-card-input"},T.shinyResizeObserver=new z,T.cardRemovedObserver=new F(`.${l.attr.CLASS_CARD}`,e=>{let t=l.getInstance(e);t&&t.card.getAttribute(l.attr.ATTR_FULL_SCREEN)==="true"&&t.exitFullScreen()}),T.instanceMap=new WeakMap,T.onReadyScheduled=!1;N("Card",T)});function ue(n,e){let t=n();return()=>{let i=n();i!==t&&e(),t=i}}var d,S,G,he=v(()=>{"use strict";L();K();d=class{constructor(e){this.resizeState={isResizing:!1,startX:0,startWidth:0,minWidth:150,maxWidth:()=>window.innerWidth-50,constrainedWidth:e=>Math.max(this.resizeState.minWidth,Math.min(this.resizeState.maxWidth(),e))};this.resizeHandleActivated=!1;this.resizeHandleEngagementX=0;this.resizeHandlePeakDx=0;this.windowSize="";var s;d.instanceMap.set(e,this),this.layout={container:e,main:e.querySelector(":scope > .main"),sidebar:e.querySelector(":scope > .sidebar"),toggle:e.querySelector(":scope > .collapse-toggle")};let t=this.layout.sidebar.querySelector(":scope > .sidebar-content > .accordion");t&&((s=t==null?void 0:t.parentElement)==null||s.classList.add("has-accordion"),t.classList.add("accordion-flush")),this._initSidebarCounters(),this._initSidebarState(),(this._isCollapsible("desktop")||this._isCollapsible("mobile"))&&this._initEventListeners(),this._initResizeHandle(),d.shinyResizeObserver.observe(this.layout.main),e.removeAttribute("data-bslib-sidebar-init");let i=e.querySelector(":scope > script[data-bslib-sidebar-init]");i&&e.removeChild(i)}get isClosed(){return this.layout.container.classList.contains(d.classes.COLLAPSE)}static getInstance(e){return d.instanceMap.get(e)}_isCollapsible(e="desktop"){let{container:t}=this.layout,i=e==="desktop"?"collapsibleDesktop":"collapsibleMobile",s=t.dataset[i];return s===void 0?!0:s.trim().toLowerCase()!=="false"}static initCollapsibleAll(e=!0){if(document.readyState==="loading"){d.onReadyScheduled||(d.onReadyScheduled=!0,document.addEventListener("DOMContentLoaded",()=>{d.initCollapsibleAll(!1)}));return}let t=`.${d.classes.LAYOUT}[data-bslib-sidebar-init]`;if(!document.querySelector(t))return;e&&d.shinyResizeObserver.flush(),document.querySelectorAll(t).forEach(s=>new d(s))}_initResizeHandle(){if(this.layout.sidebar.hasAttribute("data-resizable")){if(!this.layout.resizeHandle){let e=this._createResizeHandle();this.layout.container.appendChild(e),this.layout.resizeHandle=e,this._attachResizeEventListeners(e)}this._updateResizeAvailability()}}_createResizeHandle(){let e=document.createElement("div");e.className=d.classes.RESIZE_HANDLE,e.setAttribute("role","separator"),e.setAttribute("aria-orientation","vertical"),e.setAttribute("aria-label","Resize sidebar"),e.setAttribute("tabindex","0"),e.setAttribute("aria-keyshortcuts","ArrowLeft ArrowRight Home End"),e.title="Drag to resize sidebar";let t=document.createElement("div");t.className="resize-indicator",e.appendChild(t);let i=document.createElement("div");return i.className="visually-hidden",i.textContent="Use arrow keys to resize the sidebar, Shift for larger steps, Home/End for min/max width.",e.appendChild(i),e}_attachResizeEventListeners(e){e.addEventListener("mousedown",this._onResizeStart.bind(this)),e.addEventListener("mousemove",this._onResizeHandlePointerMove.bind(this)),e.addEventListener("mouseleave",this._onResizeHandlePointerLeave.bind(this)),document.addEventListener("mousemove",this._onResizeMove.bind(this)),document.addEventListener("mouseup",this._onResizeEnd.bind(this)),e.addEventListener("touchstart",this._onResizeStart.bind(this),{passive:!1}),document.addEventListener("touchmove",this._onResizeMove.bind(this),{passive:!1}),document.addEventListener("touchend",this._onResizeEnd.bind(this)),e.addEventListener("keydown",this._onResizeKeyDown.bind(this)),window.addEventListener("resize",ue(()=>this._getWindowSize(),()=>this._updateResizeAvailability()))}_shouldEnableResize(){let e=this._getWindowSize()==="desktop",t=!this.layout.container.classList.contains(d.classes.TRANSITIONING),i=!this.isClosed;return e&&t&&i}_onResizeStart(e){if(!this._shouldEnableResize()||!("touches"in e)&&!this.resizeHandleActivated)return;e.preventDefault();let t="touches"in e?e.touches[0].clientX:e.clientX;this.resizeState.isResizing=!0,this.resizeState.startX=t,this.resizeState.startWidth=this._getCurrentSidebarWidth(),this.layout.container.style.setProperty("--_transition-duration","0ms"),this.layout.container.classList.add(d.classes.RESIZING),document.documentElement.setAttribute(`data-bslib-${d.classes.RESIZING}`,"true"),this._dispatchResizeEvent("start",this.resizeState.startWidth)}_onResizeMove(e){if(!this.resizeState.isResizing)return;e.preventDefault();let i=("touches"in e?e.touches[0].clientX:e.clientX)-this.resizeState.startX,r=this._isRightSidebar()?this.resizeState.startWidth-i:this.resizeState.startWidth+i,o=this.resizeState.constrainedWidth(r);this._updateSidebarWidth(o),this._dispatchResizeEvent("move",o)}_onResizeEnd(){this.resizeState.isResizing&&(this.resizeState.isResizing=!1,this.layout.container.style.removeProperty("--_transition-duration"),this.layout.container.classList.remove(d.classes.RESIZING),document.documentElement.removeAttribute(`data-bslib-${d.classes.RESIZING}`),this._deactivateResizeHandle(),d.shinyResizeObserver.flush(),this._dispatchResizeEvent("end",this._getCurrentSidebarWidth()))}_onResizeKeyDown(e){if(!this._shouldEnableResize())return;let t=e.shiftKey?50:10,i=this._getCurrentSidebarWidth();switch(e.key){case"ArrowLeft":i=this._isRightSidebar()?i+t:i-t;break;case"ArrowRight":i=this._isRightSidebar()?i-t:i+t;break;case"Home":i=this.resizeState.minWidth;break;case"End":i=this.resizeState.maxWidth();break;default:return}e.preventDefault(),i=this.resizeState.constrainedWidth(i),this._updateSidebarWidth(i),d.shinyResizeObserver.flush(),this._dispatchResizeEvent("keyboard",i)}_getCurrentSidebarWidth(){return this.layout.sidebar.getBoundingClientRect().width||250}_updateSidebarWidth(e){let{container:t,resizeHandle:i}=this.layout;t.style.setProperty("--_sidebar-width",`${e}px`),i&&(i.setAttribute("aria-valuenow",e.toString()),i.setAttribute("aria-valuemin",this.resizeState.minWidth.toString()),i.setAttribute("aria-valuemax",this.resizeState.maxWidth().toString()))}_isRightSidebar(){return this.layout.container.classList.contains("sidebar-right")}_onResizeHandlePointerMove(e){if(this.resizeState.isResizing)return;let t=this.layout.resizeHandle;if(!t)return;if(!this.resizeHandleActivated){let s=this.layout.sidebar.getBoundingClientRect(),r=this._isRightSidebar()?s.left:s.right;Math.abs(e.clientX-r)<=2&&(this.resizeHandleActivated=!0,this.resizeHandleEngagementX=e.clientX,this.resizeHandlePeakDx=0,t.classList.add(d.classes.HANDLE_ACTIVE));return}let i=e.clientX-this.resizeHandleEngagementX;Math.abs(i)>Math.abs(this.resizeHandlePeakDx)&&(this.resizeHandlePeakDx=i),Math.abs(this.resizeHandlePeakDx)>3&&Math.sign(i)!==Math.sign(this.resizeHandlePeakDx)&&this._deactivateResizeHandle()}_deactivateResizeHandle(){var e;this.resizeHandleActivated=!1,this.resizeHandlePeakDx=0,(e=this.layout.resizeHandle)==null||e.classList.remove(d.classes.HANDLE_ACTIVE)}_onResizeHandlePointerLeave(){this.resizeState.isResizing||this._deactivateResizeHandle()}_updateResizeAvailability(){if(!this.layout.resizeHandle)return;let e=this._shouldEnableResize();this.layout.resizeHandle.style.display=e?"":"none",this.layout.resizeHandle.setAttribute("aria-hidden",e?"false":"true"),e?this.layout.resizeHandle.setAttribute("tabindex","0"):this.layout.resizeHandle.removeAttribute("tabindex")}_dispatchResizeEvent(e,t){let i=new CustomEvent("bslib.sidebar.resize",{bubbles:!0,detail:{phase:e,width:t,sidebar:this}});this.layout.sidebar.dispatchEvent(i)}_initEventListeners(){var t;let{toggle:e}=this.layout;e.addEventListener("click",i=>{i.preventDefault(),this.toggle("toggle")}),(t=e.querySelector(".collapse-icon"))==null||t.addEventListener("transitionend",()=>{this._finalizeState()}),!(this._isCollapsible("desktop")&&this._isCollapsible("mobile"))&&window.addEventListener("resize",ue(()=>this._getWindowSize(),()=>this._initSidebarState()))}_initSidebarCounters(){let{container:e}=this.layout,t=`.${d.classes.LAYOUT}> .main > .${d.classes.LAYOUT}:not([data-bslib-sidebar-open="always"])`;if(!(e.querySelector(t)===null))return;function s(a){return a=a?a.parentElement:null,a&&a.classList.contains("main")&&(a=a.parentElement),a&&a.classList.contains(d.classes.LAYOUT)?a:null}let r=[e],o=s(e);for(;o;)r.unshift(o),o=s(o);let c={left:0,right:0};r.forEach(function(a){let C=a.classList.contains("sidebar-right")?c.right++:c.left++;a.style.setProperty("--_js-toggle-count-this-side",C.toString()),a.style.setProperty("--_js-toggle-count-max-side",Math.max(c.right,c.left).toString())})}_getWindowSize(){let{container:e}=this.layout;return window.getComputedStyle(e).getPropertyValue("--bslib-sidebar-js-window-size").trim()}_initialToggleState(){var s,r;let{container:e}=this.layout,t=this.windowSize==="desktop"?"openDesktop":"openMobile",i=(r=(s=e.dataset[t])==null?void 0:s.trim())==null?void 0:r.toLowerCase();return i===void 0||["open","always"].includes(i)?"open":["close","closed"].includes(i)?"close":"open"}_initSidebarState(){this.windowSize=this._getWindowSize();let e=this._initialToggleState();this.toggle(e,!0)}toggle(e,t=!1){typeof e=="undefined"?e="toggle":e==="closed"&&(e="close");let{container:i,sidebar:s}=this.layout,r=this.isClosed;if(["open","close","toggle"].indexOf(e)===-1)throw new Error(`Unknown method ${e}`);if(e==="toggle"&&(e=r?"open":"close"),r&&e==="close"||!r&&e==="open"){t&&this._finalizeState();return}e==="open"&&(s.hidden=!1),i.classList.toggle(d.classes.TRANSITIONING,!t),i.classList.toggle(d.classes.COLLAPSE),t&&this._finalizeState()}_finalizeState(){let{container:e,sidebar:t,toggle:i}=this.layout;e.classList.remove(d.classes.TRANSITIONING),t.hidden=this.isClosed,i.setAttribute("aria-expanded",this.isClosed?"false":"true"),this._updateResizeAvailability();let s=new CustomEvent("bslib.sidebar",{bubbles:!0,detail:{open:!this.isClosed}});t.dispatchEvent(s),$(t).trigger("toggleCollapse.sidebarInputBinding"),$(t).trigger(this.isClosed?"hidden":"shown")}},S=d;S.shinyResizeObserver=new z,S.classes={LAYOUT:"bslib-sidebar-layout",COLLAPSE:"sidebar-collapsed",TRANSITIONING:"transitioning",RESIZE_HANDLE:"bslib-sidebar-resize-handle",RESIZING:"sidebar-resizing",HANDLE_ACTIVE:"handle-active"},S.onReadyScheduled=!1,S.instanceMap=new WeakMap;G=class extends f{find(e){return $(e).find(`.${S.classes.LAYOUT} > .bslib-sidebar-input`)}getValue(e){let t=S.getInstance(e.parentElement);return t?!t.isClosed:!1}setValue(e,t){let i=t?"open":"close";this.receiveMessage(e,{method:i})}subscribe(e,t){$(e).on("toggleCollapse.sidebarInputBinding",function(i){t(!0)})}unsubscribe(e){$(e).off(".sidebarInputBinding")}receiveMessage(e,t){let i=S.getInstance(e.parentElement);i&&i.toggle(t.method)}};E(G,"sidebar");N("Sidebar",S)});var A,I,k,Y,j,be=v(()=>{"use strict";L();j=class extends f{constructor(){super(...arguments);y(this,k);y(this,A,new WeakMap);y(this,I,new WeakMap)}find(t){return $(t).find(".bslib-task-button")}getValue(t){var i;return{value:(i=m(this,A).get(t))!=null?i:0,autoReset:t.hasAttribute("data-auto-reset")}}getType(){return"bslib.taskbutton"}subscribe(t,i){m(this,I).has(t)&&this.unsubscribe(t);let s=()=>{var r;m(this,A).set(t,((r=m(this,A).get(t))!=null?r:0)+1),i(!0),_(this,k,Y).call(this,t,"busy")};m(this,I).set(t,s),t.addEventListener("click",s)}unsubscribe(t){let i=m(this,I).get(t);i&&t.removeEventListener("click",i)}receiveMessage(s,r){return u(this,arguments,function*(t,{state:i}){_(this,k,Y).call(this,t,i)})}};A=new WeakMap,I=new WeakMap,k=new WeakSet,Y=function(t,i){t.disabled=i==="busy";let s=t.querySelector("bslib-switch-inline");s&&(s.case=i)};E(j,"task-button")});var R,x,W,pe,Z,me=v(()=>{"use strict";L();Z=class extends f{constructor(){super(...arguments);y(this,W);y(this,R,new WeakMap);y(this,x,new WeakMap)}find(t){return $(t).find(".bslib-toolbar-input-button")}getValue(t){var i;return(i=m(this,R).get(t))!=null?i:0}getType(){return"bslib.toolbar.button"}subscribe(t,i){m(this,x).has(t)&&this.unsubscribe(t);let s=()=>{var r;m(this,R).set(t,((r=m(this,R).get(t))!=null?r:0)+1),_(this,W,pe).call(this,t),i(!0)};m(this,x).set(t,s),t.addEventListener("click",s)}unsubscribe(t){let i=m(this,x).get(t);i&&t.removeEventListener("click",i)}receiveMessage(t,i){return u(this,null,function*(){if(b(i,"disabled")&&(t.disabled=i.disabled),b(i,"label")&&i.label!==void 0){let s=t.querySelector(".bslib-toolbar-label");yield p(s,i.label)}if(b(i,"showLabel")){let s=t.querySelector(".bslib-toolbar-label");i.showLabel===!1?(s.setAttribute("hidden",""),t.setAttribute("data-type","icon")):(s.removeAttribute("hidden"),t.setAttribute("data-type","both"))}if(b(i,"icon")&&i.icon!==void 0){let s=t.querySelector(".bslib-toolbar-icon");yield p(s,i.icon)}})}};R=new WeakMap,x=new WeakMap,W=new WeakSet,pe=function(t){let i=t.closest("bslib-tooltip");i&&i.hide()};E(Z,"toolbar-input-button")});var q,fe,J,ve=v(()=>{"use strict";L();J=class extends f{constructor(){super(...arguments);y(this,q)}find(t){return $(t).find(".bslib-toolbar-input-select")}getId(t){return t.id||""}getValue(t){let i=t.querySelector("select");return i==null?void 0:i.value}subscribe(t,i){let s=t.querySelector("select");s&&$(s).on("change.bslibToolbarInputSelect",()=>{_(this,q,fe).call(this,t),i(!1)})}unsubscribe(t){let i=t.querySelector("select");i&&$(i).off(".bslibToolbarInputSelect")}receiveMessage(t,i){return u(this,null,function*(){let s=t.querySelector("select");if(b(i,"label")&&i.label!==void 0){let r=t.querySelector(".bslib-toolbar-label");yield p(r,i.label)}if(b(i,"showLabel")){let r=t.querySelector(".bslib-toolbar-label");i.showLabel===!1?r.classList.add("visually-hidden"):r.classList.remove("visually-hidden")}if(b(i,"icon")&&i.icon!==void 0){let r=t.querySelector(".bslib-toolbar-icon");yield p(r,i.icon)}b(i,"options")&&s&&i.options&&(s.innerHTML=i.options),b(i,"value")&&s&&i.value!==void 0&&(s.value=i.value,$(s).trigger("change"))})}};q=new WeakSet,fe=function(t){let i=t.closest("bslib-tooltip");i&&i.hide()};E(J,"toolbar-input-select")});function Ee(n){let e=U(n),t=!n.value;e.classList.toggle("disabled",t),e.setAttribute("aria-disabled",t.toString()),t?e.setAttribute("tabindex","-1"):e.removeAttribute("tabindex")}function ee(n){n.scrollHeight!==0&&(n.style.height="auto",n.style.height=n.scrollHeight+"px")}function He(n){if(!n.hasAttribute("data-needs-modifier"))return;let e=U(n);if(!e.querySelector(`.${H.submitKey}`))return;let t=navigator.userAgent.indexOf("Mac")!==-1;e.querySelectorAll(`.${H.submitKey}`).forEach(r=>{let o=t?"\u2318":"Ctrl";r.textContent=`${o} \u23CE`});let i=t?"Command":"Ctrl";e.title=e.title.replace("Press Enter",`Press ${i}+Enter`);let s=e.getAttribute("aria-label");s&&e.setAttribute("aria-label",s.replace("Press Enter",`Press ${i}+Enter`))}function U(n){var t;let e=(t=n.parentElement)==null?void 0:t.querySelector(`.${H.button}`);if(e instanceof HTMLButtonElement)return e;throw new Error("Expected input_submit_textarea()'s container to have a button with class of 'bslib-submit-textarea-btn'")}function we(n){let e=n.selectionStart,t=n.selectionEnd;n.value=n.value.substring(0,e)+` -`+n.value.substring(t),n.selectionStart=n.selectionEnd=e+1,n.dispatchEvent(new Event("input",{bubbles:!0}))}var M,H,ge,Q,ye=v(()=>{"use strict";L();M="textSubmitInputBinding",H={input:"bslib-input-submit-textarea",container:"bslib-submit-textarea-container",button:"bslib-submit-textarea-btn",submitKey:"bslib-submit-key"},ge=new IntersectionObserver(n=>{n.forEach(e=>{e.isIntersecting&&ee(e.target)})}),Q=class extends f{find(e){return $(e).find(`.${H.input} textarea`)}initialize(e){Ee(e),ee(e),He(e)}getValue(e){return $(e).data("val")}setValue(e,t){e.value=t}subscribe(e,t){function i(){$(e).data("val",e.value),e.value="",e.dispatchEvent(new Event("input",{bubbles:!0})),t("event")}let s=U(e);s.classList.contains("shiny-bound-input")?$(s).on(`shiny:inputchanged.${M}`,i):$(s).on(`click.${M}`,i),$(e).on(`input.${M}`,function(){Ee(e),ee(e)}),$(e).on(`keydown.${M}`,function(o){if(o.key!=="Enter")return;if(!e.value){o.preventDefault();return}if(o.shiftKey)return;if(o.altKey){o.preventDefault(),we(e);return}let c=e.hasAttribute("data-needs-modifier");if(!c){o.preventDefault(),s.click();return}let a=o.ctrlKey||o.metaKey;if(c&&a){o.preventDefault(),s.click();return}});let r=e.closest(`.${H.container}`);$(r).on(`click.${M}`,o=>{o.target.classList.contains(H.container)&&e.focus()}),ge.observe(e)}unsubscribe(e){$(e).off(`.${M}`);let t=e.nextElementSibling;$(t).off(`.${M}`);let i=e.closest(`.${H.container}`);$(i).off(`.${M}`),ge.unobserve(e)}receiveMessage(e,t){return u(this,null,function*(){let i=e.value;if(t.value!==void 0&&(e.value=t.value,e.dispatchEvent(new Event("input",{bubbles:!0}))),t.placeholder!==void 0&&(e.placeholder=t.placeholder),t.label!==void 0){let s=$(e).closest(`.${H.input}`).find("label");yield ae(t.label,s)}t.submit&&(U(e).click(),e.value=i),t.focus&&e.focus()})}};E(Q,"submit-text-area")});function V(n){if(window.Shiny)for(let[e,t]of Object.entries(n))window.Shiny.addCustomMessageHandler(e,t)}var te=v(()=>{"use strict"});function ze(n){return u(this,null,function*(){var D,se;let{html:e,deps:t,autohide:i,duration:s,position:r,id:o}=n;if(!window.bootstrap||!window.bootstrap.Toast){B({headline:"Bootstrap 5 Required",message:"Toast notifications require Bootstrap 5.",status:"error"});return}let c=document.getElementById(o);if(c){let w=P.get(c);w&&(w.hide(),P.delete(c)),(se=(D=window==null?void 0:window.Shiny)==null?void 0:D.unbindAll)==null||se.call(D,c),c.remove()}let a=Ce.getOrCreateToaster(r);yield p(a,{html:e,deps:t},"beforeEnd");let h=document.getElementById(o);if(!h){B({headline:"Toast Creation Failed",message:`Failed to create toast with id "${o}".`,status:"error"});return}let C=new ie(h,{autohide:i,duration:s});P.set(h,C),C.show(),h.addEventListener("hidden.bs.toast",()=>{var w,ne;(ne=(w=window==null?void 0:window.Shiny)==null?void 0:w.unbindAll)==null||ne.call(w,h),h.remove(),P.delete(h),a.children.length===0&&a.remove()})})}function Ae(n){let{id:e}=n,t=document.getElementById(e);if(!t){B({headline:"Toast Not Found",message:`No toast with id "${e}" was found.`,status:"warning"});return}let i=P.get(t);i&&i.hide()}var _e,O,Ce,ie,P,Le=v(()=>{"use strict";te();L();_e=window.bootstrap?window.bootstrap.Toast:class{},O=class{constructor(){this.containers=new Map}getOrCreateToaster(e){let t=this.containers.get(e);return(!t||!document.body.contains(t))&&(t=O._createToaster(e),document.body.appendChild(t),this.containers.set(e,t)),t}static _createToaster(e){let t=document.createElement("div");return t.className="toast-container position-fixed p-1 p-md-2",t.setAttribute("data-bslib-toast-container",e),t.classList.add(...O._positionClasses(e)),t}static _positionClasses(e){return{"top-left":["top-0","start-0"],"top-center":["top-0","start-50","translate-middle-x"],"top-right":["top-0","end-0"],"middle-left":["top-50","start-0","translate-middle-y"],"middle-center":["top-50","start-50","translate-middle"],"middle-right":["top-50","end-0","translate-middle-y"],"bottom-left":["bottom-0","start-0"],"bottom-center":["bottom-0","start-50","translate-middle-x"],"bottom-right":["bottom-0","end-0"]}[e]}},Ce=new O,ie=class{constructor(e,t){this.progressBar=null;this.timeStart=0;this.timeRemaining=0;this.hideTimeoutId=null;this.isPaused=!1;this.isPointerOver=!1;this.hasFocus=!1;this.element=e,this.timeRemaining=t.duration||5e3;let i={animation:!0,autohide:!1};this.bsToast=new _e(e,i),t.autohide&&(this._addProgressBar(),this._setupInteractionPause())}show(){this.bsToast.show()}hide(){this.hideTimeoutId!==null&&(clearTimeout(this.hideTimeoutId),this.hideTimeoutId=null),this.bsToast.hide()}_addProgressBar(){this.progressBar=document.createElement("div"),this.progressBar.className="bslib-toast-progress-bar",this.progressBar.style.cssText=` +/*! bslib 0.11.0.9000 | (c) 2012-2026 RStudio, PBC. | License: MIT + file LICENSE */ +"use strict";(()=>{var v=(s,e)=>()=>(s&&(e=s(s=0)),e);var He=(s,e)=>()=>(e||s((e={exports:{}}).exports,e),e.exports);var re=(s,e,t)=>{if(!e.has(s))throw TypeError("Cannot "+t)};var m=(s,e,t)=>(re(s,e,"read from private field"),t?t.call(s):e.get(s)),L=(s,e,t)=>{if(e.has(s))throw TypeError("Cannot add the same private member more than once");e instanceof WeakSet?e.add(s):e.set(s,t)};var _=(s,e,t)=>(re(s,e,"access private method"),t);var h=(s,e,t)=>new Promise((i,n)=>{var r=a=>{try{c(t.next(a))}catch(p){n(p)}},o=a=>{try{c(t.throw(a))}catch(p){n(p)}},c=a=>a.done?i(a.value):Promise.resolve(a.value).then(r,o);c((t=t.apply(s,e)).next())});function E(s,e){g&&g.inputBindings.register(new s,"bslib."+e)}function N(s,e){window.bslib=window.bslib||{},window.bslib[s]?console.error(`[bslib] Global window.bslib.${s} was already defined, using previous definition.`):window.bslib[s]=e}function F({headline:s="",message:e,status:t="warning"}){document.dispatchEvent(new CustomEvent("shiny:client-message",{detail:{headline:s,message:e,status:t}}))}function u(s,e){return Object.prototype.hasOwnProperty.call(s,e)&&s[e]!==void 0}function oe(s){let e=["a[href]","area[href]","button","details summary","input","iframe","select","textarea",'[contentEditable=""]','[contentEditable="true"]','[contentEditable="TRUE"]',"[tabindex]"],t=[':not([tabindex="-1"])',":not([disabled])"],i=e.map(r=>r+t.join("")),n=s.querySelectorAll(i.join(", "));return Array.from(n)}function b(...s){return h(this,null,function*(){if(!g)throw new Error("This function must be called in a Shiny app.");return g.renderContentAsync?yield g.renderContentAsync.apply(null,s):yield g.renderContent.apply(null,s)})}function ae(s,e){return h(this,null,function*(){if(typeof s!="undefined"){if(e.length!==1)throw new Error("labelNode must be of length 1");typeof s=="string"&&(s={html:s,deps:[]}),s.html===""?e.addClass("shiny-label-null"):(yield b(e,s),e.removeClass("shiny-label-null"))}})}var g,f,y=v(()=>{"use strict";g=window.Shiny,f=g?g.InputBinding:class{}});var K,le=v(()=>{"use strict";y();K=class extends f{find(e){return $(e).find(".accordion.bslib-accordion-input")}getValue(e){let i=this._getItemInfo(e).filter(n=>n.isOpen()).map(n=>n.value);return i.length===0?null:i}subscribe(e,t){$(e).on("shown.bs.collapse.accordionInputBinding hidden.bs.collapse.accordionInputBinding",function(i){t(!0)})}unsubscribe(e){$(e).off(".accordionInputBinding")}receiveMessage(e,t){return h(this,null,function*(){let i=t.method;if(i==="set")this._setItems(e,t);else if(i==="open")this._openItems(e,t);else if(i==="close")this._closeItems(e,t);else if(i==="remove")this._removeItem(e,t);else if(i==="insert")yield this._insertItem(e,t);else if(i==="update")yield this._updateItem(e,t);else throw new Error(`Method not yet implemented: ${i}`)})}_setItems(e,t){let i=this._getItemInfo(e),n=this._getValues(e,i,t.values);i.forEach(r=>{n.indexOf(r.value)>-1?r.show():r.hide()})}_openItems(e,t){let i=this._getItemInfo(e),n=this._getValues(e,i,t.values);i.forEach(r=>{n.indexOf(r.value)>-1&&r.show()})}_closeItems(e,t){let i=this._getItemInfo(e),n=this._getValues(e,i,t.values);i.forEach(r=>{n.indexOf(r.value)>-1&&r.hide()})}_insertItem(e,t){return h(this,null,function*(){let i=this._findItem(e,t.target);i||(i=t.position==="before"?e.firstElementChild:e.lastElementChild);let n=t.panel;if(i?yield b(i,n,t.position==="before"?"beforeBegin":"afterEnd"):yield b(e,n),this._isAutoClosing(e)){let r=$(n.html).attr("data-value");$(e).find(`[data-value="${r}"] .accordion-collapse`).attr("data-bs-parent","#"+e.id)}})}_removeItem(e,t){var r;let i=this._getItemInfo(e).filter(o=>t.target.indexOf(o.value)>-1),n=(r=window.Shiny)==null?void 0:r.unbindAll;i.forEach(o=>{n&&n(o.item),o.item.remove()})}_updateItem(e,t){return h(this,null,function*(){let i=this._findItem(e,t.target);if(!i)throw new Error(`Unable to find an accordion_panel() with a value of ${t.target}`);if(u(t,"value")&&(i.dataset.value=t.value),u(t,"body")){let r=i.querySelector(".accordion-body");yield b(r,t.body)}let n=i.querySelector(".accordion-header");if(u(t,"title")){let r=n.querySelector(".accordion-title");yield b(r,t.title)}if(u(t,"icon")){let r=n.querySelector(".accordion-button > .accordion-icon");yield b(r,t.icon)}})}_getItemInfo(e){return Array.from(e.querySelectorAll(":scope > .accordion-item")).map(i=>this._getSingleItemInfo(i))}_getSingleItemInfo(e){let t=e.querySelector(".accordion-collapse"),i=()=>$(t).hasClass("show");return{item:e,value:e.dataset.value,isOpen:i,show:()=>{i()||$(t).collapse("show")},hide:()=>{i()&&$(t).collapse("hide")}}}_getValues(e,t,i){let n=i!==!0?i:t.map(o=>o.value);return this._isAutoClosing(e)&&(n=n.slice(n.length-1,n.length)),n}_findItem(e,t){return e.querySelector(`[data-value="${t}"]`)}_isAutoClosing(e){return e.classList.contains("autoclose")}};E(K,"accordion")});var A,G=v(()=>{"use strict";A=class{constructor(){this.resizeObserverEntries=[],this.resizeObserver=new ResizeObserver(e=>{let t=new Event("resize");if(window.dispatchEvent(t),!window.Shiny)return;let i=[];for(let n of e)n.target instanceof HTMLElement&&n.target.querySelector(".shiny-bound-output")&&n.target.querySelectorAll(".shiny-bound-output").forEach(r=>{if(i.includes(r))return;let{binding:o,onResize:c}=$(r).data("shinyOutputBinding");if(!o||!o.resize)return;let a=r.shinyResizeObserver;if(a&&a!==this||(a||(r.shinyResizeObserver=this),c(r),i.push(r),!r.classList.contains("shiny-plot-output")))return;let p=r.querySelector('img:not([width="100%"])');p&&p.setAttribute("width","100%")})})}observe(e){this.resizeObserver.observe(e),this.resizeObserverEntries.push(e)}unobserve(e){let t=this.resizeObserverEntries.indexOf(e);t<0||(this.resizeObserver.unobserve(e),this.resizeObserverEntries.splice(t,1))}flush(){this.resizeObserverEntries.forEach(e=>{document.body.contains(e)||this.unobserve(e)})}}});var W,de=v(()=>{"use strict";W=class{constructor(e,t){this.watching=new Set,this.observer=new MutationObserver(i=>{let n=new Set;for(let{type:r,removedNodes:o}of i)if(r==="childList"&&o.length!==0)for(let c of o)c instanceof HTMLElement&&(c.matches(e)&&n.add(c),c.querySelector(e)&&c.querySelectorAll(e).forEach(a=>n.add(a)));if(n.size!==0)for(let r of n)try{t(r)}catch(o){console.error(o)}})}observe(e){let t=this._flush();if(this.watching.has(e)){if(!t)return}else this.watching.add(e);t?this._restartObserver():this.observer.observe(e,{childList:!0,subtree:!0})}unobserve(e){this.watching.has(e)&&(this.watching.delete(e),this._flush(),this._restartObserver())}_restartObserver(){this.observer.disconnect();for(let e of this.watching)this.observer.observe(e,{childList:!0,subtree:!0})}_flush(){let e=!1,t=Array.from(this.watching);for(let i of t)document.body.contains(i)||(this.watching.delete(i),e=!0);return e}}});var l,S,ce=v(()=>{"use strict";y();G();de();l=class{constructor(e){var t;e.removeAttribute(l.attr.ATTR_INIT),(t=e.querySelector(`script[${l.attr.ATTR_INIT}]`))==null||t.remove(),this.card=e,l.instanceMap.set(e,this),l.shinyResizeObserver.observe(this.card),l.cardRemovedObserver.observe(document.body),this._addEventListeners(),this.overlay=this._createOverlay(),this._setShinyInput(),this._exitFullScreenOnEscape=this._exitFullScreenOnEscape.bind(this),this._trapFocusExit=this._trapFocusExit.bind(this)}enterFullScreen(e){var t;e&&e.preventDefault(),this.card.id&&this.overlay.anchor.setAttribute("aria-controls",this.card.id),document.addEventListener("keydown",this._exitFullScreenOnEscape,!1),document.addEventListener("keydown",this._trapFocusExit,!0),this.card.setAttribute(l.attr.ATTR_FULL_SCREEN,"true"),document.body.classList.add(l.attr.CLASS_HAS_FULL_SCREEN),this.card.insertAdjacentElement("beforebegin",this.overlay.container),(!this.card.contains(document.activeElement)||(t=document.activeElement)!=null&&t.classList.contains(l.attr.CLASS_FULL_SCREEN_ENTER))&&(this.card.setAttribute("tabindex","-1"),this.card.focus()),this._emitFullScreenEvent(!0),this._setShinyInput()}exitFullScreen(){document.removeEventListener("keydown",this._exitFullScreenOnEscape,!1),document.removeEventListener("keydown",this._trapFocusExit,!0),this.overlay.container.remove(),this.card.setAttribute(l.attr.ATTR_FULL_SCREEN,"false"),this.card.removeAttribute("tabindex"),document.body.classList.remove(l.attr.CLASS_HAS_FULL_SCREEN),this._emitFullScreenEvent(!1),this._setShinyInput()}_setShinyInput(){if(!this.card.classList.contains(l.attr.CLASS_SHINY_INPUT)||!g)return;if(!g.setInputValue){setTimeout(()=>this._setShinyInput(),0);return}let e=this.card.getAttribute(l.attr.ATTR_FULL_SCREEN);g.setInputValue(this.card.id+"_full_screen",e==="true")}_emitFullScreenEvent(e){let t=new CustomEvent("bslib.card",{bubbles:!0,detail:{fullScreen:e}});this.card.dispatchEvent(t)}_addEventListeners(){let e=this.card.querySelector(`:scope > * > .${l.attr.CLASS_FULL_SCREEN_ENTER}`);e&&e.addEventListener("click",t=>this.enterFullScreen(t))}_exitFullScreenOnEscape(e){if(!(e.target instanceof HTMLElement))return;let t=["select[open]","input[aria-expanded='true']"];e.target.matches(t.join(", "))||e.key==="Escape"&&this.exitFullScreen()}_trapFocusExit(e){if(!(e instanceof KeyboardEvent)||e.key!=="Tab")return;let t=e.target===this.card,i=e.target===this.overlay.anchor,n=this.card.contains(e.target),r=()=>{e.preventDefault(),e.stopImmediatePropagation()};if(!(n||t||i)){r(),this.card.focus();return}let o=oe(this.card).filter(C=>!C.classList.contains(l.attr.CLASS_FULL_SCREEN_ENTER));if(!(o.length>0)){r(),this.overlay.anchor.focus();return}if(t)return;let a=o[o.length-1],p=e.target===a;if(i&&e.shiftKey){r(),a.focus();return}if(p&&!e.shiftKey){r(),this.overlay.anchor.focus();return}}_createOverlay(){let e=document.createElement("div");e.id=l.attr.ID_FULL_SCREEN_OVERLAY,e.onclick=this.exitFullScreen.bind(this);let t=this._createOverlayCloseAnchor();return e.appendChild(t),{container:e,anchor:t}}_createOverlayCloseAnchor(){let e=document.createElement("a");return e.classList.add(l.attr.CLASS_FULL_SCREEN_EXIT),e.tabIndex=0,e.setAttribute("aria-expanded","true"),e.setAttribute("aria-label","Close card"),e.setAttribute("role","button"),e.onclick=t=>{this.exitFullScreen(),t.stopPropagation()},e.onkeydown=t=>{(t.key==="Enter"||t.key===" ")&&this.exitFullScreen()},e.innerHTML=this._overlayCloseHtml(),e}_overlayCloseHtml(){return"Close "}static getInstance(e){return l.instanceMap.get(e)}static initializeAllCards(e=!0){if(document.readyState==="loading"){l.onReadyScheduled||(l.onReadyScheduled=!0,document.addEventListener("DOMContentLoaded",()=>{l.initializeAllCards(!1)}));return}e&&l.shinyResizeObserver.flush();let t=`.${l.attr.CLASS_CARD}[${l.attr.ATTR_INIT}]`;if(!document.querySelector(t))return;document.querySelectorAll(t).forEach(n=>new l(n))}},S=l;S.attr={ATTR_INIT:"data-bslib-card-init",CLASS_CARD:"bslib-card",ATTR_FULL_SCREEN:"data-full-screen",CLASS_HAS_FULL_SCREEN:"bslib-has-full-screen",CLASS_FULL_SCREEN_ENTER:"bslib-full-screen-enter",CLASS_FULL_SCREEN_EXIT:"bslib-full-screen-exit",ID_FULL_SCREEN_OVERLAY:"bslib-full-screen-overlay",CLASS_SHINY_INPUT:"bslib-card-input"},S.shinyResizeObserver=new A,S.cardRemovedObserver=new W(`.${l.attr.CLASS_CARD}`,e=>{let t=l.getInstance(e);t&&t.card.getAttribute(l.attr.ATTR_FULL_SCREEN)==="true"&&t.exitFullScreen()}),S.instanceMap=new WeakMap,S.onReadyScheduled=!1;N("Card",S)});function ue(s,e){let t=s();return()=>{let i=s();i!==t&&e(),t=i}}var d,T,j,he=v(()=>{"use strict";y();G();d=class{constructor(e){this.resizeState={isResizing:!1,startX:0,startWidth:0,minWidth:150,maxWidth:()=>window.innerWidth-50,constrainedWidth:e=>Math.max(this.resizeState.minWidth,Math.min(this.resizeState.maxWidth(),e))};this.resizeHandleActivated=!1;this.resizeHandleEngagementX=0;this.resizeHandlePeakDx=0;this.windowSize="";var n;d.instanceMap.set(e,this),this.layout={container:e,main:e.querySelector(":scope > .main"),sidebar:e.querySelector(":scope > .sidebar"),toggle:e.querySelector(":scope > .collapse-toggle")};let t=this.layout.sidebar.querySelector(":scope > .sidebar-content > .accordion");t&&((n=t==null?void 0:t.parentElement)==null||n.classList.add("has-accordion"),t.classList.add("accordion-flush")),this._initSidebarCounters(),this._initSidebarState(),(this._isCollapsible("desktop")||this._isCollapsible("mobile"))&&this._initEventListeners(),this._initResizeHandle(),d.shinyResizeObserver.observe(this.layout.main),e.removeAttribute("data-bslib-sidebar-init");let i=e.querySelector(":scope > script[data-bslib-sidebar-init]");i&&e.removeChild(i)}get isClosed(){return this.layout.container.classList.contains(d.classes.COLLAPSE)}static getInstance(e){return d.instanceMap.get(e)}_isCollapsible(e="desktop"){let{container:t}=this.layout,i=e==="desktop"?"collapsibleDesktop":"collapsibleMobile",n=t.dataset[i];return n===void 0?!0:n.trim().toLowerCase()!=="false"}static initCollapsibleAll(e=!0){if(document.readyState==="loading"){d.onReadyScheduled||(d.onReadyScheduled=!0,document.addEventListener("DOMContentLoaded",()=>{d.initCollapsibleAll(!1)}));return}let t=`.${d.classes.LAYOUT}[data-bslib-sidebar-init]`;if(!document.querySelector(t))return;e&&d.shinyResizeObserver.flush(),document.querySelectorAll(t).forEach(n=>new d(n))}_initResizeHandle(){if(this.layout.sidebar.hasAttribute("data-resizable")){if(!this.layout.resizeHandle){let e=this._createResizeHandle();this.layout.container.appendChild(e),this.layout.resizeHandle=e,this._attachResizeEventListeners(e)}this._updateResizeAvailability()}}_createResizeHandle(){let e=document.createElement("div");e.className=d.classes.RESIZE_HANDLE,e.setAttribute("role","separator"),e.setAttribute("aria-orientation","vertical"),e.setAttribute("aria-label","Resize sidebar"),e.setAttribute("tabindex","0"),e.setAttribute("aria-keyshortcuts","ArrowLeft ArrowRight Home End"),e.title="Drag to resize sidebar";let t=document.createElement("div");t.className="resize-indicator",e.appendChild(t);let i=document.createElement("div");return i.className="visually-hidden",i.textContent="Use arrow keys to resize the sidebar, Shift for larger steps, Home/End for min/max width.",e.appendChild(i),e}_attachResizeEventListeners(e){e.addEventListener("mousedown",this._onResizeStart.bind(this)),e.addEventListener("mousemove",this._onResizeHandlePointerMove.bind(this)),e.addEventListener("mouseleave",this._onResizeHandlePointerLeave.bind(this)),document.addEventListener("mousemove",this._onResizeMove.bind(this)),document.addEventListener("mouseup",this._onResizeEnd.bind(this)),e.addEventListener("touchstart",this._onResizeStart.bind(this),{passive:!1}),document.addEventListener("touchmove",this._onResizeMove.bind(this),{passive:!1}),document.addEventListener("touchend",this._onResizeEnd.bind(this)),e.addEventListener("keydown",this._onResizeKeyDown.bind(this)),window.addEventListener("resize",ue(()=>this._getWindowSize(),()=>this._updateResizeAvailability()))}_shouldEnableResize(){let e=this._getWindowSize()==="desktop",t=!this.layout.container.classList.contains(d.classes.TRANSITIONING),i=!this.isClosed;return e&&t&&i}_onResizeStart(e){if(!this._shouldEnableResize()||!("touches"in e)&&!this.resizeHandleActivated)return;e.preventDefault();let t="touches"in e?e.touches[0].clientX:e.clientX;this.resizeState.isResizing=!0,this.resizeState.startX=t,this.resizeState.startWidth=this._getCurrentSidebarWidth(),this.layout.container.style.setProperty("--_transition-duration","0ms"),this.layout.container.classList.add(d.classes.RESIZING),document.documentElement.setAttribute(`data-bslib-${d.classes.RESIZING}`,"true"),this._dispatchResizeEvent("start",this.resizeState.startWidth)}_onResizeMove(e){if(!this.resizeState.isResizing)return;e.preventDefault();let i=("touches"in e?e.touches[0].clientX:e.clientX)-this.resizeState.startX,r=this._isRightSidebar()?this.resizeState.startWidth-i:this.resizeState.startWidth+i,o=this.resizeState.constrainedWidth(r);this._updateSidebarWidth(o),this._dispatchResizeEvent("move",o)}_onResizeEnd(){this.resizeState.isResizing&&(this.resizeState.isResizing=!1,this.layout.container.style.removeProperty("--_transition-duration"),this.layout.container.classList.remove(d.classes.RESIZING),document.documentElement.removeAttribute(`data-bslib-${d.classes.RESIZING}`),this._deactivateResizeHandle(),d.shinyResizeObserver.flush(),this._dispatchResizeEvent("end",this._getCurrentSidebarWidth()))}_onResizeKeyDown(e){if(!this._shouldEnableResize())return;let t=e.shiftKey?50:10,i=this._getCurrentSidebarWidth();switch(e.key){case"ArrowLeft":i=this._isRightSidebar()?i+t:i-t;break;case"ArrowRight":i=this._isRightSidebar()?i-t:i+t;break;case"Home":i=this.resizeState.minWidth;break;case"End":i=this.resizeState.maxWidth();break;default:return}e.preventDefault(),i=this.resizeState.constrainedWidth(i),this._updateSidebarWidth(i),d.shinyResizeObserver.flush(),this._dispatchResizeEvent("keyboard",i)}_getCurrentSidebarWidth(){return this.layout.sidebar.getBoundingClientRect().width||250}_updateSidebarWidth(e){let{container:t,resizeHandle:i}=this.layout;t.style.setProperty("--_sidebar-width",`${e}px`),i&&(i.setAttribute("aria-valuenow",e.toString()),i.setAttribute("aria-valuemin",this.resizeState.minWidth.toString()),i.setAttribute("aria-valuemax",this.resizeState.maxWidth().toString()))}_isRightSidebar(){return this.layout.container.classList.contains("sidebar-right")}_onResizeHandlePointerMove(e){if(this.resizeState.isResizing)return;let t=this.layout.resizeHandle;if(!t)return;if(!this.resizeHandleActivated){let n=this.layout.sidebar.getBoundingClientRect(),r=this._isRightSidebar()?n.left:n.right;Math.abs(e.clientX-r)<=2&&(this.resizeHandleActivated=!0,this.resizeHandleEngagementX=e.clientX,this.resizeHandlePeakDx=0,t.classList.add(d.classes.HANDLE_ACTIVE));return}let i=e.clientX-this.resizeHandleEngagementX;Math.abs(i)>Math.abs(this.resizeHandlePeakDx)&&(this.resizeHandlePeakDx=i),Math.abs(this.resizeHandlePeakDx)>3&&Math.sign(i)!==Math.sign(this.resizeHandlePeakDx)&&this._deactivateResizeHandle()}_deactivateResizeHandle(){var e;this.resizeHandleActivated=!1,this.resizeHandlePeakDx=0,(e=this.layout.resizeHandle)==null||e.classList.remove(d.classes.HANDLE_ACTIVE)}_onResizeHandlePointerLeave(){this.resizeState.isResizing||this._deactivateResizeHandle()}_updateResizeAvailability(){if(!this.layout.resizeHandle)return;let e=this._shouldEnableResize();this.layout.resizeHandle.style.display=e?"":"none",this.layout.resizeHandle.setAttribute("aria-hidden",e?"false":"true"),e?this.layout.resizeHandle.setAttribute("tabindex","0"):this.layout.resizeHandle.removeAttribute("tabindex")}_dispatchResizeEvent(e,t){let i=new CustomEvent("bslib.sidebar.resize",{bubbles:!0,detail:{phase:e,width:t,sidebar:this}});this.layout.sidebar.dispatchEvent(i)}_initEventListeners(){var t;let{toggle:e}=this.layout;e.addEventListener("click",i=>{i.preventDefault(),this.toggle("toggle")}),(t=e.querySelector(".collapse-icon"))==null||t.addEventListener("transitionend",()=>{this._finalizeState()}),!(this._isCollapsible("desktop")&&this._isCollapsible("mobile"))&&window.addEventListener("resize",ue(()=>this._getWindowSize(),()=>this._initSidebarState()))}_initSidebarCounters(){let{container:e}=this.layout,t=`.${d.classes.LAYOUT}> .main > .${d.classes.LAYOUT}:not([data-bslib-sidebar-open="always"])`;if(!(e.querySelector(t)===null))return;function n(a){return a=a?a.parentElement:null,a&&a.classList.contains("main")&&(a=a.parentElement),a&&a.classList.contains(d.classes.LAYOUT)?a:null}let r=[e],o=n(e);for(;o;)r.unshift(o),o=n(o);let c={left:0,right:0};r.forEach(function(a){let C=a.classList.contains("sidebar-right")?c.right++:c.left++;a.style.setProperty("--_js-toggle-count-this-side",C.toString()),a.style.setProperty("--_js-toggle-count-max-side",Math.max(c.right,c.left).toString())})}_getWindowSize(){let{container:e}=this.layout;return window.getComputedStyle(e).getPropertyValue("--bslib-sidebar-js-window-size").trim()}_initialToggleState(){var n,r;let{container:e}=this.layout,t=this.windowSize==="desktop"?"openDesktop":"openMobile",i=(r=(n=e.dataset[t])==null?void 0:n.trim())==null?void 0:r.toLowerCase();return i===void 0||["open","always"].includes(i)?"open":["close","closed"].includes(i)?"close":"open"}_initSidebarState(){this.windowSize=this._getWindowSize();let e=this._initialToggleState();this.toggle(e,!0)}toggle(e,t=!1){typeof e=="undefined"?e="toggle":e==="closed"&&(e="close");let{container:i,sidebar:n}=this.layout,r=this.isClosed;if(["open","close","toggle"].indexOf(e)===-1)throw new Error(`Unknown method ${e}`);if(e==="toggle"&&(e=r?"open":"close"),r&&e==="close"||!r&&e==="open"){t&&this._finalizeState();return}e==="open"&&(n.hidden=!1),i.classList.toggle(d.classes.TRANSITIONING,!t),i.classList.toggle(d.classes.COLLAPSE),t&&this._finalizeState()}_finalizeState(){let{container:e,sidebar:t,toggle:i}=this.layout;e.classList.remove(d.classes.TRANSITIONING),t.hidden=this.isClosed,i.setAttribute("aria-expanded",this.isClosed?"false":"true"),this._updateResizeAvailability();let n=new CustomEvent("bslib.sidebar",{bubbles:!0,detail:{open:!this.isClosed}});t.dispatchEvent(n),$(t).trigger("toggleCollapse.sidebarInputBinding"),$(t).trigger(this.isClosed?"hidden":"shown")}},T=d;T.shinyResizeObserver=new A,T.classes={LAYOUT:"bslib-sidebar-layout",COLLAPSE:"sidebar-collapsed",TRANSITIONING:"transitioning",RESIZE_HANDLE:"bslib-sidebar-resize-handle",RESIZING:"sidebar-resizing",HANDLE_ACTIVE:"handle-active"},T.onReadyScheduled=!1,T.instanceMap=new WeakMap;j=class extends f{find(e){return $(e).find(`.${T.classes.LAYOUT} > .bslib-sidebar-input`)}getValue(e){let t=T.getInstance(e.parentElement);return t?!t.isClosed:!1}setValue(e,t){let i=t?"open":"close";this.receiveMessage(e,{method:i})}subscribe(e,t){$(e).on("toggleCollapse.sidebarInputBinding",function(i){t(!0)})}unsubscribe(e){$(e).off(".sidebarInputBinding")}receiveMessage(e,t){let i=T.getInstance(e.parentElement);i&&i.toggle(t.method)}};E(j,"sidebar");N("Sidebar",T)});var z,I,P,Z,Y,be=v(()=>{"use strict";y();Y=class extends f{constructor(){super(...arguments);L(this,P);L(this,z,new WeakMap);L(this,I,new WeakMap)}find(t){return $(t).find(".bslib-task-button")}getValue(t){var i;return{value:(i=m(this,z).get(t))!=null?i:0,autoReset:t.hasAttribute("data-auto-reset")}}getType(){return"bslib.taskbutton"}subscribe(t,i){m(this,I).has(t)&&this.unsubscribe(t);let n=()=>{var r;m(this,z).set(t,((r=m(this,z).get(t))!=null?r:0)+1),i(!0),_(this,P,Z).call(this,t,"busy")};m(this,I).set(t,n),t.addEventListener("click",n)}unsubscribe(t){let i=m(this,I).get(t);i&&t.removeEventListener("click",i)}receiveMessage(n,r){return h(this,arguments,function*(t,{state:i}){_(this,P,Z).call(this,t,i)})}};z=new WeakMap,I=new WeakMap,P=new WeakSet,Z=function(t,i){t.disabled=i==="busy";let n=t.querySelector("bslib-switch-inline");n&&(n.case=i)};E(Y,"task-button")});var R,x,q,pe,J,me=v(()=>{"use strict";y();J=class extends f{constructor(){super(...arguments);L(this,q);L(this,R,new WeakMap);L(this,x,new WeakMap)}find(t){return $(t).find(".bslib-toolbar-input-button")}getValue(t){var i;return(i=m(this,R).get(t))!=null?i:0}getType(){return"bslib.toolbar.button"}subscribe(t,i){m(this,x).has(t)&&this.unsubscribe(t);let n=()=>{var r;m(this,R).set(t,((r=m(this,R).get(t))!=null?r:0)+1),_(this,q,pe).call(this,t),i(!0)};m(this,x).set(t,n),t.addEventListener("click",n)}unsubscribe(t){let i=m(this,x).get(t);i&&t.removeEventListener("click",i)}receiveMessage(t,i){return h(this,null,function*(){if(u(i,"disabled")&&(t.disabled=i.disabled),u(i,"label")&&i.label!==void 0){let n=t.querySelector(".bslib-toolbar-label");yield b(n,i.label)}if(u(i,"showLabel")){let n=t.querySelector(".bslib-toolbar-label");i.showLabel===!1?(n.setAttribute("hidden",""),t.setAttribute("data-type","icon")):(n.removeAttribute("hidden"),t.setAttribute("data-type","both"))}if(u(i,"icon")&&i.icon!==void 0){let n=t.querySelector(".bslib-toolbar-icon");yield b(n,i.icon)}})}};R=new WeakMap,x=new WeakMap,q=new WeakSet,pe=function(t){let i=t.closest("bslib-tooltip");i&&i.hide()};E(J,"toolbar-input-button")});var U,fe,Q,ve=v(()=>{"use strict";y();Q=class extends f{constructor(){super(...arguments);L(this,U)}find(t){return $(t).find(".bslib-toolbar-input-select")}getId(t){return t.id||""}getValue(t){let i=t.querySelector("select");return i==null?void 0:i.value}subscribe(t,i){let n=t.querySelector("select");n&&$(n).on("change.bslibToolbarInputSelect",()=>{_(this,U,fe).call(this,t),i(!1)})}unsubscribe(t){let i=t.querySelector("select");i&&$(i).off(".bslibToolbarInputSelect")}receiveMessage(t,i){return h(this,null,function*(){let n=t.querySelector("select");if(u(i,"label")&&i.label!==void 0){let r=t.querySelector(".bslib-toolbar-label");yield b(r,i.label)}if(u(i,"showLabel")){let r=t.querySelector(".bslib-toolbar-label");i.showLabel===!1?r.classList.add("visually-hidden"):r.classList.remove("visually-hidden")}if(u(i,"icon")&&i.icon!==void 0){let r=t.querySelector(".bslib-toolbar-icon");yield b(r,i.icon)}u(i,"options")&&n&&i.options&&(n.innerHTML=i.options),u(i,"value")&&n&&i.value!==void 0&&(n.value=i.value,$(n).trigger("change"))})}};U=new WeakSet,fe=function(t){let i=t.closest("bslib-tooltip");i&&i.hide()};E(Q,"toolbar-input-select")});function O(s){if(window.Shiny)for(let[e,t]of Object.entries(s))window.Shiny.addCustomMessageHandler(e,t)}var V=v(()=>{"use strict"});function _e(s){return h(this,null,function*(){let e=document.getElementById(s.id);if(e){if(u(s,"label")&&s.label!==void 0){let t=e.querySelector(".bslib-toolbar-label");t&&(yield b(t,s.label))}if(u(s,"icon")&&s.icon!==void 0){let t=e.querySelector(".bslib-toolbar-icon");t&&(yield b(t,s.icon))}if(u(s,"showLabel")){let t=e.querySelector(".bslib-toolbar-label");t&&(s.showLabel===!1?t.setAttribute("hidden",""):t.removeAttribute("hidden"))}u(s,"color")&&s.color!==void 0&&(e.classList.remove(...we),e.classList.add(`text-bg-${s.color}`)),u(s,"pill")&&(s.pill?e.classList.add("rounded-pill"):e.classList.remove("rounded-pill"))}})}var we,ge=v(()=>{"use strict";V();y();we=["text-bg-primary","text-bg-secondary","text-bg-success","text-bg-danger","text-bg-warning","text-bg-info","text-bg-light","text-bg-dark"];O({"bslib.update-toolbar-badge":_e})});function ye(s){let e=X(s),t=!s.value;e.classList.toggle("disabled",t),e.setAttribute("aria-disabled",t.toString()),t?e.setAttribute("tabindex","-1"):e.removeAttribute("tabindex")}function te(s){s.scrollHeight!==0&&(s.style.height="auto",s.style.height=s.scrollHeight+"px")}function Ce(s){if(!s.hasAttribute("data-needs-modifier"))return;let e=X(s);if(!e.querySelector(`.${H.submitKey}`))return;let t=navigator.userAgent.indexOf("Mac")!==-1;e.querySelectorAll(`.${H.submitKey}`).forEach(r=>{let o=t?"\u2318":"Ctrl";r.textContent=`${o} \u23CE`});let i=t?"Command":"Ctrl";e.title=e.title.replace("Press Enter",`Press ${i}+Enter`);let n=e.getAttribute("aria-label");n&&e.setAttribute("aria-label",n.replace("Press Enter",`Press ${i}+Enter`))}function X(s){var t;let e=(t=s.parentElement)==null?void 0:t.querySelector(`.${H.button}`);if(e instanceof HTMLButtonElement)return e;throw new Error("Expected input_submit_textarea()'s container to have a button with class of 'bslib-submit-textarea-btn'")}function Ae(s){let e=s.selectionStart,t=s.selectionEnd;s.value=s.value.substring(0,e)+` +`+s.value.substring(t),s.selectionStart=s.selectionEnd=e+1,s.dispatchEvent(new Event("input",{bubbles:!0}))}var M,H,Ee,ee,Le=v(()=>{"use strict";y();M="textSubmitInputBinding",H={input:"bslib-input-submit-textarea",container:"bslib-submit-textarea-container",button:"bslib-submit-textarea-btn",submitKey:"bslib-submit-key"},Ee=new IntersectionObserver(s=>{s.forEach(e=>{e.isIntersecting&&te(e.target)})}),ee=class extends f{find(e){return $(e).find(`.${H.input} textarea`)}initialize(e){ye(e),te(e),Ce(e)}getValue(e){return $(e).data("val")}setValue(e,t){e.value=t}subscribe(e,t){function i(){$(e).data("val",e.value),e.value="",e.dispatchEvent(new Event("input",{bubbles:!0})),t("event")}let n=X(e);n.classList.contains("shiny-bound-input")?$(n).on(`shiny:inputchanged.${M}`,i):$(n).on(`click.${M}`,i),$(e).on(`input.${M}`,function(){ye(e),te(e)}),$(e).on(`keydown.${M}`,function(o){if(o.key!=="Enter")return;if(!e.value){o.preventDefault();return}if(o.shiftKey)return;if(o.altKey){o.preventDefault(),Ae(e);return}let c=e.hasAttribute("data-needs-modifier");if(!c){o.preventDefault(),n.click();return}let a=o.ctrlKey||o.metaKey;if(c&&a){o.preventDefault(),n.click();return}});let r=e.closest(`.${H.container}`);$(r).on(`click.${M}`,o=>{o.target.classList.contains(H.container)&&e.focus()}),Ee.observe(e)}unsubscribe(e){$(e).off(`.${M}`);let t=e.nextElementSibling;$(t).off(`.${M}`);let i=e.closest(`.${H.container}`);$(i).off(`.${M}`),Ee.unobserve(e)}receiveMessage(e,t){return h(this,null,function*(){let i=e.value;if(t.value!==void 0&&(e.value=t.value,e.dispatchEvent(new Event("input",{bubbles:!0}))),t.placeholder!==void 0&&(e.placeholder=t.placeholder),t.label!==void 0){let n=$(e).closest(`.${H.input}`).find("label");yield ae(t.label,n)}t.submit&&(X(e).click(),e.value=i),t.focus&&e.focus()})}};E(ee,"submit-text-area")});function Re(s){return h(this,null,function*(){var B,se;let{html:e,deps:t,autohide:i,duration:n,position:r,id:o}=s;if(!window.bootstrap||!window.bootstrap.Toast){F({headline:"Bootstrap 5 Required",message:"Toast notifications require Bootstrap 5.",status:"error"});return}let c=document.getElementById(o);if(c){let w=D.get(c);w&&(w.hide(),D.delete(c)),(se=(B=window==null?void 0:window.Shiny)==null?void 0:B.unbindAll)==null||se.call(B,c),c.remove()}let a=Ie.getOrCreateToaster(r);yield b(a,{html:e,deps:t},"beforeEnd");let p=document.getElementById(o);if(!p){F({headline:"Toast Creation Failed",message:`Failed to create toast with id "${o}".`,status:"error"});return}let C=new ie(p,{autohide:i,duration:n});D.set(p,C),C.show(),p.addEventListener("hidden.bs.toast",()=>{var w,ne;(ne=(w=window==null?void 0:window.Shiny)==null?void 0:w.unbindAll)==null||ne.call(w,p),p.remove(),D.delete(p),a.children.length===0&&a.remove()})})}function xe(s){let{id:e}=s,t=document.getElementById(e);if(!t){F({headline:"Toast Not Found",message:`No toast with id "${e}" was found.`,status:"warning"});return}let i=D.get(t);i&&i.hide()}var ze,k,Ie,ie,D,Te=v(()=>{"use strict";V();y();ze=window.bootstrap?window.bootstrap.Toast:class{},k=class{constructor(){this.containers=new Map}getOrCreateToaster(e){let t=this.containers.get(e);return(!t||!document.body.contains(t))&&(t=k._createToaster(e),document.body.appendChild(t),this.containers.set(e,t)),t}static _createToaster(e){let t=document.createElement("div");return t.className="toast-container position-fixed p-1 p-md-2",t.setAttribute("data-bslib-toast-container",e),t.classList.add(...k._positionClasses(e)),t}static _positionClasses(e){return{"top-left":["top-0","start-0"],"top-center":["top-0","start-50","translate-middle-x"],"top-right":["top-0","end-0"],"middle-left":["top-50","start-0","translate-middle-y"],"middle-center":["top-50","start-50","translate-middle"],"middle-right":["top-50","end-0","translate-middle-y"],"bottom-left":["bottom-0","start-0"],"bottom-center":["bottom-0","start-50","translate-middle-x"],"bottom-right":["bottom-0","end-0"]}[e]}},Ie=new k,ie=class{constructor(e,t){this.progressBar=null;this.timeStart=0;this.timeRemaining=0;this.hideTimeoutId=null;this.isPaused=!1;this.isPointerOver=!1;this.hasFocus=!1;this.element=e,this.timeRemaining=t.duration||5e3;let i={animation:!0,autohide:!1};this.bsToast=new ze(e,i),t.autohide&&(this._addProgressBar(),this._setupInteractionPause())}show(){this.bsToast.show()}hide(){this.hideTimeoutId!==null&&(clearTimeout(this.hideTimeoutId),this.hideTimeoutId=null),this.bsToast.hide()}_addProgressBar(){this.progressBar=document.createElement("div"),this.progressBar.className="bslib-toast-progress-bar",this.progressBar.style.cssText=` animation: bslib-toast-progress ${this.timeRemaining}ms linear forwards; animation-play-state: running; - `;let e=this.element.querySelector(".toast-header");e?e.insertBefore(this.progressBar,e.firstChild):this.element.insertBefore(this.progressBar,this.element.firstChild)}_setupInteractionPause(){this.timeStart=Date.now(),this._startHideTimeout(this.timeRemaining),this.element.addEventListener("pointerenter",()=>this._handlePointerEnter()),this.element.addEventListener("pointerleave",()=>this._handlePointerLeave()),this.element.addEventListener("focusin",()=>this._handleFocusIn()),this.element.addEventListener("focusout",()=>this._handleFocusOut())}_handlePointerEnter(){this.isPointerOver=!0,this._pause()}_handlePointerLeave(){this.isPointerOver=!1,this.hasFocus||this._resume()}_handleFocusIn(){this.hasFocus=!0,this._pause()}_handleFocusOut(){this.hasFocus=!1,this.isPointerOver||this._resume()}_pause(){if(this.isPaused)return;this.isPaused=!0;let e=Date.now()-this.timeStart;this.timeRemaining=Math.max(100,this.timeRemaining-e),this.hideTimeoutId!==null&&clearTimeout(this.hideTimeoutId),this.progressBar&&(this.progressBar.style.animationPlayState="paused")}_resume(){this.isPaused&&(this.isPaused=!1,this.timeStart=Date.now(),this._startHideTimeout(this.timeRemaining),this.progressBar&&(this.progressBar.style.animationPlayState="running"))}_startHideTimeout(e){this.hideTimeoutId!==null&&clearTimeout(this.hideTimeoutId),this.hideTimeoutId=window.setTimeout(()=>{this.bsToast.hide()},e)}},P=new WeakMap;V({"bslib.show-toast":ze,"bslib.hide-toast":Ae})});var Re=Me(Te=>{le();ce();he();be();me();ve();ye();Le();L();te();var Ie={"bslib.toggle-input-binary":n=>u(Te,null,function*(){let e=document.getElementById(n.id);e||console.warn("[bslib.toggle-input-binary] No element found",n);let t=$(e).data("shiny-input-binding");if(!(t instanceof f)){console.warn("[bslib.toggle-input-binary] No input binding found",n);return}let i=n.value;typeof i=="undefined"&&(i=!t.getValue(e)),yield t.receiveMessage(e,{value:i})})};window.Shiny&&V(Ie);function Se(){let n=document.createElement("div");n.innerHTML=` + `;let e=this.element.querySelector(".toast-header");e?e.insertBefore(this.progressBar,e.firstChild):this.element.insertBefore(this.progressBar,this.element.firstChild)}_setupInteractionPause(){this.timeStart=Date.now(),this._startHideTimeout(this.timeRemaining),this.element.addEventListener("pointerenter",()=>this._handlePointerEnter()),this.element.addEventListener("pointerleave",()=>this._handlePointerLeave()),this.element.addEventListener("focusin",()=>this._handleFocusIn()),this.element.addEventListener("focusout",()=>this._handleFocusOut())}_handlePointerEnter(){this.isPointerOver=!0,this._pause()}_handlePointerLeave(){this.isPointerOver=!1,this.hasFocus||this._resume()}_handleFocusIn(){this.hasFocus=!0,this._pause()}_handleFocusOut(){this.hasFocus=!1,this.isPointerOver||this._resume()}_pause(){if(this.isPaused)return;this.isPaused=!0;let e=Date.now()-this.timeStart;this.timeRemaining=Math.max(100,this.timeRemaining-e),this.hideTimeoutId!==null&&clearTimeout(this.hideTimeoutId),this.progressBar&&(this.progressBar.style.animationPlayState="paused")}_resume(){this.isPaused&&(this.isPaused=!1,this.timeStart=Date.now(),this._startHideTimeout(this.timeRemaining),this.progressBar&&(this.progressBar.style.animationPlayState="running"))}_startHideTimeout(e){this.hideTimeoutId!==null&&clearTimeout(this.hideTimeoutId),this.hideTimeoutId=window.setTimeout(()=>{this.bsToast.hide()},e)}},D=new WeakMap;O({"bslib.show-toast":Re,"bslib.hide-toast":xe})});var ke=He(Me=>{le();ce();he();be();me();ve();ge();Le();Te();y();V();var Oe={"bslib.toggle-input-binary":s=>h(Me,null,function*(){let e=document.getElementById(s.id);e||console.warn("[bslib.toggle-input-binary] No element found",s);let t=$(e).data("shiny-input-binding");if(!(t instanceof f)){console.warn("[bslib.toggle-input-binary] No input binding found",s);return}let i=s.value;typeof i=="undefined"&&(i=!t.getValue(e)),yield t.receiveMessage(e,{value:i})})};window.Shiny&&O(Oe);function Se(){let s=document.createElement("div");s.innerHTML=` `,document.body.appendChild(n.children[0])}document.readyState==="complete"?Se():document.addEventListener("DOMContentLoaded",Se)});Re();})(); + `,document.body.appendChild(s.children[0])}document.readyState==="complete"?Se():document.addEventListener("DOMContentLoaded",Se)});ke();})(); //# sourceMappingURL=components.min.js.map diff --git a/srcts/src/components/index.ts b/srcts/src/components/index.ts index 86b6f67e9..a282c508a 100644 --- a/srcts/src/components/index.ts +++ b/srcts/src/components/index.ts @@ -7,6 +7,7 @@ import "./sidebar"; import "./taskButton"; import "./toolbarInputButton"; import "./toolbarInputSelect"; +import "./toolbarBadge"; import "./submitTextArea"; import "./toast"; diff --git a/srcts/src/components/toolbarBadge.ts b/srcts/src/components/toolbarBadge.ts new file mode 100644 index 000000000..5807596c7 --- /dev/null +++ b/srcts/src/components/toolbarBadge.ts @@ -0,0 +1,69 @@ +import { shinyAddCustomMessageHandlers } from "./_shinyAddCustomMessageHandlers"; +import { shinyRenderContent, hasDefinedProperty } from "./_utils"; +import type { HtmlDep } from "./_utils"; + +interface UpdateToolbarBadgeMessage { + id: string; + label?: string | { html: string; deps: HtmlDep[] }; + icon?: string | { html: string; deps: HtmlDep[] }; + showLabel?: boolean; + color?: string; + pill?: boolean; +} + +const badgeColorClasses = [ + "text-bg-primary", + "text-bg-secondary", + "text-bg-success", + "text-bg-danger", + "text-bg-warning", + "text-bg-info", + "text-bg-light", + "text-bg-dark", +]; + +async function updateToolbarBadge( + message: UpdateToolbarBadgeMessage +): Promise { + const el = document.getElementById(message.id); + if (!el) return; + + if (hasDefinedProperty(message, "label") && message.label !== undefined) { + const labelEl = el.querySelector(".bslib-toolbar-label") as HTMLElement; + if (labelEl) await shinyRenderContent(labelEl, message.label); + } + + if (hasDefinedProperty(message, "icon") && message.icon !== undefined) { + const iconEl = el.querySelector(".bslib-toolbar-icon") as HTMLElement; + if (iconEl) await shinyRenderContent(iconEl, message.icon); + } + + if (hasDefinedProperty(message, "showLabel")) { + const labelEl = el.querySelector(".bslib-toolbar-label") as HTMLElement; + if (labelEl) { + if (message.showLabel === false) { + labelEl.setAttribute("hidden", ""); + } else { + labelEl.removeAttribute("hidden"); + } + } + } + + if (hasDefinedProperty(message, "color") && message.color !== undefined) { + el.classList.remove(...badgeColorClasses); + el.classList.add(`text-bg-${message.color}`); + } + + if (hasDefinedProperty(message, "pill")) { + if (message.pill) { + el.classList.add("rounded-pill"); + } else { + el.classList.remove("rounded-pill"); + } + } +} + +shinyAddCustomMessageHandlers({ + // eslint-disable-next-line @typescript-eslint/naming-convention + "bslib.update-toolbar-badge": updateToolbarBadge, +}); From 69c5cd411119c895396db8bf0019cb0df62db7de Mon Sep 17 00:00:00 2001 From: E Nelson Date: Tue, 26 May 2026 13:03:35 -0400 Subject: [PATCH 10/22] feat: add toolbar_badge() to toolbar example app --- inst/examples-shiny/toolbar/app.R | 44 ++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/inst/examples-shiny/toolbar/app.R b/inst/examples-shiny/toolbar/app.R index f02fa2aa6..f5b62c0ee 100644 --- a/inst/examples-shiny/toolbar/app.R +++ b/inst/examples-shiny/toolbar/app.R @@ -51,7 +51,12 @@ ui <- page_navbar( card_footer( toolbar( align = "left", - "Showing 10 of 247 records" + toolbar_badge( + id = "record_count", + label = "Showing 10 of 247 records", + color = "secondary", + pill = TRUE + ) ), toolbar( align = "right", @@ -184,6 +189,13 @@ ui <- page_navbar( "Recent Activity", toolbar( align = "right", + toolbar_badge( + id = "activity_status", + label = "Live", + color = "success", + pill = TRUE + ), + toolbar_divider(), toolbar_input_button( id = "activity_refresh", label = "Refresh", @@ -535,6 +547,17 @@ server <- function(input, output, session) { head(filtered_sales(), 10) }) + # Update the record count badge whenever the filter changes + observe({ + total <- nrow(filtered_sales()) + shown <- min(10L, total) + update_toolbar_badge( + "record_count", + label = sprintf("Showing %d of %d records", shown, total), + color = if (total == nrow(sales_data)) "secondary" else "primary" + ) + }) + # Stats that update based on filter output$stats <- renderText({ data <- filtered_sales() @@ -715,11 +738,26 @@ server <- function(input, output, session) { chart_color(input$color_scheme) }) - # Activity feed controls + # Activity feed controls — cycle status badge through states on each refresh + activity_refresh_count <- reactiveVal(0L) + observeEvent(input$activity_refresh, { + n <- activity_refresh_count() + 1L + activity_refresh_count(n) + + # Cycle: Live (success) -> Updating (warning) -> Stale (secondary) -> Live + states <- list( + list(label = "Live", color = "success"), + list(label = "Updating", color = "warning"), + list(label = "Stale", color = "secondary") + ) + state <- states[[(n %% length(states)) + 1L]] + + update_toolbar_badge("activity_status", label = state$label, color = state$color) + show_toast( toast( - "Activity refreshed!", + paste("Activity status:", state$label), type = "info", duration_s = 2 ) From 1df49b1444224997656a3fa9d6c767fa9a9b7ba2 Mon Sep 17 00:00:00 2001 From: E Nelson Date: Tue, 26 May 2026 13:05:15 -0400 Subject: [PATCH 11/22] news: document toolbar_badge() addition (#1316) --- NEWS.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/NEWS.md b/NEWS.md index 302c24eab..b88c92870 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,11 @@ # bslib (development version) +## New features + +* Added `toolbar_badge()` for displaying status text and icons in a `toolbar()`. + Use `update_toolbar_badge()` to update the badge label, icon, color, and pill + style from the server. (#1316) + # bslib 0.11.0 ## New features From 825791a55f638573a4060aaa2350424525f6d637 Mon Sep 17 00:00:00 2001 From: E Nelson Date: Tue, 26 May 2026 13:12:29 -0400 Subject: [PATCH 12/22] chore: commit regenerated JS bundles and man pages --- inst/components/dist/code-editor.js | 2 +- inst/components/dist/code-editor.min.js | 2 +- inst/components/dist/components.js.map | 6 +++--- inst/components/dist/components.min.js.map | 8 ++++---- inst/components/dist/web-components.js | 2 +- inst/components/dist/web-components.min.js | 2 +- man/navbar_options.Rd | 2 +- man/toolbar.Rd | 1 + man/toolbar_divider.Rd | 1 + man/toolbar_input_button.Rd | 1 + man/toolbar_input_select.Rd | 1 + 11 files changed, 16 insertions(+), 12 deletions(-) diff --git a/inst/components/dist/code-editor.js b/inst/components/dist/code-editor.js index 2c9a753e1..dd87ac4b5 100644 --- a/inst/components/dist/code-editor.js +++ b/inst/components/dist/code-editor.js @@ -1,4 +1,4 @@ -/*! bslib 0.11.0 | (c) 2012-2026 RStudio, PBC. | License: MIT + file LICENSE */ +/*! bslib 0.11.0.9000 | (c) 2012-2026 RStudio, PBC. | License: MIT + file LICENSE */ var __accessCheck = (obj, member, msg) => { if (!member.has(obj)) throw TypeError("Cannot " + msg); diff --git a/inst/components/dist/code-editor.min.js b/inst/components/dist/code-editor.min.js index 9467a4b80..6afb1069b 100644 --- a/inst/components/dist/code-editor.min.js +++ b/inst/components/dist/code-editor.min.js @@ -1,3 +1,3 @@ -/*! bslib 0.11.0 | (c) 2012-2026 RStudio, PBC. | License: MIT + file LICENSE */ +/*! bslib 0.11.0.9000 | (c) 2012-2026 RStudio, PBC. | License: MIT + file LICENSE */ var O=(n,r,e)=>{if(!r.has(n))throw TypeError("Cannot "+e)};var p=(n,r,e)=>(O(n,r,"read from private field"),e?e.call(n):r.get(n)),b=(n,r,e)=>{if(r.has(n))throw TypeError("Cannot add the same private member more than once");r instanceof WeakSet?r.add(n):r.set(n,e)},U=(n,r,e,t)=>(O(n,r,"write to private field"),t?t.call(n,e):r.set(n,e),e);var m=(n,r,e)=>(O(n,r,"access private method"),e);var h=(n,r,e)=>new Promise((t,i)=>{var s=a=>{try{l(e.next(a))}catch(g){i(g)}},u=a=>{try{l(e.throw(a))}catch(g){i(g)}},l=a=>a.done?t(a.value):Promise.resolve(a.value).then(s,u);l((e=e.apply(n,r)).next())});function R(n,{type:r=null}={}){if(!window.Shiny)return;class e extends window.Shiny.InputBinding{constructor(){super()}find(i){return $(i).find(n)}getValue(i){return"getValue"in i?i.getValue():i.value}getType(i){return r}subscribe(i,s){i.onChangeCallback=s}unsubscribe(i){i.onChangeCallback=s=>{}}receiveMessage(i,s){i.receiveMessage(i,s)}}window.Shiny.inputBindings.register(new e,`${n}-Binding`)}var E=window.Shiny,ee=E?E.InputBinding:class{};function P({headline:n="",message:r,status:e="warning"}){document.dispatchEvent(new CustomEvent("shiny:client-message",{detail:{headline:n,message:r,status:e}}))}function c(n,r){return Object.prototype.hasOwnProperty.call(n,r)&&n[r]!==void 0}function X(...n){return h(this,null,function*(){if(!E)throw new Error("This function must be called in a Shiny app.");return E.renderContentAsync?yield E.renderContentAsync.apply(null,n):yield E.renderContent.apply(null,n)})}function j(n,r){return h(this,null,function*(){if(typeof n!="undefined"){if(r.length!==1)throw new Error("labelNode must be of length 1");typeof n=="string"&&(n={html:n,deps:[]}),n.html===""?r.addClass("shiny-label-null"):(yield X(r,n),r.removeClass("shiny-label-null"))}})}var q="plain",z=2,J="github-light",Q="github-dark",V=400,S,w,y,f,L,M,_,C,x,o=class extends HTMLElement{constructor(){super(...arguments);this.onChangeCallback=()=>{}}get language(){var e;return(e=this.getAttribute("language"))!=null?e:q}set language(e){this.setAttribute("language",e)}get readonly(){return this.hasAttribute("readonly")&&this.getAttribute("readonly")!=="false"}set readonly(e){this.setAttribute("readonly",String(e))}get lineNumbers(){return this.getAttribute("line-numbers")!=="false"}set lineNumbers(e){this.setAttribute("line-numbers",String(e))}get wordWrap(){return this.getAttribute("word-wrap")==="true"}set wordWrap(e){this.setAttribute("word-wrap",String(e))}get tabSize(){let e=this.getAttribute("tab-size"),t=e?parseInt(e):z;return isNaN(t)?z:t}set tabSize(e){this.setAttribute("tab-size",String(e))}get insertSpaces(){return this.getAttribute("insert-spaces")!=="false"}set insertSpaces(e){this.setAttribute("insert-spaces",String(e))}get themeLight(){var e;return(e=this.getAttribute("theme-light"))!=null?e:J}set themeLight(e){this.setAttribute("theme-light",e)}get themeDark(){var e;return(e=this.getAttribute("theme-dark"))!=null?e:Q}set themeDark(e){this.setAttribute("theme-dark",e)}get value(){var e,t;return(t=(e=this.prismEditor)==null?void 0:e.value)!=null?t:""}set value(e){this.prismEditor&&this.prismEditor.setOptions({value:e})}getValue(){if(this.prismEditor)return this.value}connectedCallback(){if(this.prismEditor)return;this.initPromise=this._initializeEditor(),this.initPromise.then(()=>{this.onChangeCallback(!1)}).catch(t=>{P({headline:"Code Editor Initialization Error",message:"An error occurred while initializing the code editor. See console for details.",status:"error"}),console.error("Failed to initialize code editor:",t)});let e=()=>this.onChangeCallback(!0);this.addEventListener("bslibCodeEditorUpdate",e)}disconnectedCallback(){var e,t;(e=this.darkLightObserver)==null||e.disconnect(),this.darkLightObserver=void 0,(t=this.readonlyTooltipCleanup)==null||t.call(this),this.readonlyTooltipCleanup=void 0}attributeChangedCallback(e,t,i){var u,l;if(t===i||!this.prismEditor)return;let s=this.prismEditor;switch(e){case"language":i&&(this.languageChangePromise=this._handleLanguageChange(i));break;case"readonly":{let a=i==="true";s.setOptions({readOnly:a}),a&&!this.readonlyTooltipCleanup?this._setupReadOnlyTooltip(s):!a&&this.readonlyTooltipCleanup&&(this.readonlyTooltipCleanup(),this.readonlyTooltipCleanup=void 0);break}case"line-numbers":s.setOptions({lineNumbers:i!=="false"});break;case"word-wrap":s.setOptions({wordWrap:i==="true"});break;case"tab-size":{let a=i?parseInt(i):z;isNaN(a)||s.setOptions({tabSize:a});break}case"insert-spaces":s.setOptions({insertSpaces:i!=="false"});break;case"theme-light":i&&m(u=o,C,x).call(u,i);break;case"theme-dark":i&&m(l=o,C,x).call(l,i);break;default:break}}_initializeEditor(){return h(this,null,function*(){var D,B,N;let e=this.querySelector(".code-editor");if(!e){P({headline:"Code Editor Initialization Error",message:"Expected to find `.code-editor` inside `` container element.",status:"error"});return}let t=this.language,i=(D=this.getAttribute("value"))!=null?D:"";this.removeAttribute("value");let s=this.readonly,u=this.lineNumbers,l=this.wordWrap,a=this.tabSize,g=this.insertSpaces,v=m(B=o,f,L).call(B);yield m(N=o,M,_).call(N,t);let[{createEditor:T},{copyButton:F},{defaultCommands:W}]=yield Promise.all([import(`${v}/index.js`),import(`${v}/extensions/copyButton/index.js`),import(`${v}/extensions/commands.js`)]),k=T(e,{language:t,value:i,tabSize:a,insertSpaces:g,lineNumbers:u,wordWrap:l,readOnly:s},F(),W()),A=k.keyCommandMap.Enter;k.keyCommandMap.Enter=(I,G,K)=>I.metaKey||I.ctrlKey?(this.dispatchEvent(new CustomEvent("bslibCodeEditorUpdate")),e.classList.add("code-editor-submit-flash"),setTimeout(()=>{e.classList.remove("code-editor-submit-flash")},V),!0):A==null?void 0:A(I,G,K),this.prismEditor=k,this.darkLightObserver=this._setupThemeWatcher();let H=this.querySelector("textarea");return H&&H.addEventListener("blur",()=>{this.dispatchEvent(new CustomEvent("bslibCodeEditorUpdate"))}),s&&this._setupReadOnlyTooltip(k),k})}_setupThemeWatcher(){let e=()=>{var l;let u=document.documentElement.getAttribute("data-bs-theme")==="dark"?this.themeDark:this.themeLight;m(l=o,C,x).call(l,u)};e();let t=new MutationObserver(()=>e());return t.observe(document.documentElement,{attributes:!0,attributeFilter:["data-bs-theme"]}),t}_handleLanguageChange(e){return h(this,null,function*(){var i;let t=this.prismEditor;if(t)try{yield m(i=o,M,_).call(i,e),t.setOptions({language:e}),t.update()}catch(s){P({headline:"Code Editor Language Load Error",message:`Failed to load language '${e}'. See console for details.`,status:"error"}),console.error(`Failed to load language '${e}':`,s)}})}_setupReadOnlyTooltip(e){return h(this,null,function*(){var t;try{let i=m(t=o,f,L).call(t),[{addTooltip:s},{cursorPosition:u}]=yield Promise.all([import(`${i}/tooltips.js`),import(`${i}/extensions/cursor.js`)]);u()(e);let l=document.createElement("div");l.className="code-editor-readonly-tooltip alert alert-danger",l.textContent="Cannot edit read-only editor.";let[a,g]=s(e,l,!1),v=()=>{this.classList.add("is-invalid"),a()},T=()=>{this.classList.remove("is-invalid"),g()};e.textarea.addEventListener("beforeinput",v,!0),e.on("selectionChange",T),e.textarea.addEventListener("click",T),this.readonlyTooltipCleanup=()=>{e.textarea.removeEventListener("beforeinput",v,!0),e.textarea.removeEventListener("click",T),g()}}catch(i){console.error("Failed to setup read-only tooltip:",i)}})}receiveMessage(e,t){return h(this,null,function*(){var i;if(this.initPromise&&(yield this.initPromise),!this.prismEditor){P({headline:"Code Editor could not update",message:"An update was ignored because the editor is not yet initialized.",status:"warning"});return}if(c(t,"value")&&(this.value=(i=t.value)!=null?i:""),c(t,"label")){let s=$(this).find("label");yield j(t.label,s)}c(t,"tab_size")&&t.tab_size!==void 0&&(this.tabSize=t.tab_size),c(t,"indentation")&&(this.insertSpaces=t.indentation==="space"),c(t,"read_only")&&t.read_only!==void 0&&(this.readonly=t.read_only),c(t,"line_numbers")&&t.line_numbers!==void 0&&(this.lineNumbers=t.line_numbers),c(t,"word_wrap")&&t.word_wrap!==void 0&&(this.wordWrap=t.word_wrap),c(t,"language")&&t.language&&(this.language=t.language,this.languageChangePromise&&(yield this.languageChangePromise,this.languageChangePromise=void 0)),c(t,"theme_light")&&t.theme_light&&(this.themeLight=t.theme_light),c(t,"theme_dark")&&t.theme_dark&&(this.themeDark=t.theme_dark),this.dispatchEvent(new CustomEvent("bslibCodeEditorUpdate"))})}},d=o;S=new WeakMap,w=new WeakMap,y=new WeakMap,f=new WeakSet,L=function(){if(p(o,y)!==null)return p(o,y);let e=document.querySelector('script[src*="prism-code-editor"][src$="index.js"]');if(!e)throw new Error("Could not find prism-code-editor script element. Ensure the prism-code-editor dependency is properly loaded.");let t=e.getAttribute("src");if(!t)throw new Error("prism-code-editor script element has no src attribute");let i=new URL(t,document.baseURI).href;return U(o,y,i.replace(/\/index\.js$/,"")),p(o,y)},M=new WeakSet,_=function(e){return h(this,null,function*(){var s;if(p(o,S).has(e)||["plain","plaintext","text","txt"].includes(e))return;let t=e;e==="html"&&(t="markup"),yield import(`${m(s=o,f,L).call(s)}/prism/languages/${t}.js`),p(o,S).add(e)})},C=new WeakSet,x=function(e){var u;if(p(o,w).has(e))return;let t=`code-editor-theme-${e}`;if(document.getElementById(t)){p(o,w).add(e);return}let i=m(u=o,f,L).call(u),s=document.createElement("link");s.id=t,s.rel="stylesheet",s.href=`${i}/themes/${e}.css`,s.addEventListener("load",()=>{p(o,w).add(e)}),s.addEventListener("error",()=>{console.error(`Failed to load code editor theme: ${e}`)}),document.head.appendChild(s)},b(d,f),b(d,M),b(d,C),d.tagName="bslib-code-editor",d.isShinyInput=!0,d.observedAttributes=["language","readonly","line-numbers","word-wrap","tab-size","insert-spaces","theme-light","theme-dark"],b(d,S,new Set),b(d,w,new Set),b(d,y,null);customElements.define(d.tagName,d);window.Shiny&&R(d.tagName);export{d as BslibCodeEditor}; //# sourceMappingURL=code-editor.min.js.map diff --git a/inst/components/dist/components.js.map b/inst/components/dist/components.js.map index dfe7db55e..e2ad07278 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/toolbarInputSelect.ts", "../../../srcts/src/components/submitTextArea.ts", "../../../srcts/src/components/_shinyAddCustomMessageHandlers.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 // eslint-disable-next-line @typescript-eslint/naming-convention\n HANDLE_ACTIVE: \"handle-active\",\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.sidebar.hasAttribute(\"data-resizable\")) return;\n\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 handle.addEventListener(\n \"mousemove\",\n this._onResizeHandlePointerMove.bind(this)\n );\n handle.addEventListener(\n \"mouseleave\",\n this._onResizeHandlePointerLeave.bind(this)\n );\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 // Fine pointers (mouse) must cross the handle midpoint before grabbing,\n // so that clicks on the sidebar scrollbar don't start a resize.\n if (!(\"touches\" in event) && !this.resizeHandleActivated) 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 // Reset handle activation state\n this._deactivateResizeHandle();\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 * Whether the resize handle has been activated by the mouse crossing the\n * sidebar's outer edge. This prevents clicks on the sidebar scrollbar\n * (which overlaps the handle) from starting a resize.\n * @private\n */\n private resizeHandleActivated = false;\n\n /**\n * The clientX where the handle was activated, used to detect when the mouse\n * reverses direction back past this point (which dismisses the handle).\n * @private\n */\n private resizeHandleEngagementX = 0;\n\n /**\n * The peak displacement from the engagement point, used to detect direction\n * reversal past the engagement point.\n * @private\n */\n private resizeHandlePeakDx = 0;\n\n /**\n * Track mouse movement over the resize handle to detect when the cursor\n * crosses the sidebar's outer edge, which activates the handle for grabbing.\n * After activation, dismisses if the mouse reverses back past the\n * engagement point.\n * @private\n * @param {MouseEvent} event\n */\n private _onResizeHandlePointerMove(event: MouseEvent): void {\n if (this.resizeState.isResizing) return;\n\n const handle = this.layout.resizeHandle;\n if (!handle) return;\n\n if (!this.resizeHandleActivated) {\n const sidebarRect = this.layout.sidebar.getBoundingClientRect();\n const midpoint = this._isRightSidebar()\n ? sidebarRect.left\n : sidebarRect.right;\n\n if (Math.abs(event.clientX - midpoint) <= 2) {\n this.resizeHandleActivated = true;\n this.resizeHandleEngagementX = event.clientX;\n this.resizeHandlePeakDx = 0;\n handle.classList.add(Sidebar.classes.HANDLE_ACTIVE);\n }\n return;\n }\n\n const dx = event.clientX - this.resizeHandleEngagementX;\n\n if (Math.abs(dx) > Math.abs(this.resizeHandlePeakDx)) {\n this.resizeHandlePeakDx = dx;\n }\n\n // Dismiss if mouse reversed direction back past the engagement point\n if (\n Math.abs(this.resizeHandlePeakDx) > 3 &&\n Math.sign(dx) !== Math.sign(this.resizeHandlePeakDx)\n ) {\n this._deactivateResizeHandle();\n }\n }\n\n /**\n * Remove the active state from the resize handle.\n * @private\n */\n private _deactivateResizeHandle(): void {\n this.resizeHandleActivated = false;\n this.resizeHandlePeakDx = 0;\n this.layout.resizeHandle?.classList.remove(Sidebar.classes.HANDLE_ACTIVE);\n }\n\n /**\n * Reset resize handle activation when the mouse leaves the handle.\n * @private\n */\n private _onResizeHandlePointerLeave(): void {\n if (this.resizeState.isResizing) return;\n this._deactivateResizeHandle();\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