1#' Add low-level theming customizations 2#' 3#' Compared to higher-level theme customization available in [bs_theme()], these functions 4#' are a more direct interface to Bootstrap Sass, and therefore, do nothing to 5#' ensure theme customizations are portable between major Bootstrap versions. 6#' 7#' @inheritParams bs_theme_update 8#' @param ... 9#' * `bs_add_variables()`: Should be named Sass variables or values that can be passed in directly to the `defaults` argument of a [sass::sass_layer()]. 10#' * `bs_bundle()`: Should be arguments that can be handled by [sass::sass_bundle()] to be appended to the `theme` 11#' @param .where Whether to place the variable definitions before other Sass 12#' `"defaults"`, after other Sass `"declarations"`, or after other Sass 13#' `"rules"`. 14#' @param .default_flag Whether or not to add a `!default` flag (if missing) to 15#' variable expressions. It's recommended to keep this as `TRUE` when `.where 16#' = "defaults"`. 17#' 18#' @return a modified [bs_theme()] object. 19#' 20#' @references \url{https://getbootstrap.com/docs/4.4/getting-started/theming/} 21#' @references \url{https://rstudio.github.io/sass/articles/sass.html#layering} 22#' @examples 23#' 24#' # Function to preview the styling a (primary) Bootstrap button 25#' library(htmltools) 26#' button <- tags$a(class = "btn btn-primary", href = "#", role = "button", "Hello") 27#' preview_button <- function(theme) { 28#' if (interactive()) { 29#' browsable(tags$body(bs_theme_dependencies(theme), button)) 30#' } 31#' } 32#' 33#' # Here we start with a theme based on a Bootswatch theme, 34#' # then override some variable defaults 35#' theme <- bs_add_variables( 36#' bs_theme(bootswatch = "sketchy", primary = "orange"), 37#' "body-bg" = "#EEEEEE", 38#' "font-family-base" = "monospace", 39#' "font-size-base" = "1.4rem", 40#' "btn-padding-y" = ".16rem", 41#' "btn-padding-x" = "2rem" 42#' ) 43#' 44#' preview_button(theme) 45#' 46#' # If you need to set a variable based on another Bootstrap variable 47#' theme <- bs_add_variables(theme, "body-color" = "$success", .where = "declarations") 48#' preview_button(theme) 49#' 50#' # Start a new global theme and add some custom rules that 51#' # use Bootstrap variables to define a custom styling for a 52#' # 'person card' 53#' person_rules <- system.file("custom", "person.scss", package = "bslib") 54#' theme <- bs_add_rules(bs_theme(), sass::sass_file(person_rules)) 55#' # Include custom CSS that leverages bootstrap Sass variables 56#' person <- function(name, title, company) { 57#' tags$div( 58#' class = "person", 59#' h3(class = "name", name), 60#' div(class = "title", title), 61#' div(class = "company", company) 62#' ) 63#' } 64#' if (interactive()) { 65#' browsable(shiny::fluidPage( 66#' theme = theme, 67#' person("Andrew Carnegie", "Owner", "Carnegie Steel Company"), 68#' person("John D. Rockefeller", "Chairman", "Standard Oil") 69#' )) 70#' } 71#' 72#' @export 73#' @describeIn bs_bundle Add Bootstrap Sass [variable defaults](https://getbootstrap.com/docs/4.4/getting-started/theming/#variable-defaults) 74bs_add_variables <- function(theme, ..., .where = "defaults", .default_flag = identical(.where, "defaults")) { 75 assert_bs_theme(theme) 76 77 vars <- rlang::list2(...) 78 if (any(names2(vars) == "")) stop("Variables must be named.", call. = FALSE) 79 80 # Workaround to the problem of 'blue' winning in the scenario of: 81 # bs_add_variables("body-bg" = "blue") 82 # bs_add_variables("body-bg" = "red") 83 if (.default_flag) { 84 vars <- ensure_default_flag(vars) 85 } 86 87 bs_bundle( 88 theme, do.call(sass_layer, rlang::list2(!!.where := vars)) 89 ) 90} 91 92# Given a named list of variable definitions, 93# searches each variable's expression for a !default flag, 94# and if missing, adds it. 95ensure_default_flag <- function(x) { 96 Map( 97 x, rlang::names2(x), 98 f = function(val, nm) { 99 # sass::font_collection() has it's own default_flag, so warn if they conflict 100 if (sass::is_font_collection(val)) { 101 if (identical(val$default_flag, FALSE)) { 102 message( 103 "Ignoring `bs_add_variables()`'s `.default_flag = TRUE` for ", 104 "the ", nm, " variable (since it has it's own `default_flag`)." 105 ) 106 } 107 return(val) 108 } 109 val <- paste(as_sass(val), collapse = "\n") 110 if (grepl("!default\\s*;*\\s*$", val)) { 111 val 112 } else { 113 paste(sub(";+$", "", val), "!default") 114 } 115 } 116 ) 117} 118 119#' @describeIn bs_bundle Add additional [Sass rules](https://sass-lang.com/documentation/style-rules) 120#' @param rules Sass rules. Anything understood by [sass::as_sass()] may be 121#' provided (e.g., a list, character vector, [sass::sass_file()], etc) 122#' @export 123bs_add_rules <- function(theme, rules) { 124 bs_bundle(theme, sass_layer(rules = rules)) 125} 126 127#' @describeIn bs_bundle Add additional [Sass 128#' functions](https://rstudio.github.io/sass/articles/sass.html#functions-1) 129#' @param functions A character vector or [sass::sass_file()] containing 130#' functions definitions. 131#' @export 132bs_add_functions <- function(theme, functions) { 133 bs_bundle(theme, sass_layer(functions = functions)) 134} 135 136#' @describeIn bs_bundle Add additional [Sass 137#' mixins](https://rstudio.github.io/sass/articles/sass.html#mixins-1) 138#' @param mixins A character vector or [sass::sass_file()] containing 139#' mixin definitions. 140#' @export 141bs_add_mixins <- function(theme, mixins) { 142 bs_bundle(theme, sass_layer(mixins = mixins)) 143} 144 145#' @describeIn bs_bundle Add additional [sass::sass_bundle()] objects to an existing `theme`. 146#' @export 147bs_bundle <- function(theme, ...) { 148 assert_bs_theme(theme) 149 structure( 150 sass_bundle(theme, ...), 151 class = class(theme) 152 ) 153} 154