From 4a4cc93af32726dd1bc87ac5fd4702a6c3384bdb Mon Sep 17 00:00:00 2001 From: Carson Date: Thu, 6 Oct 2022 10:45:46 -0500 Subject: [PATCH] Introducing card(), value_box(), and layout_column_wrap() --- .Rbuildignore | 1 + DESCRIPTION | 14 +- NAMESPACE | 12 + R/bs-theme.R | 12 +- R/card.R | 308 ++++++++ R/imports.R | 2 +- R/layout.R | 108 +++ R/navs.R | 92 ++- R/utils-tags.R | 52 +- R/value-box.R | 67 ++ README.Rmd | 11 - README.md | 4 - _pkgdown.yml | 38 +- inst/components/card-full-screen.js | 33 + inst/components/card.scss | 132 ++++ inst/components/tag-require.js | 14 + inst/components/value-box.scss | 83 +++ inst/components/vfill.scss | 20 + inst/examples/card/layout/app.R | 91 +++ inst/examples/value_box/app.R | 99 +++ man/card.Rd | 72 ++ man/card_body.Rd | 82 +++ man/layout_column_wrap.Rd | 73 ++ man/navs.Rd | 34 +- man/value_box.Rd | 61 ++ pkgdown/extra.css | 25 + vignettes/_variables-template.Rmd | 3 - vignettes/bs4-variables.Rmd | 3 - vignettes/bs5-variables.Rmd | 3 - vignettes/cards.Rmd | 692 ++++++++++++++++++ vignettes/shiny-hex.svg | 84 +++ vignettes/value-box.svg | 3 + vignettes/variables-table/variables-table.R | 5 +- vignettes/variables-table/variables-table.css | 14 - 34 files changed, 2252 insertions(+), 95 deletions(-) create mode 100644 R/card.R create mode 100644 R/layout.R create mode 100644 R/value-box.R create mode 100644 inst/components/card-full-screen.js create mode 100644 inst/components/card.scss create mode 100644 inst/components/tag-require.js create mode 100644 inst/components/value-box.scss create mode 100644 inst/components/vfill.scss create mode 100644 inst/examples/card/layout/app.R create mode 100644 inst/examples/value_box/app.R create mode 100644 man/card.Rd create mode 100644 man/card_body.Rd create mode 100644 man/layout_column_wrap.Rd create mode 100644 man/value_box.Rd create mode 100644 pkgdown/extra.css create mode 100644 vignettes/cards.Rmd create mode 100644 vignettes/shiny-hex.svg create mode 100644 vignettes/value-box.svg diff --git a/.Rbuildignore b/.Rbuildignore index 651d084d8..40232db19 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -13,6 +13,7 @@ inst/lib/bsw3/.npmignore docs sandbox revdep +pkgdown ^_pkgdown\.yml$ vignettes ^\.github$ diff --git a/DESCRIPTION b/DESCRIPTION index b69e24fb3..e95ef9d36 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -26,7 +26,9 @@ Imports: jquerylib (>= 0.1.3), rlang, cachem, - memoise + memoise, + base64enc, + mime Suggests: shiny (>= 1.6.0), rmarkdown (>= 2.7), @@ -36,7 +38,9 @@ Suggests: withr, rappdirs, curl, - magrittr + magrittr, + fontawesome, + bsicons License: MIT + file LICENSE Encoding: UTF-8 RoxygenNote: 7.2.1 @@ -52,9 +56,11 @@ Collate: 'bs-theme-preview.R' 'bs-theme-update.R' 'bs-theme.R' + 'card.R' 'deprecated.R' 'files.R' 'imports.R' + 'layout.R' 'nav-items.R' 'nav-update.R' 'navs-legacy.R' @@ -67,6 +73,7 @@ Collate: 'staticimports.R' 'utils-shiny.R' 'utils-tags.R' + 'value-box.R' 'version-default.R' 'versions.R' URL: https://rstudio.github.io/bslib/, https://github.com/rstudio/bslib @@ -82,7 +89,6 @@ Config/Needs/website: glue, purrr, rprojroot, - rstudio/quillt, stringr, tidyr Config/Needs/deploy: @@ -94,3 +100,5 @@ Config/Needs/deploy: reactable, rprojroot, rsconnect +Remotes: + rstudio/bsicons diff --git a/NAMESPACE b/NAMESPACE index ab86a7ae1..18c2cc6c9 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -2,6 +2,7 @@ S3method(print,bslib_fragment) S3method(print,bslib_page) +export(as.card_item) export(bootstrap) export(bootstrap_sass) export(bootswatch_themes) @@ -41,11 +42,20 @@ export(bs_theme_preview) export(bs_theme_set) export(bs_theme_update) export(bs_themer) +export(card) +export(card_body) +export(card_body_fill) +export(card_footer) +export(card_header) +export(card_image) +export(card_title) export(font_collection) export(font_face) export(font_google) export(font_link) +export(is.card_item) export(is_bs_theme) +export(layout_column_wrap) export(nav) export(nav_append) export(nav_content) @@ -74,6 +84,7 @@ export(precompiled_css_path) export(run_with_themer) export(theme_bootswatch) export(theme_version) +export(value_box) export(version_default) export(versions) import(htmltools) @@ -96,5 +107,6 @@ importFrom(tools,file_path_sans_ext) importFrom(utils,URLencode) importFrom(utils,download.file) importFrom(utils,getFromNamespace) +importFrom(utils,head) importFrom(utils,modifyList) importFrom(utils,packageVersion) diff --git a/R/bs-theme.R b/R/bs-theme.R index c210401f5..1ae3b5558 100644 --- a/R/bs-theme.R +++ b/R/bs-theme.R @@ -269,7 +269,17 @@ bootstrap_bundle <- function(version) { ), # Additions to BS5 that are always included (i.e., not a part of compatibility) sass_layer(rules = pandoc_tables), - bs3compat = bs3compat_bundle() + bs3compat = bs3compat_bundle(), + # card() CSS (can be removed) + vfill = sass_layer( + rules = sass_file(system_file("components/vfill.scss", package = "bslib")) + ), + card = sass_layer( + rules = sass_file(system_file("components/card.scss", package = "bslib")) + ), + value_box = sass_layer( + rules = sass_file(system_file("components/value-box.scss", package = "bslib")) + ) ), four = sass_bundle( sass_layer( diff --git a/R/card.R b/R/card.R new file mode 100644 index 000000000..519c44a7c --- /dev/null +++ b/R/card.R @@ -0,0 +1,308 @@ +#' A Bootstrap card component +#' +#' A general purpose container for grouping related UI elements together with a +#' border and optional padding. To learn more about [card()]s, see [this +#' article](https://rstudio.github.io/bslib/articles/cards.html). +#' +#' @param ... Unnamed arguments can be any valid child of an [htmltools +#' tag][htmltools::tags] (which includes card items such as [card_body()]. +#' Named arguments become HTML attributes on returned UI element. +#' @param full_screen If `TRUE`, an icon will appear when hovering over the card +#' body. Clicking the icon expands the card to fit viewport size. Consider +#' pairing this feature with [card_body_fill()] to get output that responds to +#' changes in the size of the card. +#' @param height Any valid [CSS unit][htmltools::validateCssUnit] (e.g., +#' `height="200px"`). +#' @param class Additional CSS classes for the returned UI element. +#' @param wrapper A function (which returns a UI element) to call on unnamed +#' arguments in `...` which are not already card item(s) (like +#' [card_header()], [card_body()], etc.). Note that non-card items are grouped +#' together into one `wrapper` call (e.g. given `card("a", "b", +#' card_body("c"), "d")`, `wrapper` would be called twice, once with `"a"` and +#' `"b"` and once with `"d"`). Consider setting `wrapper` to [card_body_fill] +#' if the entire card wants responsive sizing or `NULL` to avoid wrapping +#' altogether +#' +#' @return A [htmltools::div()] tag. +#' +#' @export +#' @seealso [card_body()] for putting stuff inside the card. +#' @seealso [navs_tab_card()] for cards with multiple tabs. +#' @seealso [layout_column_wrap()] for laying out multiple cards (or multiple +#' columns inside a card). +#' @examples +#' +#' library(htmltools) +#' +#' if (interactive()) { +#' card( +#' full_screen = TRUE, +#' card_header( +#' "This is the header" +#' ), +#' card_body( +#' p("This is the body."), +#' p("This is still the body.") +#' ), +#' card_footer( +#' "This is the footer" +#' ) +#' ) +#' } +#' +card <- function(..., full_screen = FALSE, height = NULL, class = NULL, wrapper = card_body) { + + args <- rlang::list2(...) + argnames <- rlang::names2(args) + + attribs <- args[nzchar(argnames)] + children <- as_card_items(args[!nzchar(argnames)], wrapper = wrapper) + + tag <- div( + class = "card", + class = vfill_classes, + class = class, + style = css(height = validateCssUnit(height)), + !!!attribs, + !!!children, + if (full_screen) full_screen_toggle() + ) + + as_fragment( + tag_require(tag, version = 4, caller = "card()") + ) +} + + +as_card_items <- function(children, wrapper) { + # We don't want NULLs creating empty card bodys + children <- children[vapply(children, function(x) length(x) > 0, logical(1))] + + if (!is.function(wrapper)) { + return(children) + } + + # Any children that are `is.card_item` should be included verbatim. Any + # children that are not, should be wrapped in card_body(). Consecutive children + # that are not card_item, should be wrapped in a single card_body(). + needs_wrap <- !vapply(children, is.card_item, logical(1)) + needs_wrap_rle <- rle(needs_wrap) + start_indices <- c(1, head(cumsum(needs_wrap_rle$lengths) + 1, -1)) + children <- mapply( + start_indices, needs_wrap_rle$lengths, needs_wrap_rle$values, + FUN = function(start, length, wrap) { + these_children <- children[start:(start + length - 1)] + if (wrap) { + list(wrapper(these_children)) + } else { + these_children + } + }, + SIMPLIFY = FALSE + ) + unlist(children, recursive = FALSE) +} + +#' Card items +#' +#' Components designed to be provided as direct children of a [card()]. To learn +#' about [card()]s, see [this +#' article](https://rstudio.github.io/bslib/articles/cards.html). +#' +#' @param ... Unnamed arguments can be any valid child of an [htmltools +#' tag][htmltools::tags]. Named arguments become HTML attributes on returned +#' UI element. +#' @inheritParams card +#' +#' @return An [htmltools::div()] tag. +#' +#' @export +#' @seealso [card()] for creating a card component. +#' @seealso [navs_tab_card()] for cards with multiple tabs. +#' @seealso [layout_column_wrap()] for laying out multiple cards (or multiple +#' columns inside a card). +card_body <- function(..., height = NULL, class = NULL) { + card_body_( + fill = FALSE, + height = height, + class = class, + ... + ) +} + +#' @rdname card_body +#' @param gap A [CSS length unit][htmltools::validateCssUnit()] defining the +#' `gap` (i.e., spacing) between elements provided to `...`. +#' @export +card_body_fill <- function(..., gap = NULL, class = NULL) { + card_body_( + fill = TRUE, + gap = gap, + class = class, + ... + ) +} + + +#' @rdname card_body +#' @param container a function to generate an HTML element. +#' @export +card_title <- function(..., container = htmltools::h5) { + container( + class = "card-title", + # Our card.scss wants to set margin-bottom on headers, so make + # sure this rule isn't overridden + style = css(margin_bottom = "var(--bs-card-title-spacer-y, 0.5rem)"), + ... + ) +} + +card_body_ <- function(..., fill = TRUE, gap = NULL, height = NULL, class = NULL, container = htmltools::div) { + + res <- container( + class = "card-body", + class = if (fill) vfill_classes, + class = if (fill) "p-0", + class = class, + style = css( + flex = if (fill) "1 1 auto" else "0 0 auto", + height = validateCssUnit(height), + gap = validateCssUnit(gap) + ), + ... + ) + + as.card_item(res) +} + + +#' @rdname card_body +#' @param container a function that generates an [htmltools tag][htmltools::tags]. +#' @export +card_header <- function(..., class = NULL, container = htmltools::div) { + as.card_item( + container(class = "card-header", class = class, ...) + ) +} + +#' @rdname card_body +#' @export +card_footer <- function(..., class = NULL) { + as.card_item( + div(class = "card-footer mt-auto", class = class, ...) + ) +} + +#' @rdname card_body +#' @param file a file path pointing an image. The image will be base64 encoded +#' and provided to the `src` attribute of the ``. Alternatively, you may +#' set this value to `NULL` and provide the `src` yourself. +#' @param href an optional URL to link to. +#' @param border_radius where to apply `border-radius` on the image. +#' @param mime_type the mime type of the `file`. +#' @param container a function to generate an HTML element to contain the image. +#' @param width Any valid [CSS unit][htmltools::validateCssUnit] (e.g., `width="100%"`). +#' @export +card_image <- function( + file, ..., href = NULL, border_radius = c("top", "bottom", "all", "none"), + mime_type = NULL, class = NULL, height = NULL, width = NULL, container = card_body_fill) { + + src <- NULL + if (length(file) > 0) { + src <- base64enc::dataURI( + file = file, mime = mime_type %||% mime::guess_type(file) + ) + } + + image <- tags$img( + src = src, + class = "img-fluid", + class = "vfill-item", + class = class, + class = switch( + match.arg(border_radius), + all = "card-img", + top = "card-img-top", + bottom = "card-img-bottom", + NULL + ), + style = css( + height = validateCssUnit(height), + width = validateCssUnit(width) + ), + ... + ) + + if (!is.null(href)) { + image <- tags$a( + href = href, + class = vfill_classes, + image + ) + } + + if (is.function(container)) { + image <- container(image) + } + + image +} + +#' @rdname card_body +#' @param x an object to test (or coerce to) a card item. +#' @export +as.card_item <- function(x) { + class(x) <- c("card_item", class(x)) + x +} + +#' @rdname card_body +#' @export +is.card_item <- function(x) { + inherits(x, "card_item") +} + + +full_screen_toggle <- function() { + tags$a( + tags$span(class = "badge rounded-pill bg-dark m-2", style="padding:0.55rem !important;", + class = "bslib-full-screen-enter", + "data-bs-toggle" = "tooltip", + "data-bs-placement" = "bottom", + title = "Expand", + bsicons::bs_icon("arrows-fullscreen", class = "null"), + htmlDependency( + name = "bslib-card-full-screen", + version = get_package_version("bslib"), + package = "bslib", + src = "components", + script = "card-full-screen.js" + ), + # TODO: shiny should probably use ResizeObserver() itself (i.e., we + # shouldn't need to trigger a resize on the window) + # https://github.com/rstudio/shiny/pull/3682 + tags$script(HTML( + "var resizeEvent = window.document.createEvent('UIEvents'); + resizeEvent.initUIEvent('resize', true, false, window, 0); + var ro = new ResizeObserver(() => { window.dispatchEvent(resizeEvent); }); + document.querySelectorAll('.card').forEach(function(x) { ro.observe(x); })" + )) + ) + ) +} + + +# jcheng 2022-06-06: Removing for now; list items have more features than I'm +# ready to design an API for right now +# +# #' @rdname card_body +# #' @export +# card_list <- function(...) { +# res <- tags$ul(class = "list-group list-group-flush", ...) +# as.card_item(res) +# } +# +# #' @export +# card_list_item <- function(...) { +# tags$li(class = "list-group-item", ...) +# } diff --git a/R/imports.R b/R/imports.R index d794ab4a7..e82e48718 100644 --- a/R/imports.R +++ b/R/imports.R @@ -2,7 +2,7 @@ utils::globalVariables("!!") #' @import htmltools #' @import sass -#' @importFrom utils modifyList packageVersion download.file URLencode getFromNamespace +#' @importFrom utils modifyList packageVersion download.file URLencode getFromNamespace head #' @importFrom stats setNames na.omit #' @importFrom grDevices col2rgb #' @importFrom tools file_path_sans_ext diff --git a/R/layout.R b/R/layout.R new file mode 100644 index 000000000..cd823a988 --- /dev/null +++ b/R/layout.R @@ -0,0 +1,108 @@ +#' A grid-like, column-first, layout +#' +#' Wraps a 1d sequence of UI elements into a 2d grid. The number of columns (and +#' rows) in the grid dependent on the column `width` as well as the size of the +#' display. For more explanation and illustrative examples, see [here](https://rstudio.github.io/bslib/articles/cards.html#multiple-cards) +#' +#' @param ... Unnamed arguments should be UI elements (e.g., [card()]) +#' Named arguments become attributes on the containing [htmltools::tag] element. +#' @param width The desired width of each card, which can be any of the +#' following: +#' * A (unit-less) number between 0 and 1. +#' * This should be specified as `1/num`, where `num` represents the number +#' of desired columns. +#' * A [CSS length unit][htmltools::validateCssUnit()] +#' * Either the minimum (when `fixed_width=FALSE`) or fixed width +#' (`fixed_width=TRUE`). +#' * `NULL` +#' * Allows power users to set the `grid-template-columns` CSS property +#' manually, either via a `style` attribute or a CSS stylesheet. +#' @param fixed_width Whether or not to interpret the `width` as a minimum +#' (`fixed_width=FALSE`) or fixed (`fixed_width=TRUE`) width when it is a CSS +#' length unit. +#' @param heights_equal If `"all"` (the default), every card in every row of the +#' grid will have the same height. If `"row"`, then every card in _each_ row +#' of the grid will have the same height, but heights may vary between rows. +#' @param fill whether or not the grid items should grow to fill the row height. +#' @inheritParams card +#' @inheritParams card_body +#' +#' @export +#' @examples +#' +#' x <- card("A simple card") +#' # Always has 2 columns (on non-mobile) +#' layout_column_wrap(1/2, x, x, x) +#' # Has three columns when viewport is wider than 750px +#' layout_column_wrap("250px", x, x, x) +#' +layout_column_wrap <- function( + width, ..., fixed_width = FALSE, heights_equal = c("all", "row"), + fill = TRUE, height = NULL, gap = NULL, class = NULL) { + + heights_equal <- match.arg(heights_equal) + + args <- rlang::list2(...) + argnames <- rlang::names2(args) + + attribs <- args[nzchar(argnames)] + children <- args[!nzchar(argnames)] + + if (length(width) > 1) { + stop("`width` of length greater than 1 is not currently supported.") + } + + colspec <- if (!is.null(width)) { + if (width > 0 && width <= 1) { + num_cols <- 1 / width + if (num_cols != as.integer(num_cols)) { + stop("Could not interpret width argument; see ?layout_column_wrap") + } + paste0(rep_len("1fr", num_cols), collapse = " ") + } else { + if (fixed_width) { + paste0("repeat(auto-fit, ", validateCssUnit(width), ")") + } else { + paste0("repeat(auto-fit, minmax(", validateCssUnit(width), ", 1fr))") + } + } + } + + # Wrap grid items in flex containers for essentially two reasons: + # 1. Allow .vfill-item children (e.g. plotOutput("id", height = NULL)) + # to fill the grid row. + # 2. Allow for fill=FALSE, which useful for allowing contents to + # shrink but not grow (i.e., default flex behavior). + children <- lapply(children, function(x) { + div( + class = "vfill-container", + div( + class = "vfill-container", + class = if (fill) "vfill-item", + x + ) + ) + }) + + tag <- div( + class = "bslib-grid-layout", + class = "vfill-item", + class = class, + style = css( + grid_template_columns = colspec, + grid_auto_rows = if (heights_equal == "all") "1fr", + height = validateCssUnit(height), + gap = validateCssUnit(gap) + ), + !!!attribs, + children + ) + + as_fragment( + tag_require(tag, version = 4, caller = "layout_column_wrap()") + ) +} + + + +vfill_classes <- c("vfill-container", "vfill-item") diff --git a/R/navs.R b/R/navs.R index b9ec7e3df..69c7149ef 100644 --- a/R/navs.R +++ b/R/navs.R @@ -1,57 +1,91 @@ #' @export +#' @inheritParams card +#' @inheritParams card_body +#' @param title A (left-aligned) title to place in the card header/footer. If +#' provided, other nav items are automatically right aligned. #' @rdname navs -navs_tab_card <- function(..., id = NULL, selected = NULL, - header = NULL, footer = NULL) { - tabs <- tagQuery( - navs_tab(..., id = id, selected = selected, header = header, footer = footer) +navs_tab_card <- function(..., id = NULL, selected = NULL, title = NULL, + header = NULL, footer = NULL, height = NULL, + full_screen = FALSE, wrapper = card_body) { + + items <- rlang::list2(...) + + # The children of each nav() (i.e., tabPanel()) should be card items. + items <- lapply(items, function(x) { + if (isTabPanel(x)) { + x$children <- as_card_items(x$children, wrapper = wrapper) + } + x + }) + + tabs <- navs_tab( + !!!items, id = id, selected = selected, header = header, footer = footer ) # https://getbootstrap.com/docs/5.0/components/card/#navigation - nav <- tabs$ + nav <- tagQuery(tabs)$ find(".nav")$ addClass("card-header-tabs")$ selectedTags() - content <- tabs$find(".tab-content")$selectedTags() - - card(header = nav, content, caller = "navs_tab_card()") + card( + height = height, + full_screen = full_screen, + if (!is.null(title)) { + card_header(class = "bslib-card-title", tags$span(title), nav) + } else { + card_header(nav) + }, + navs_card_body(tabs) + ) } #' @export #' @param placement placement of the nav items relative to the content. #' @rdname navs -navs_pill_card <- function(..., id = NULL, selected = NULL, - header = NULL, footer = NULL, - placement = c("above", "below")) { +navs_pill_card <- function(..., id = NULL, selected = NULL, title = NULL, + header = NULL, footer = NULL, height = NULL, + full_screen = FALSE, placement = c("above", "below")) { + + items <- rlang::list2(...) - pills <- tagQuery( - navs_pill(..., id = id, selected = selected, header = header, footer = footer) + pills <- navs_pill( + !!!items, id = id, selected = selected, + header = header, footer = footer ) above <- match.arg(placement) == "above" - nav <- pills$ + nav <- tagQuery(pills)$ find(".nav")$ addClass(if (above) "card-header-pills")$ selectedTags() - content <- pills$find(".tab-content")$selectedTags() + nav_args <- if (!is.null(title)) { + list(class = "bslib-card-title", tags$span(title), nav) + } else { + list(nav) + } - args <- list(nav, content, caller = "navs_pill_card()") + card( + height = height, + full_screen = full_screen, + if (above) card_header(!!!nav_args), + navs_card_body(pills), + if (!above) card_footer(!!!nav_args) + ) +} - names(args)[1] <- if (above) "header" else "footer" +navs_card_body <- function(tabs) { - do.call(card, args) -} + tabs <- tagAppendAttributes(tabs, class = vfill_classes, .cssSelector = ".tab-content") + tabs <- tagAppendAttributes(tabs, class = vfill_classes, .cssSelector = ".tab-content > *") -card <- function(..., header = NULL, footer = NULL, caller) { - tag <- div( - class = "card", - if (!is.null(header)) div(class = "card-header", header), - div(class = "card-body", ...), - if (!is.null(footer)) div(class = "card-footer", footer) - ) - as_fragment( - tag_require(tag, version = 4, caller = caller) - ) + content <- tagQuery(tabs)$find(".tab-content")$selectedTags() + + if (length(content) > 1) { + stop("Found more than 1 .tab-content CSS class. Please use another name for your CSS classes.") + } + + as.card_item(content[[1]]) } diff --git a/R/utils-tags.R b/R/utils-tags.R index b497d5277..6ebc88af7 100644 --- a/R/utils-tags.R +++ b/R/utils-tags.R @@ -1,21 +1,47 @@ -tag_require <- function(tag, version = 4, caller = "") { +tag_require <- function(tag, version = version_default(), caller = "") { tagAddRenderHook( tag, replace = FALSE, func = function(x) { - current_version <- theme_version(bs_current_theme()) %||% 3 - if (isTRUE(current_version >= version)) - return(x) - - stop( - caller, " requires Bootstrap ", version, " or higher. ", - "Please supply `bslib::bs_theme(version = ", version, - ")` to the UI's page layout function.", - call. = FALSE - ) + # If we know for sure the version isn't sufficient, it's safe to throw + current_version <- theme_version(bs_current_theme()) + if (isTRUE(current_version < version)) { + stop( + caller, " requires Bootstrap ", version, " or higher. ", + "To specify the version of Bootstrap, see https://rstudio.github.io/bslib/#basic-usage", + call. = FALSE + ) + } + # We generally don't know the theme/version if any of these conditions are true: + # 1. We're inside an Rmd output format that doesn't run through rmarkdown::html_document_base + # * pkgdown is one known case where the Bootstrap version may be customized, but bslib + # doesn't currently have a way to know what the version is. + # 2. shiny::bootstrapLib() is being called while shiny is not shiny::isRunning() + # * At one point I was hoping bootstrapLib() could set the relevant context when + # statically rendered, but we didn't end up merging this + # https://github.com/rstudio/htmltools/pull/267 + # 3. Someone else is providing the bootstrap dependency + # * I currently don't know of any cases where this is relevant, but it might be + # + # So, since there are totally legitimate cases where the version requirement + # could be met, but we don't know for sure what's happening server-side, + # resort to a client-side check/warning + return(tag_require_client_side(x, version, caller)) } ) } -is_tag <- function(x) { - inherits(x, "shiny.tag") + +tag_require_client_side <- function(tag, version = version_default(), caller = "") { + tagAppendChild( + tagAppendAttributes( + tag, "data-require-bs-version" = version, "data-require-bs-caller" = caller + ), + htmlDependency( + name = "bslib-tag-require", + version = get_package_version("bslib"), + package = "bslib", + src = "components", + script = "tag-require.js" + ) + ) } diff --git a/R/value-box.R b/R/value-box.R new file mode 100644 index 000000000..971c06ff9 --- /dev/null +++ b/R/value-box.R @@ -0,0 +1,67 @@ +#' A card for displaying a summary of information +#' +#' @param title a [htmltools::tag()] child to display above `value`. +#' @param value a [htmltools::tag()] child to display below `title`. +#' @param ... Unnamed arguments may be any [htmltools::tag()] children +#' to display below `value` (if these children are not already +#' [is.card_item()], then they are wrapped in a [card_body()]). +#' Named arguments become attributes on the containing element. +#' @param showcase a [htmltools::tag()] child to showcase (e.g., a +#' [bsicons::bs_icon()], a [plotly::plotlyOutput()], etc). +#' @param showcase_layout where to display the showcase relative to the other +#' content. +#' @param class utility classes for customizing the appearance of the summary +#' card. Use `bg-*` and `text-*` classes (e.g, `"bg-danger"` and +#' `"text-light"`) to customize the background/foreground colors. +#' @inheritParams card +#' @export +#' @examples +#' +#' library(htmltools) +#' +#' if (interactive()) { +#' value_box( +#' "KPI Title", +#' HTML("$1 Billion Dollars"), +#' span( +#' bsicons::bs_icon("arrow-up"), +#' " 30% VS PREVIOUS 30 DAYS" +#' ), +#' showcase = bsicons::bs_icon("piggy-bank"), +#' class = "bg-success" +#' ) +#' } +value_box <- function(title, value, ..., showcase = NULL, showcase_layout = c("top-right", "left-center"), full_screen = FALSE, class = NULL) { + showcase_layout <- match.arg(showcase_layout) + + args <- rlang::list2(...) + argnames <- rlang::names2(args) + + attribs <- args[nzchar(argnames)] + children <- args[!nzchar(argnames)] + + tag <- div( + class = c( + "card bslib-value-box-container vfill-item bg-primary", + paste0("showcase-", showcase_layout), + class + ), + !!!attribs, + if (!is.null(showcase)) div(class = "value-box-showcase border-end", showcase), + card( + class = "bslib-value-box", + # color:unset so that the color inherits from bg-* on the parent + # (not the h3/h6 rules set by bootstrap core) + tags$h6(title, class = "mb-1", style = css(color = "unset")), + tags$h3(value, class = "mb-2", style = css(color = "unset")), + !!!children, + full_screen = full_screen + ) + ) + + tag <- tag_require( + tag, version = 5, caller = "value_box()" + ) + + as_fragment(tag) +} diff --git a/README.Rmd b/README.Rmd index ffb449f65..bd47778ca 100644 --- a/README.Rmd +++ b/README.Rmd @@ -20,17 +20,6 @@ knitr::opts_chunk$set( library(bslib) ``` -```{scss, echo = FALSE} -@media (min-width: 800px) { - .usage { - display: flex; - * { - flex: 1; - } - } -} -``` - # bslib The `bslib` R package provides tools for customizing [Bootstrap themes](https://getbootstrap.com/docs/4.6/getting-started/theming/) directly from R, making it much easier to customize the appearance of [Shiny](https://shiny.rstudio.com/) apps & [R Markdown](https://rmarkdown.rstudio.com/) documents. `bslib`'s primary goals are: diff --git a/README.md b/README.md index ae14c6c53..64fbeafb8 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,6 @@ status](https://github.com/rstudio/bslib/actions/workflows/R-CMD-check.yaml/badg - - # bslib The `bslib` R package provides tools for customizing [Bootstrap diff --git a/_pkgdown.yml b/_pkgdown.yml index d8a5c1f4f..127eaf9e7 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -1,12 +1,19 @@ destination: docs toc: - depth: 2 + depth: 3 url: https://rstudio.github.io/bslib/ template: - package: quillt + bootstrap: 5 + bslib: + info: "#E6F2FD" + base_font: + google: "Atkinson Hyperlegible" + headings-color: "#206B72" + link-color: "#216B73" + link-decoration: underline var(--bs-info) dotted params: footer: bslib is an R package developed by RStudio docsearch: @@ -14,7 +21,8 @@ template: index_name: bslib navbar: - type: default + bg: info + type: light structure: left: [intro, articles, integration] right: [reference, news, github] @@ -25,6 +33,8 @@ navbar: menu: #- text: Why Sass over CSS? # href: articles/why-sass.html + - text: Cards + href: articles/cards.html - text: BS5 Sass variables href: articles/bs5-variables.html - text: BS4 Sass variables @@ -58,14 +68,30 @@ reference: - bs_theme_preview - run_with_themer - bs_themer -- title: Create navs and navbars +- title: Cards description: | - Tools for creating and dynamically updating navs. + Create cards + contents: + - card + - as.card_item +- title: Value box + description: | + Highlight important findings + contents: + - value_box +- title: Navigation + description: | + Create (tabbed) sections of content. contents: - nav - navs_tab - nav_select -- title: Create a Bootstrap page +- title: Layout multiple UI elements + description: | + Useful layout templates + contents: + - layout_column_wrap +- title: Page layouts contents: - page - title: Dynamic theming diff --git a/inst/components/card-full-screen.js b/inst/components/card-full-screen.js new file mode 100644 index 000000000..991f3019a --- /dev/null +++ b/inst/components/card-full-screen.js @@ -0,0 +1,33 @@ +// Enable tooltips since the .bslib-full-screen-enter icon wants them +$(function() { + var tooltipList = [].slice.call( + document.querySelectorAll('[data-bs-toggle="tooltip"]') + ); + tooltipList.map(function(x) { + return new bootstrap.Tooltip(x); + }); +}); + +$(document).on('click', '.bslib-full-screen-enter', function(e) { + const $card = $(e.target).parents('.card').last(); + // Re-size/position the card (and add an overlay behind it) + $card.addClass("bslib-full-screen"); + const overlay = $("
Close
"); + $card[0].insertAdjacentElement("beforebegin", overlay[0]); +}); + +$(document).on('click', '.bslib-full-screen-exit', function(e) { + exitFullScreen(); +}); + +document.addEventListener('keyup', function(e) { + if (e.key === 'Escape') exitFullScreen(); +}, false); + +function exitFullScreen() { + const $card = $('.bslib-full-screen'); + if ($card) { + $('#bslib-full-screen-overlay').remove(); + $card.removeClass('bslib-full-screen'); + } +} diff --git a/inst/components/card.scss b/inst/components/card.scss new file mode 100644 index 000000000..11f1baad9 --- /dev/null +++ b/inst/components/card.scss @@ -0,0 +1,132 @@ +.card-footer { + margin-top: auto; +} + +.bslib-grid-layout { + display: grid !important; + gap: $spacer; + .card { + margin-bottom: 0; + } +} + +@include media-breakpoint-down(sm) { + .bslib-grid-layout { + grid-template-columns: 1fr !important; + } +} + +.card .shiny-plot-output { + overflow: hidden; +} + +.card { + margin-bottom: $spacer; + + // Avoid "double padding" when two card_body()s + // are immediate siblings + > .card-body + .card-body { + padding-top: 0; + } + + > .card-body { + overflow: auto; + & > { + p, h1, h2, h3, h4, h5, h6 { + margin-top: 0; + + &:last-child { + margin-bottom: 0; + } + } + } + } + + > .card-header { + .form-group { + margin-bottom: 0; + } + .selectize-control { + margin-bottom: 0; + // TODO: we should probably add this to selectize's SCSS since this actually makes selectInput() + // usable with width="fit-content" + .item { + margin-right: 1.15rem; + } + } + } +} + + +.bslib-card-title { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + .nav { + margin-left: auto; + } +} + +/************************************************* +* Full screen card logic +*************************************************/ + +.bslib-full-screen { + position: fixed; + inset: 3.5rem 1rem 1rem; + height: auto !important; + width: auto !important; + z-index: $zindex-popover; +} + +.bslib-full-screen-enter { + display: none; + position: absolute; + bottom: 1px; + right: 3px; + font-size: .8rem; + cursor: pointer; + opacity: .6; + color: rgba(var(--bs-body-bg-rgb), 1); + &:hover { + opacity: 1; + } +} + +.card:hover:not(.bslib-full-screen) .bslib-full-screen-enter { + display: block; +} + +// card_summary() allows for a full-screen card to be embedded within a parent card +.card:hover .bslib-full-screen .bslib-full-screen-enter { + display: none; +} + + +.bslib-full-screen-exit { + position: relative; + top: 1.35rem; + font-size: 0.9rem; + cursor: pointer; + text-decoration: none; + display: flex; + float: right; + margin-right: 2.15rem; + align-items: center; + color: rgba(var(--bs-body-bg-rgb), 0.8); + &:hover { + color: rgba(var(--bs-body-bg-rgb), 1); + } + svg { + margin-left: 0.5rem; + font-size: 1.5rem; + } +} + +#bslib-full-screen-overlay { + position: fixed; + inset: 0; + background-color: rgba(var(--bs-body-color-rgb), 0.6); + z-index: $zindex-popover - 1; +} diff --git a/inst/components/tag-require.js b/inst/components/tag-require.js new file mode 100644 index 000000000..4deb07c85 --- /dev/null +++ b/inst/components/tag-require.js @@ -0,0 +1,14 @@ +window.addEventListener('DOMContentLoaded', function(e) { + const nodes = document.querySelectorAll('[data-require-bs-version]'); + if (!nodes) { + return; + } + const VERSION = window.bootstrap ? parseInt(window.bootstrap.Tab.VERSION) : 3; + for (let i = 0; i < nodes.length; i++) { + const version = nodes[i].getAttribute('data-require-bs-version'); + const caller = nodes[i].getAttribute('data-require-bs-caller'); + if (version > VERSION) { + console.error(`${caller} requires Bootstrap version ${version} but this page is using version ${VERSION}`); + } + } +}); diff --git a/inst/components/value-box.scss b/inst/components/value-box.scss new file mode 100644 index 000000000..4bf0caabb --- /dev/null +++ b/inst/components/value-box.scss @@ -0,0 +1,83 @@ +$value-box-showcase-font-size: 3rem !default; + +.bslib-value-box-container { + flex-direction: row; + + .value-box-showcase { + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; + + .bi, + .fa { + opacity: .92; + } + .bi { + font-size: 5rem; + } + .fa { + font-size: 4rem; + } + } + + &.showcase-top-right .value-box-showcase { + position: absolute; + top: 0; + right: 0; + z-index: 1; + border: none !important; + } + + &.showcase-left-center .value-box-showcase { + // This CSS var was introduced 5.2 + --bs-border-opacity: 0.3; + // Temporary hack until we upgrade + border-color: rgba($border-color, var(--bs-border-opacity)) !important; + } +} + +.bslib-value-box { + justify-content: center; + width: 100%; + font-size: .9rem; + font-weight: 500; + background-color: inherit; + color: inherit; + border: none; + padding: 1.5rem 1rem; + + * { + padding: 0 !important; + } +} + + +.bslib-full-screen { + .bslib-value-box { + flex: 0; + flex-basis: fit-content; + flex-basis: -moz-fit-content; + } + + .value-box-showcase { + flex: 1 1 auto; + + // TODO: is this the best way to override the inline + // height/width on output bindings? + > * { + width: var(--showcase-full-width, 100%) !important; + height: var(--showcase-full-height, 600px) !important; + } + } + + &.showcase-top-right { + .value-box-showcase { + position: static; + order: 2; + display: flex; + justify-content: center; + align-items: center; + } + } +} diff --git a/inst/components/vfill.scss b/inst/components/vfill.scss new file mode 100644 index 000000000..4b174e083 --- /dev/null +++ b/inst/components/vfill.scss @@ -0,0 +1,20 @@ +.vfill-container { + display: flex; + flex-direction: column; + overflow: auto; + > .vfill-item { + flex: var(--bslib-vfill-flex, 1 1 auto); + overflow: auto; + } +} + +// Workaround for pkgdown's CSS to make tab-pane all a consistent height +// https://github.com/r-lib/pkgdown/blob/956f07/inst/BS5/assets/pkgdown.scss#L342-L355 +.tab-content > .tab-pane.vfill-container { + display: none; +} + +// Override Bootstrap's `display:block` rule +.tab-content > .active.vfill-container { + display: flex; +} diff --git a/inst/examples/card/layout/app.R b/inst/examples/card/layout/app.R new file mode 100644 index 000000000..fce5e6278 --- /dev/null +++ b/inst/examples/card/layout/app.R @@ -0,0 +1,91 @@ +library(shiny) +library(bslib) +library(plotly) +library(leaflet) +library(DT) +library(gt) + +theme_set(theme_minimal(base_size = 16)) +thematic::thematic_shiny() + +card_ui <- function(outputFunc = plotOutput) { + nm <- deparse(substitute(outputFunc)) + output_func <- function(id, ...) { + outputFunc(paste0(nm, id), ...) + } + + layout_column_wrap( + width = 1/3, + heights_equal = "row", + height = "calc(100vh - 80px)", + card( + full_screen = TRUE, + card_header("Single output"), + card_body_fill(output_func("1")), + p("some text") + ), + card( + full_screen = TRUE, + card_header("Multiple rows of stretchy output"), + card_body_fill( + output_func("2"), + output_func("3") + ), + p("Some other text") + ), + card( + full_screen = TRUE, + wrapper = card_body_fill, + card_header("Multiple columns of stretchy output"), + layout_column_wrap( + 1/2, + output_func("4"), + output_func("5") + ), + output_func("6") + ), + card( + full_screen = TRUE, + card_header("Shiny devs"), + tags$p("Current status"), + tags$ul( + tags$li("Carson"), + tags$li("Barret"), + ) + ) + ) +} + +ui_output <- function(...) uiOutput(..., fill = TRUE) + +ui <- page_navbar( + title = "Card body layout with multiple outputs", + theme = bslib::bs_theme(bg = "black", fg = "white"), + nav("Base", card_ui(plotOutput)), + nav("Plotly", card_ui(plotlyOutput)), + nav("Leaflet", card_ui(ui_output)), + nav("DT", card_ui(dataTableOutput)), + nav("gt", card_ui(gt_output)) +) + +server <- function(input, output, session) { + + output$p2 <- renderPlotly(plot_ly()) + + lapply(1:10, function(i) { + output[[paste0("plotOutput", i)]] <- renderPlot(qplot(x = 1:10) + labs(x = "x label", y = "y label")) + output[[paste0("plotlyOutput", i)]] <- renderPlotly(plot_ly(x = rnorm(100), type = "histogram")) + output[[paste0("ui_output", i)]] <- renderUI({ + leaflet() %>% + addTiles() %>% + fitBounds(0, 40, 10, 50) %>% + setView(-93.65, 42.0285, zoom = 17) %>% + addPopups(-93.65, 42.0285, "Here is the Department of Statistics, ISU") + }) + output[[paste0("dataTableOutput", i)]] <- renderDataTable(datatable(mtcars, fillContainer = F)) + output[[paste0("gt_output", i)]] <- render_gt(gt(mtcars[1:5, ])) + }) + +} + +shinyApp(ui, server) diff --git a/inst/examples/value_box/app.R b/inst/examples/value_box/app.R new file mode 100644 index 000000000..5d5980040 --- /dev/null +++ b/inst/examples/value_box/app.R @@ -0,0 +1,99 @@ +library(shiny) +library(bslib) +library(bsicons) +library(plotly) + +ui <- page_fluid( + theme = bslib::bs_theme( + base_font = font_google("Open Sans") + ), + br(), + layout_column_wrap( + width = "200px", + value_box( + title = "Unemployment Rate", + value = "2.7%", + div("Started at 1.5%"), + div("Averaged 3% over that period"), + div("Peaked at 5.2% in Dec 1982"), + showcase = plotlyOutput("unemploy", height = "50px", width = "100px"), + full_screen = TRUE + ), + value_box( + title = "Personal Savings Rate", + value = "7.6%", + div("Started at 12.6%"), + div("Averaged 8.6% over that period"), + div("Peaked 17.3% in May 1975"), + showcase = plotlyOutput("psavert", height = "50px", width = "100px"), + showcase_layout = "left-center", + full_screen = TRUE, + class = "bg-success" + ), + value_box( + title = "Personal Consumption", + value = "$3.8B", + div("Started at $0.25B"), + div("Averaged $1.7B over that period"), + showcase = bsicons::bs_icon("piggy-bank"), + showcase_layout = "left-center", + #full_screen = TRUE, + class = "bg-danger" + ) + ) +) + +server <- function(input, output) { + + output$unemploy <- renderPlotly({ + plotly_time_series( + economics, x = ~date, y = ~100 * unemploy / pop + ) + }) + + output$psavert <- renderPlotly({ + plotly_time_series( + economics, x = ~date, y = ~psavert + ) + }) + + output$pce <- renderPlotly({ + plotly_time_series( + economics, x = ~date, y = ~ 100 * pce / pop + ) + }) + + plotly_time_series <- function(d, x, y) { + info <- getCurrentOutputInfo() + large <- isTRUE(info$height() > 200) + + plot_ly(d, x = x, y = y) %>% + add_lines( + color = I(info$fg()), + span = I(1), + #hoverinfo = if (!large) "none", + fill = 'tozeroy', + alpha = 0.2 + ) %>% + layout( + hovermode = "x", + margin = list(t = 0, r = 0, l = 0, b = 0), + font = list(color = info$fg()), + paper_bgcolor = "transparent", + plot_bgcolor = "transparent", + xaxis = list( + title = "", + visible = large, + showgrid = FALSE + ), + yaxis = list( + title = "", + visible = large, + showgrid = FALSE + ) + ) %>% + config(displayModeBar = FALSE) + } +} + +shinyApp(ui, server) diff --git a/man/card.Rd b/man/card.Rd new file mode 100644 index 000000000..0a8b9090e --- /dev/null +++ b/man/card.Rd @@ -0,0 +1,72 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/card.R +\name{card} +\alias{card} +\title{A Bootstrap card component} +\usage{ +card( + ..., + full_screen = FALSE, + height = NULL, + class = NULL, + wrapper = card_body +) +} +\arguments{ +\item{...}{Unnamed arguments can be any valid child of an \link[htmltools:builder]{htmltools tag} (which includes card items such as \code{\link[=card_body]{card_body()}}. +Named arguments become HTML attributes on returned UI element.} + +\item{full_screen}{If \code{TRUE}, an icon will appear when hovering over the card +body. Clicking the icon expands the card to fit viewport size. Consider +pairing this feature with \code{\link[=card_body_fill]{card_body_fill()}} to get output that responds to +changes in the size of the card.} + +\item{height}{Any valid \link[htmltools:validateCssUnit]{CSS unit} (e.g., +\code{height="200px"}).} + +\item{class}{Additional CSS classes for the returned UI element.} + +\item{wrapper}{A function (which returns a UI element) to call on unnamed +arguments in \code{...} which are not already card item(s) (like +\code{\link[=card_header]{card_header()}}, \code{\link[=card_body]{card_body()}}, etc.). Note that non-card items are grouped +together into one \code{wrapper} call (e.g. given \code{card("a", "b", card_body("c"), "d")}, \code{wrapper} would be called twice, once with \code{"a"} and +\code{"b"} and once with \code{"d"}). Consider setting \code{wrapper} to \link{card_body_fill} +if the entire card wants responsive sizing or \code{NULL} to avoid wrapping +altogether} +} +\value{ +A \code{\link[htmltools:builder]{htmltools::div()}} tag. +} +\description{ +A general purpose container for grouping related UI elements together with a +border and optional padding. To learn more about \code{\link[=card]{card()}}s, see \href{https://rstudio.github.io/bslib/articles/cards.html}{this article}. +} +\examples{ + +library(htmltools) + +if (interactive()) { + card( + full_screen = TRUE, + card_header( + "This is the header" + ), + card_body( + p("This is the body."), + p("This is still the body.") + ), + card_footer( + "This is the footer" + ) + ) +} + +} +\seealso{ +\code{\link[=card_body]{card_body()}} for putting stuff inside the card. + +\code{\link[=navs_tab_card]{navs_tab_card()}} for cards with multiple tabs. + +\code{\link[=layout_column_wrap]{layout_column_wrap()}} for laying out multiple cards (or multiple +columns inside a card). +} diff --git a/man/card_body.Rd b/man/card_body.Rd new file mode 100644 index 000000000..d4a2372bf --- /dev/null +++ b/man/card_body.Rd @@ -0,0 +1,82 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/card.R +\name{card_body} +\alias{card_body} +\alias{card_body_fill} +\alias{card_title} +\alias{card_header} +\alias{card_footer} +\alias{card_image} +\alias{as.card_item} +\alias{is.card_item} +\title{Card items} +\usage{ +card_body(..., height = NULL, class = NULL) + +card_body_fill(..., gap = NULL, class = NULL) + +card_title(..., container = htmltools::h5) + +card_header(..., class = NULL, container = htmltools::div) + +card_footer(..., class = NULL) + +card_image( + file, + ..., + href = NULL, + border_radius = c("top", "bottom", "all", "none"), + mime_type = NULL, + class = NULL, + height = NULL, + width = NULL, + container = card_body_fill +) + +as.card_item(x) + +is.card_item(x) +} +\arguments{ +\item{...}{Unnamed arguments can be any valid child of an \link[htmltools:builder]{htmltools tag}. Named arguments become HTML attributes on returned +UI element.} + +\item{height}{Any valid \link[htmltools:validateCssUnit]{CSS unit} (e.g., +\code{height="200px"}).} + +\item{class}{Additional CSS classes for the returned UI element.} + +\item{gap}{A \link[htmltools:validateCssUnit]{CSS length unit} defining the +\code{gap} (i.e., spacing) between elements provided to \code{...}.} + +\item{container}{a function to generate an HTML element to contain the image.} + +\item{file}{a file path pointing an image. The image will be base64 encoded +and provided to the \code{src} attribute of the \verb{}. Alternatively, you may +set this value to \code{NULL} and provide the \code{src} yourself.} + +\item{href}{an optional URL to link to.} + +\item{border_radius}{where to apply \code{border-radius} on the image.} + +\item{mime_type}{the mime type of the \code{file}.} + +\item{width}{Any valid \link[htmltools:validateCssUnit]{CSS unit} (e.g., \code{width="100\%"}).} + +\item{x}{an object to test (or coerce to) a card item.} +} +\value{ +An \code{\link[htmltools:builder]{htmltools::div()}} tag. +} +\description{ +Components designed to be provided as direct children of a \code{\link[=card]{card()}}. To learn +about \code{\link[=card]{card()}}s, see \href{https://rstudio.github.io/bslib/articles/cards.html}{this article}. +} +\seealso{ +\code{\link[=card]{card()}} for creating a card component. + +\code{\link[=navs_tab_card]{navs_tab_card()}} for cards with multiple tabs. + +\code{\link[=layout_column_wrap]{layout_column_wrap()}} for laying out multiple cards (or multiple +columns inside a card). +} diff --git a/man/layout_column_wrap.Rd b/man/layout_column_wrap.Rd new file mode 100644 index 000000000..d330174d8 --- /dev/null +++ b/man/layout_column_wrap.Rd @@ -0,0 +1,73 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/layout.R +\name{layout_column_wrap} +\alias{layout_column_wrap} +\title{A grid-like, column-first, layout} +\usage{ +layout_column_wrap( + width, + ..., + fixed_width = FALSE, + heights_equal = c("all", "row"), + fill = TRUE, + height = NULL, + gap = NULL, + class = NULL +) +} +\arguments{ +\item{width}{The desired width of each card, which can be any of the +following: +\itemize{ +\item A (unit-less) number between 0 and 1. +\itemize{ +\item This should be specified as \code{1/num}, where \code{num} represents the number +of desired columns. +} +\item A \link[htmltools:validateCssUnit]{CSS length unit} +\itemize{ +\item Either the minimum (when \code{fixed_width=FALSE}) or fixed width +(\code{fixed_width=TRUE}). +} +\item \code{NULL} +\itemize{ +\item Allows power users to set the \code{grid-template-columns} CSS property +manually, either via a \code{style} attribute or a CSS stylesheet. +} +}} + +\item{...}{Unnamed arguments should be UI elements (e.g., \code{\link[=card]{card()}}) +Named arguments become attributes on the containing \link[htmltools:builder]{htmltools::tag} element.} + +\item{fixed_width}{Whether or not to interpret the \code{width} as a minimum +(\code{fixed_width=FALSE}) or fixed (\code{fixed_width=TRUE}) width when it is a CSS +length unit.} + +\item{heights_equal}{If \code{"all"} (the default), every card in every row of the +grid will have the same height. If \code{"row"}, then every card in \emph{each} row +of the grid will have the same height, but heights may vary between rows.} + +\item{fill}{whether or not the grid items should grow to fill the row height.} + +\item{height}{Any valid \link[htmltools:validateCssUnit]{CSS unit} (e.g., +\code{height="200px"}).} + +\item{gap}{A \link[htmltools:validateCssUnit]{CSS length unit} defining the +\code{gap} (i.e., spacing) between elements provided to \code{...}.} + +\item{class}{Additional CSS classes for the returned UI element.} +} +\description{ +Wraps a 1d sequence of UI elements into a 2d grid. The number of columns (and +rows) in the grid dependent on the column \code{width} as well as the size of the +display. For more explanation and illustrative examples, see \href{https://rstudio.github.io/bslib/articles/cards.html#multiple-cards}{here} +} +\examples{ + +x <- card("A simple card") +# Always has 2 columns (on non-mobile) +layout_column_wrap(1/2, x, x, x) +# Has three columns when viewport is wider than 750px +layout_column_wrap("250px", x, x, x) + +} diff --git a/man/navs.Rd b/man/navs.Rd index b75368e9c..0faf1960b 100644 --- a/man/navs.Rd +++ b/man/navs.Rd @@ -41,14 +41,27 @@ navs_bar( fluid = TRUE ) -navs_tab_card(..., id = NULL, selected = NULL, header = NULL, footer = NULL) +navs_tab_card( + ..., + id = NULL, + selected = NULL, + title = NULL, + header = NULL, + footer = NULL, + height = NULL, + full_screen = FALSE, + wrapper = card_body +) navs_pill_card( ..., id = NULL, selected = NULL, + title = NULL, header = NULL, footer = NULL, + height = NULL, + full_screen = FALSE, placement = c("above", "below") ) } @@ -72,7 +85,8 @@ layout.} \item{widths}{Column widths of the navigation list and tabset content areas respectively.} -\item{title}{The title to display in the navbar} +\item{title}{A (left-aligned) title to place in the card header/footer. If +provided, other nav items are automatically right aligned.} \item{position}{Determines whether the navbar should be displayed at the top of the page with normal scrolling behavior (\code{"static-top"}), pinned at @@ -91,6 +105,22 @@ text color. If \code{"auto"} (the default), the best contrast to \code{bg} is ch elements into a menu when the width of the browser is less than 940 pixels (useful for viewing on smaller touchscreen device)} +\item{height}{Any valid \link[htmltools:validateCssUnit]{CSS unit} (e.g., +\code{height="200px"}).} + +\item{full_screen}{If \code{TRUE}, an icon will appear when hovering over the card +body. Clicking the icon expands the card to fit viewport size. Consider +pairing this feature with \code{\link[=card_body_fill]{card_body_fill()}} to get output that responds to +changes in the size of the card.} + +\item{wrapper}{A function (which returns a UI element) to call on unnamed +arguments in \code{...} which are not already card item(s) (like +\code{\link[=card_header]{card_header()}}, \code{\link[=card_body]{card_body()}}, etc.). Note that non-card items are grouped +together into one \code{wrapper} call (e.g. given \code{card("a", "b", card_body("c"), "d")}, \code{wrapper} would be called twice, once with \code{"a"} and +\code{"b"} and once with \code{"d"}). Consider setting \code{wrapper} to \link{card_body_fill} +if the entire card wants responsive sizing or \code{NULL} to avoid wrapping +altogether} + \item{placement}{placement of the nav items relative to the content.} } \description{ diff --git a/man/value_box.Rd b/man/value_box.Rd new file mode 100644 index 000000000..56ea7b821 --- /dev/null +++ b/man/value_box.Rd @@ -0,0 +1,61 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/value-box.R +\name{value_box} +\alias{value_box} +\title{A card for displaying a summary of information} +\usage{ +value_box( + title, + value, + ..., + showcase = NULL, + showcase_layout = c("top-right", "left-center"), + full_screen = FALSE, + class = NULL +) +} +\arguments{ +\item{title}{a \code{\link[htmltools:builder]{htmltools::tag()}} child to display above \code{value}.} + +\item{value}{a \code{\link[htmltools:builder]{htmltools::tag()}} child to display below \code{title}.} + +\item{...}{Unnamed arguments may be any \code{\link[htmltools:builder]{htmltools::tag()}} children +to display below \code{value} (if these children are not already +\code{\link[=is.card_item]{is.card_item()}}, then they are wrapped in a \code{\link[=card_body]{card_body()}}). +Named arguments become attributes on the containing element.} + +\item{showcase}{a \code{\link[htmltools:builder]{htmltools::tag()}} child to showcase (e.g., a +\code{\link[bsicons:bs_icon]{bsicons::bs_icon()}}, a \code{\link[plotly:plotly-shiny]{plotly::plotlyOutput()}}, etc).} + +\item{showcase_layout}{where to display the showcase relative to the other +content.} + +\item{full_screen}{If \code{TRUE}, an icon will appear when hovering over the card +body. Clicking the icon expands the card to fit viewport size. Consider +pairing this feature with \code{\link[=card_body_fill]{card_body_fill()}} to get output that responds to +changes in the size of the card.} + +\item{class}{utility classes for customizing the appearance of the summary +card. Use \verb{bg-*} and \verb{text-*} classes (e.g, \code{"bg-danger"} and +\code{"text-light"}) to customize the background/foreground colors.} +} +\description{ +A card for displaying a summary of information +} +\examples{ + +library(htmltools) + +if (interactive()) { + value_box( + "KPI Title", + HTML("$1 Billion Dollars"), + span( + bsicons::bs_icon("arrow-up"), + " 30\% VS PREVIOUS 30 DAYS" + ), + showcase = bsicons::bs_icon("piggy-bank"), + class = "bg-success" + ) +} +} diff --git a/pkgdown/extra.css b/pkgdown/extra.css new file mode 100644 index 000000000..c0652cb61 --- /dev/null +++ b/pkgdown/extra.css @@ -0,0 +1,25 @@ +@media (min-width: 800px) { + .usage { + display: flex; + * { + flex: 1; + } + } +} + +p a { + text-decoration-skip-ink: none; + text-decoration-thickness: from-font; +} + +a:focus, a:hover { + text-decoration: underline; +} + +.navbar-light .navbar-nav .active>.nav-link { + background: #D0E5FC; +} + +table.dataTable.table-bordered { + border-collapse: collapse !important; +} diff --git a/vignettes/_variables-template.Rmd b/vignettes/_variables-template.Rmd index 605668082..e2dd61f9d 100644 --- a/vignettes/_variables-template.Rmd +++ b/vignettes/_variables-template.Rmd @@ -27,9 +27,6 @@ Below is a search-able table of Bootstrap {{version}} Sass variables. If you are * Towards the top of the table are more general theming options like `white`, `gray-*`, `black`, `primary`, `border-radius`, and so on, which end up impacting more specific theming variables like `btn-border-radius`. * `bs_theme()`'s `bg` and `fg` arguments provide a more convenient way to set the `white`, `gray-*`, and `black` variables, so there is no need to set these Sass variables directly (same goes for `base_font` -> `$font-family-base`, `heading_font` -> `$headings-font-family`, and `code_font` -> `$font-family-monospace`). - -
```{r} variables_dt({{version}}) ``` -
diff --git a/vignettes/bs4-variables.Rmd b/vignettes/bs4-variables.Rmd index 8e9b3c9af..084cd3100 100644 --- a/vignettes/bs4-variables.Rmd +++ b/vignettes/bs4-variables.Rmd @@ -27,9 +27,6 @@ Below is a search-able table of Bootstrap 4 Sass variables. If you aren't sure w * Towards the top of the table are more general theming options like `white`, `gray-*`, `black`, `primary`, `border-radius`, and so on, which end up impacting more specific theming variables like `btn-border-radius`. * `bs_theme()`'s `bg` and `fg` arguments provide a more convenient way to set the `white`, `gray-*`, and `black` variables, so there is no need to set these Sass variables directly (same goes for `base_font` -> `$font-family-base`, `heading_font` -> `$headings-font-family`, and `code_font` -> `$font-family-monospace`). - -
```{r} variables_dt(4) ``` -
diff --git a/vignettes/bs5-variables.Rmd b/vignettes/bs5-variables.Rmd index 22df396d6..0044056f2 100644 --- a/vignettes/bs5-variables.Rmd +++ b/vignettes/bs5-variables.Rmd @@ -27,9 +27,6 @@ Below is a search-able table of Bootstrap 5 Sass variables. If you aren't sure w * Towards the top of the table are more general theming options like `white`, `gray-*`, `black`, `primary`, `border-radius`, and so on, which end up impacting more specific theming variables like `btn-border-radius`. * `bs_theme()`'s `bg` and `fg` arguments provide a more convenient way to set the `white`, `gray-*`, and `black` variables, so there is no need to set these Sass variables directly (same goes for `base_font` -> `$font-family-base`, `heading_font` -> `$headings-font-family`, and `code_font` -> `$font-family-monospace`). - -
```{r} variables_dt(5) ``` -
diff --git a/vignettes/cards.Rmd b/vignettes/cards.Rmd new file mode 100644 index 000000000..549055e59 --- /dev/null +++ b/vignettes/cards.Rmd @@ -0,0 +1,692 @@ +--- +title: "Cards" +resource_files: + - shiny-hex.svg + - infobox.svg + - value-box.svg +--- + +Cards are a common organizing unit for modern user interfaces (UI). At their core, they're just rectangular containers with borders and padding. However, when utilized properly to group related information, they help users better digest, engage, and navigate through content. This is why most successful dashboard/UI frameworks make cards a core feature of their component library. This article provides an overview of the API that bslib provides to create [Bootstrap cards](https://getbootstrap.com/docs/5.0/components/card/). + +One major feature that bslib adds to Bootstrap cards is the ability to expand the card to a [full screen view](#responsive-sizing). Often this feature wants to be coupled with [content that responds](#responsive-sizing) to sizing changes in the card. To help illustrate, we'll mostly use statically rendered [htmlwidgets](http://www.htmlwidgets.org/) like `{plotly}` and `{leaflet}`, but the same API can be used for [dynamically rendered content in Shiny](#dynamic-rendering-shiny): + +```{r setup, include=FALSE} +knitr::opts_chunk$set(echo = TRUE, warning = FALSE, message = FALSE) + +lorem_ipsum_dolor_sit_amet <- "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Id nibh tortor id aliquet lectus proin nibh nisl. Adipiscing at in tellus integer feugiat. Arcu bibendum at varius vel pharetra vel turpis nunc eget. Cursus sit amet dictum sit amet justo. Sit amet consectetur adipiscing elit. Vestibulum mattis ullamcorper velit sed ullamcorper. Enim facilisis gravida neque convallis a. Elit duis tristique sollicitudin nibh sit amet. Magna eget est lorem ipsum. Gravida dictum fusce ut placerat orci nulla pellentesque dignissim. Mauris in aliquam sem fringilla ut morbi. Id semper risus in hendrerit gravida rutrum quisque non tellus. At erat pellentesque adipiscing commodo elit at imperdiet dui. Fames ac turpis egestas maecenas pharetra convallis posuere morbi. Duis convallis convallis tellus id interdum velit laoreet id. Aliquet lectus proin nibh nisl. Nunc vel risus commodo viverra maecenas accumsan lacus vel facilisis. Bibendum enim facilisis gravida neque convallis a." + +# pkgdown really wants BS5+ markup for tabs, and this is currently the best way to achieving that :( +# (note this isn't a problem for any format based on html_document_base) +shiny:::setCurrentTheme(bslib::bs_theme()) +``` + +```{scss, echo = FALSE} +div.infobox { + padding: 2em; + margin: 1em 0; + padding-left: 100px; + background-size: 70px; + background-repeat: no-repeat; + background-position: 15px center; + min-height: 120px; + color: #1f5386; + background-color: #bed3ec; + border: solid 5px #dfedff; + background-image: url("infobox.svg"); + a { + color: #e783b8; + } +} + +/* Credit to https://getcssscan.com/css-box-shadow-examples */ +.card { + box-shadow: + rgba(0, 0, 0, 0.25) 0px 54px 55px, + rgba(0, 0, 0, 0.12) 0px -12px 30px, + rgba(0, 0, 0, 0.12) 0px 4px 6px, + rgba(0, 0, 0, 0.17) 0px 12px 13px, + rgba(0, 0, 0, 0.09) 0px -3px 5px; + margin-bottom: 3rem; +} + +.bslib-grid-layout .card { + box-shadow: none; + margin-bottom: 0rem; +} + +.bslib-value-box.card { + box-shadow: none; + margin-bottom: 0rem; + color: white !important; +} + +.bslib-grid-layout { + margin-bottom: 2rem; + overflow-y: scroll +} + +.section.level2 { + margin-top: 5rem; +} +``` + +```{r ref.label="anim_helpers",echo=FALSE} +``` + + +```{r} +library(bslib) +library(shiny) +library(htmltools) +library(plotly) +library(leaflet) + +plotly_widget <- plot_ly(x = diamonds$cut) %>% + config(displayModeBar = FALSE) %>% + layout(margin = list(t = 10, b = 10, l = 10, r = 10)) + +leaflet_widget <- leaflet() %>% + addTiles() +``` + + +## Hello `card()` + +::: row +::: col-md-6 +A `card()` is designed to handle any number of "known" card items (e.g., `card_header()`, `card_body()`, etc) as unnamed arguments (i.e., children). As we'll see shotly, `card()` also has a number of optional named arguments (e.g., `full_screen`, `height`, etc). + +At their core, `card()` and card items are just an HTML `div()` with a special Bootstrap class, so you can use Bootstrap's utility classes to customize things like [colors](https://getbootstrap.com/docs/5.2/utilities/background/), [text](https://getbootstrap.com/docs/5.2/utilities/text/), [borders](https://getbootstrap.com/docs/5.2/utilities/borders), etc. +::: + +::: col-md-6 +```{r} +card( + card_header("A header", class = "bg-dark"), + card_body( + "Some text with a ", + tags$a("link", href = "https://github.com") + ) +) +``` +::: +::: + +## Implicit `card_body()` + +::: row +::: col-md-6 +If you find yourself using `card_body()` without changing any of its defaults, consider dropping it altogether since any direct children of `card()` that aren't "known" `card()` items, are wrapped together into an implicit `card_body()` call.[^1] For example, the code to the right generates HTML that is identical to the previous example: +::: + +[^1]: If you want to customize this behavior, you can provide a function function to `wrapper` argument (or set it to `NULL` to avoid wrapping the non card items in a container). + +::: col-md-6 +```{r} +card( + card_header("A header", class = "bg-dark"), + "Some text with a ", + tags$a("link", href = "https://github.com") +) +``` +::: +::: + +## Fixed sizing + +::: row +::: col-md-6 +By default, a `card()`'s size grows to accommodate the size of it's contents. Thus, if some portion of the `card_body()` contains a large amount of text, table(s), etc., consider setting a fixed `height`. And in that case, if the contents exceed the specified height, they'll be scrollable. +::: + +::: col-md-6 +```{r} +card( + card_header("A long, scrolling, description"), + card_body( + height = 150, + lorem_ipsum_dolor_sit_amet + ) +) +``` +::: +::: + +::: row +::: col-md-6 +Most htmlwidgets (like `plotly::plot_ly()`), as well as some Shiny output bindings (like `shiny::plotOutput()`), default to a fixed height of 400 pixels. That means, if the `card_body()` containing such an output needs to be smaller (or larger) than 400px, the output won't shrink (or grow) as needed. For that sort of responsive behavior, use `card_body_fill()` instead of `card_body()`. +::: + +::: col-md-6 +```{r} + +``` + +```{r} +card( + height = 200, + card_header("An undesirable scrolling plot"), + plotly_widget +) +``` +::: +::: + + +## Responsive sizing + +::: row +::: col-md-6 +Unlike `card_body()`, `card_body_fill()` encourages its children to grow and shrink vertically as needed in response to its `card()`'s height. This responsive sizing behavior is particularly useful in combination with `card(full_screen = TRUE, ...)`, which adds an icon (displayed on hover) to expand the `card()` to a full screen view. It also defaults to no padding since that’s often desirable when growing/shrinking a singular visual output to fit the container. +::: + +::: col-md-6 +```{r} +card( + height = 250, full_screen = TRUE, + card_header("A full screen card w/ responsive plot"), + card_body_fill(plotly_widget), + card_footer( + class = "fs-6", + "Copyright 2022 RStudio, PBC" + ) +) +``` +::: +::: + +::: {.infobox} +In order for `card_body_fill()` to work properly with htmlwidgets, you currently need development version(s): + +```r +remotes::install_github(c("ramnathv/htmlwidgets", "ropensci/plotly", "rstudio/leaflet", "rstudio/DT")) +``` +::: + +::: row +::: col-md-6 +Under-the-hood, `card_body_fill()` achieves its behavior because it is a [flex container](https://css-tricks.com/snippets/css/a-guide-to-flexbox/), which makes its direct children flex items. This can lead to suprising, yet useful, differences in behavior from `card_body()`. For example, each inline element (like text, `actionLink()`, `actionButton()`, etc) is placed in a new row and stretches horizontally (as shown in the example). In the case where you want particular elements inside of `card_body_fill()` to behave as though they're in `card_body()` (i.e., have the `actionLink()` and `actionButton()` appear inline on the same line), just wrap those elements in a `div()`. +::: + +::: col-md-6 +```{r} +card( + height = 250, full_screen = TRUE, + card_header("A plot with an action links"), + card_body_fill( + plotly_widget, + actionLink( + "go", "Action link", + class = "link-primary align-self-center" + ), + actionButton( + "go_btn", "Action button", + class = "btn-primary rounded-0" + ) + ) +) +``` +::: +::: + + +## Fixed & responsive sizing + +::: row +::: col-md-6 +Sometimes it's desirable to combine both `card_body_fill()` with `card_body()` to allow some portion of the body to grow/shrink as needed, but also keep another portion at a fixed/defined height. +::: + +::: col-md-6 +```{r} +card( + height = 300, full_screen = TRUE, + card_header("Plot with long description"), + card_body_fill(plotly_widget), + card_body( + height = "30%", + lorem_ipsum_dolor_sit_amet + ) +) +``` +::: +::: + +## Spacing & alignment + +::: row +::: col-md-6 +Keep in mind that, by default, `card_body()` adds padding around it's content while `card_body_fill()` doesn't. In either case, you can override those defaults with the [spacing utility classes](https://getbootstrap.com/docs/5.2/utilities/spacing/). Also keep in mind that the [flex utility classes](https://getbootstrap.com/docs/5.2/utilities/flex/) can be quite useful for aligning content. +::: + +::: col-md-6 +```{r} +card( + height = 300, full_screen = TRUE, + card_body_fill( + class = "p-2", + plotOutput("id") + ), + card_body( + class = "py-5 d-flex justify-content-center", + "Description with lots of padding" + ) +) +``` +::: +::: + +::: row +::: col-md-6 +Utility classes are really useful since they not only also help with spacing and alignment of stuff _within_ a `card_body()` (or `card_body_fill()`), but more generally enable easy customization of colors, fonts, and more. +::: + +::: col-md-6 +```{r} +card( + card_title("A title"), + p("Paragraph 1", class = "text-muted"), + p("Paragraph 2", class = "text-end"), + div( + class = "p-5 d-flex justify-content-center bg-secondary", + p("Paragraph 3.") + ) +) +``` +::: +::: + + +::: row +::: col-md-6 +In the case of `card_body_fill()`, since it's based on flexbox, you can add uniform spacing between children via the `gap` argument. Note there is a similar way to space between [multiple columns](#multiple-columns). +::: + +::: col-md-6 +```{r} +card( + card_body_fill( + gap = "1rem", class = "p-1", + div("Thing 1", class = "bg-secondary"), + div("Thing 2", class = "bg-secondary") + ) +) +``` +::: +::: + +## Dynamic rendering (Shiny) + +::: row +::: col-md-6 +Since this article is statically rendered, the examples here use statically rendered content/widgets, but the same `card()` functionality works for dynamically rendered content via Shiny (e.g., `shiny::plotOutput()`, `plotly::plotlyOutput()`, etc). +::: + +::: col-md-6 +```{r, message=FALSE} +# User interface logic +card( + height = 200, full_screen = TRUE, + card_header("Imagine this is a real plot"), + card_body_fill( + plotOutput("plot_id") + ) +) +``` +::: +::: + +::: row +::: col-md-6 +One neat thing about dynamic rendering is that you can leverage `shiny::getCurrentOutputInfo()` to render content differently depending on the height of its container, which is particularly useful with `card(full_screen = T, ...)`. For example, you may want additional captions/labels when a plot is large, additional controls on a table, etc (see the [value boxes](#value-boxes) section below for a concrete example). +::: + +::: col-md-6 +```{r, eval=FALSE} +# Server logic +output$plot_id <- renderPlot({ + info <- getCurrentOutputInfo() + if (info$height() > 600) { + # code for "large" plot + } else { + # code for "small" plot + } +}) +``` +::: +::: + + +::: {.infobox} +In order for `card_body_fill()` to work properly with `plotOutput()`, you currently need development version(s) (and for it to work with other `*Output()` containers you may need to set `*Output(height = NULL)` or update the relevant package). + +```r +remotes::install_github(c("rstudio/shiny", "ramnathv/htmlwidgets")) +``` +::: + +## Value boxes + +::: row +::: col-md-6 +`value_box()` uses `card()` as a foundation, mainly to inherit it's `full_screen` capabilities. It's first 2 arguments (`title` and `value`) are the only requirements, but you can add additional content below the value as well as something to `showcase`, like an icon. By default, the `showcase` will appear in the upper-right corner of the value box, but it can also be display to the left of the value, as done here. + +Although we don't demonstrate it here, keep in mind that you may customize a `value_box()`'s color via [background color utility classes](https://getbootstrap.com/docs/5.2/utilities/background/) (e.g., `value_box(class = "bg-success")`) +::: + +::: col-md-6 +```{r} +library(bsicons) +value_box( + "My big number", + "100%", + p("It's going up", bs_icon("arrow-up")), + showcase = bs_icon("graph-up"), + showcase_layout = "left-center" +) +``` +::: +::: + +Advanced users can make clever use of `shiny::getCurrentOutputInfo()` to make "expandable sparklines" within a `value_box()`'s `showcase` when `full_screen=TRUE`. For example, when the `value_box()` is small (i.e., not full screen), one could embed a small/minimal `plotlyOutput()` (i.e., no axes, hoverinfo, etc) line chart (showing the real data behind the value), but then add proper labeling, tooltips, etc. when the `value_box()` made full screen (see here for [source code](https://github.com/rstudio/bslib/tree/main/inst/examples/value_box) behind this motivating example): + + + +## Static images + +::: row +::: col-md-6 +`card_image()` makes it easy to embed static (i.e., pre-generated) images into a card. Provide a URL to `href` to make it clickable. In the case of multiple `card_image()`s, consider laying them out in [multiple cards](#multiple-cards) with `layout_column_wrap()` to produce a grid of clickable thumbnails. +::: + +::: col-md-6 +```{r} +card( + card_image( + "shiny-hex.svg", height = 200, + href = "https://github.com/rstudio/shiny" + ), + card_title("Shiny for R"), + p("It's a wonderful thing.") +) +``` +::: +::: + + + +::: row +::: col-md-6 +Unlike `shiny::plotOutput()`, `card_image()` won't be able to resize bitmap images (i.e., png, jpeg, etc.) intelligently, so it's highly recommended to use vector-based formats like SVG where possible. +::: + +::: col-md-6 +```{r, message=FALSE} +library(ggplot2) +img_file <- tempfile(fileext = ".svg") +ggsave( + img_file, device = "svg", + ggplot(mtcars, aes(wt, mpg)) + + geom_point() + + theme_bw(base_size = 16) +) +card( + full_screen = TRUE, + card_image(img_file), + card_title("ggplot2"), + p("It's a wonderful thing.") +) +``` +::: +::: + + +## Multiple tabs + +::: row +::: col-md-6 +`navs_tab_card()` (as well as `navs_pill_card()`) makes it easy to create cards with multiple tabs (or pills). These functions have the same `full_screen` capabilities as normal `card()`s as well some other options like `title` (since there is no natural place for a `card_header()` to be used). Note that, similar to `card()`, the children of each `nav()` panel will be implicitly wrapped in a `card_body()` call, so use `card_body_fill()` where appropriate to get [responsive sizing](#responsive-sizing). +::: + + +::: col-md-6 +```{r} +library(leaflet) +navs_tab_card( + height = 300, full_screen = TRUE, + title = "HTML Widgets", + nav( + "Plotly", + card_body_fill(plotly_widget) + ), + nav( + "Leaflet", + card_body_fill(leaflet_widget) + ), + nav( + NULL, icon = shiny::icon("circle-info"), + "Learn more about", + tags$a("htmlwidgets", href = "http://www.htmlwidgets.org/") + ) +) +``` +::: +::: + + +## Multiple columns + +::: row +::: col-md-6 +To create multiple columns within a card, it's recommended to use `layout_column_wrap()` (which can also be used to layout [multiple cards](#multiple-cards)), especially if the height of those columns should grow/shrink as needed. +::: + +::: col-md-6 +```{r} +card( + height = 300, full_screen = TRUE, + card_body_fill( + class = "p-1", + layout_column_wrap( + width = 1/2, + plotOutput("p1"), + plotOutput("p2") + ) + ), + card_body( + height = "30%", + lorem_ipsum_dolor_sit_amet + ) +) +``` +::: +::: + + +## Multiple cards + +This section outlines various layouts made possible by `layout_column_wrap()`. To illustrate, we'll use three `card()` instances with various content, but keep in mind that `layout_column_wrap()` is designed to work [other UI elements as well](#multiple-columns). + +**Note:** The examples in this section are not intended to be viewed on mobile devices. At small window widths, all of the layouts here collapse into a more mobile-friendly approach of "show each card at maximum width". + +```{r} +card1 <- card( + card_header("Scrolling content"), + lorem_ipsum_dolor_sit_amet +) +card2 <- card( + card_header("Nothing much here"), + "This is it." +) +card3 <- card( + full_screen = TRUE, + card_header("Filling content"), + card_body_fill(plotOutput("p")) +) +``` + + +### Uniform width and height + +When displaying multiple cards at once, it's often most visually appealing to have them displayed in a grid-like layout where each card has the same height and width. `layout_column_wrap()` optimizes for this design principle, and only demands a `width` for each column (or a number of columns). In the event that there are more cards than columns available, cards are wrapped into a new row (by default, all rows have the same height, but you can easily [vary the row height](#by-row)). + +#### Fixed number of columns + +For a fixed number of columns, provide `width = 1/n`, where `n` is the number of columns.[^2] As the animation (except on mobile devices) below shows, as the width of the `layout_column_wrap()` container changes, each card grows or shrinks to maintain its 1/4 width. + +[^2]: Do not attempt to use percent-based widths with `width`--like `"50%"` instead of `1/2`. Percentages will almost certainly not give you the results you want. + +```{r} +layout_column_wrap( + width = 1/2, height = 300, + card1, card2, card3 +) |> + anim_width("100%", "67%") +``` + +One potential issue with a fixed number of columns is that, on medium sized screens, the card width may become too small. If that happens to be a problem, specifying a "responsive" number of columns may be preferable. + +#### Responsive number of columns + +For a responsive number of columns (i.e., the number of columns depends on the window size), provide `width` with any valid CSS unit, like 200 pixels. In our case (with three cards), the 3rd card gets wrapped onto a new line when the viewport is less than 600 pixels, but on wider screens, the cards equally distribute the free space. + +```{r} +layout_column_wrap( + width = "200px", height = 300, + card1, card2, card3 +) |> + anim_width("100%", "67%") +``` + + +#### Fixed column width + +To keep the `width` of each column fixed (don't allow cards to grow to take up free space), set `fixed_width = TRUE`. + +```{r} +layout_column_wrap( + width = "200px", height = 300, + fixed_width = TRUE, + card1, card2, card3 +) |> + anim_width("100%", "67%") +``` + + +### Varying heights + +By default, when `layout_column_wrap()` wraps columns onto a new row, _all_ rows are given equal height. + +#### By row + +To allow the height of each row to be different, set `heights_equal = "row"`: + +```{r} +layout_column_wrap( + width = 1/2, height = 300, + heights_equal = "row", + card1, card3, card2 +) +``` + +#### By column + +By default, each the height on each card grows to fill the available vertical space in a particular row. To opt out of that behavior, set `fill = FALSE`. + +```{r} +layout_column_wrap( + width = "200px", height = 300, fill = FALSE, + card1, card2, card3 +) +``` + + +### Varying widths & more complex layouts + +Set `width` to `NULL` and provide a custom [`grid-template-columns` property](https://developer.mozilla.org/en-US/docs/Web/CSS/grid-template-columns) (and possibly other [CSS grid properties](https://css-tricks.com/snippets/css/complete-guide-grid/)) to accomplish more complex layouts, like varying column widths. This particular layout gives the 1st and 3rd card twice as much space as the 2nd: + +```{r} +layout_column_wrap( + width = NULL, height = 300, fill = FALSE, + style = css(grid_template_columns = "2fr 1fr 2fr"), + card1, card2, card3 +) +``` + +If you aren't keen on learning and/or using CSS grid directly, you may also want to try the [Shiny UI editor](https://rstudio.github.io/shinyuieditor/) (and/or `{gridlayout}`) to produce the layout. + + +## Appendix + +In the spirit of reproducibility, this section discloses custom CSS and R code used in the examples above. + +The following CSS is used to give `plotOutput()` a background color; it's necessary here because this documentation page is not actually hooked up to a Shiny app, so we can't show a real plot. + +```{css} +.shiny-plot-output { + background-color: #216B7288; + height: 400px; + width: 100%; +} +``` + +These R functions add animation-related CSS class and styles to whatever tags you give it. + +```{r anim_helpers,echo=TRUE,results="hide"} +anim_width <- function(x, width1, width2) { + x |> tagAppendAttributes( + class = "animate-width", + style = css( + `--width1` = validateCssUnit(width1), + `--width2` = validateCssUnit(width2), + ), + ) +} + +anim_height <- function(x, height1, height2) { + # Wrap in a div fixed at the height of height2, so the rest of + # the content on the page doesn't shift up and down + div(style = css(height = validateCssUnit(height2)), + x |> tagAppendAttributes( + class = "animate-height", + style = css( + `--height1` = validateCssUnit(height1), + `--height2` = validateCssUnit(height2), + ), + ) + ) +} +``` + +And here are the CSS animation rules that power those `anim_width` and `anim_height` R functions. + +```{css} +@keyframes changewidth { + from { width: var(--width1); } + 25% { width: var(--width1); } + 50% { width: var(--width2); } + 75% { width: var(--width2); } + to { width: var(--width1); } +} +.animate-width { + animation-duration: 6s; + animation-name: changewidth; + animation-iteration-count: infinite; + border-right: 2px solid #DDD; + padding-right: 1rem; +} + +@keyframes changeheight { + from { height: var(--height1); } + 25% { height: var(--height1); } + 50% { height: var(--height2); } + 75% { height: var(--height2); } + to { height: var(--height1); } +} +.animate-height { + height: 600px; + animation-duration: var(--anim-duration, 6s); + animation-name: changeheight; + animation-iteration-count: infinite; +} +``` diff --git a/vignettes/shiny-hex.svg b/vignettes/shiny-hex.svg new file mode 100644 index 000000000..3c3c1ba0a --- /dev/null +++ b/vignettes/shiny-hex.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vignettes/value-box.svg b/vignettes/value-box.svg new file mode 100644 index 000000000..ec68e402f --- /dev/null +++ b/vignettes/value-box.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/vignettes/variables-table/variables-table.R b/vignettes/variables-table/variables-table.R index 0ad8690cb..01f2cf32b 100644 --- a/vignettes/variables-table/variables-table.R +++ b/vignettes/variables-table/variables-table.R @@ -45,8 +45,9 @@ variables_dt <- function(version) { DT::datatable( variables_df(version), escape = FALSE, - style = "bootstrap", - class = "cell-border", + style = "bootstrap4", + class = "table-bordered", + width = "80vw", rownames = FALSE, selection = "none", extensions = c("Select", "RowGroup", "Scroller"), diff --git a/vignettes/variables-table/variables-table.css b/vignettes/variables-table/variables-table.css index 6b8a1c313..9db2d9357 100644 --- a/vignettes/variables-table/variables-table.css +++ b/vignettes/variables-table/variables-table.css @@ -28,17 +28,3 @@ table.dataTable tr td:nth-child(4){ .selected .dep-link { color: white; } -#table-wrapper { - position: relative; - --table_w: min(90vw, 1300px); - width: var(--table_w); - margin-left: calc(50% - var(--table_w)/2); -} -@media (min-width: 992px) { - #table-wrapper.pkgdown { - /* This is 65% instead of 50% because of how pkgdown/bootstrap - controls widths and the center content div is 75% of its - container div leaving an extra 15% to take into account */ - margin-left: calc(65% - var(--table_w)/2); - } -}