From: Mark Otto Date: Tue, 5 May 2026 23:09:38 +0000 (-0700) Subject: v6: More utilities API, docs, tests (#42381) X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=3f649e319ee44d896522a533422c575218c2bf7a;p=thirdparty%2Fbootstrap.git v6: More utilities API, docs, tests (#42381) * Utilities API and class improvements * Expand utility API test coverage * Update utilities API and border-radius docs * Fix bundlewatch * Introduce `@mixin generate-utilities-loop()` * Add text wrapping utilities in Sass utilities API in the docs * Copy updates, links, callout for postcss prefix, fix a border-radius * inline warning * error over warn * Fix bundlewatch * more copy edits * fix layout and headings * lint --------- Co-authored-by: Julien Déramond --- diff --git a/.bundlewatch.config.json b/.bundlewatch.config.json index 3a39bdd910..ab63a36164 100644 --- a/.bundlewatch.config.json +++ b/.bundlewatch.config.json @@ -18,7 +18,7 @@ }, { "path": "./dist/css/bootstrap-utilities.css", - "maxSize": "17.75 kB" + "maxSize": "18.0 kB" }, { "path": "./dist/css/bootstrap-utilities.min.css", @@ -26,11 +26,11 @@ }, { "path": "./dist/css/bootstrap.css", - "maxSize": "48.25 kB" + "maxSize": "48.5 kB" }, { "path": "./dist/css/bootstrap.min.css", - "maxSize": "45.0 kB" + "maxSize": "45.25 kB" }, { "path": "./dist/js/bootstrap.bundle.js", diff --git a/scss/_utilities.scss b/scss/_utilities.scss index 5b6adb4bbd..13fae2ac28 100644 --- a/scss/_utilities.scss +++ b/scss/_utilities.scss @@ -128,12 +128,12 @@ $utilities: map.merge( values: $position-values ), "start": ( - property: left, + property: inset-inline-start, class: start, values: $position-values ), "end": ( - property: right, + property: inset-inline-end, class: end, values: $position-values ), @@ -188,6 +188,7 @@ $utilities: map.merge( ) ), "border-y": ( + class: border-y, property: border-block, values: ( null: var(--border-width) var(--border-style) var(--border-color), @@ -195,6 +196,7 @@ $utilities: map.merge( ) ), "border-x": ( + class: border-x, property: border-inline, values: ( null: var(--border-width) var(--border-style) var(--border-color), @@ -375,8 +377,8 @@ $utilities: map.merge( responsive: true, property: justify-self, values: ( - start: flex-start, - end: flex-end, + start: start, + end: end, center: center, ) ), @@ -687,19 +689,13 @@ $utilities: map.merge( values: lowercase uppercase capitalize ), // scss-docs-end utils-text-transform - // "white-space": ( - // property: white-space, - // class: text, - // values: ( - // wrap: normal, - // nowrap: nowrap, - // ) - // ), + // scss-docs-start utils-text-wrap "text-wrap": ( - property: white-space, + property: text-wrap, class: text, values: wrap nowrap balance pretty, ), + // scss-docs-end utils-text-wrap // scss-docs-start utils-text-break "word-wrap": ( property: word-wrap word-break, @@ -709,12 +705,6 @@ $utilities: map.merge( // scss-docs-end utils-text-break // scss-docs-end utils-text // scss-docs-start utils-color - // "color-attr": ( - // selector: "attr-includes", - // class: "fg-", - // property: color, - // values: var(--fg), - // ), "fg": ( property: ( "--fg": null, @@ -870,7 +860,10 @@ $utilities: map.merge( // scss-docs-end utils-interaction // scss-docs-start utils-border-radius "border-radius": ( - property: border-radius, + property: ( + "--rounded-size": null, + "border-radius": var(--rounded-size) + ), class: rounded, values: map.merge( $radii, @@ -881,49 +874,61 @@ $utilities: map.merge( ) ) ), + "rounded-size": ( + property: --rounded-size, + class: rounded-size, + values: map.merge( + $radii, + ( + null: map.get($radii, 5), + circle: 50%, + pill: var(--radius-pill) + ) + ) + ), "rounded-top": ( property: border-start-start-radius border-start-end-radius, class: rounded-top, values: map.merge( $radii, ( - null: map.get($radii, 5), + null: var(--rounded-size, var(--radius-5)), circle: 50%, pill: var(--radius-pill) ) ) ), "rounded-end": ( - property: border-end-end-radius border-end-start-radius, + property: border-start-end-radius border-end-end-radius, class: rounded-end, values: map.merge( $radii, ( - null: map.get($radii, 5), + null: var(--rounded-size, var(--radius-5)), circle: 50%, pill: var(--radius-pill) ) ) ), "rounded-bottom": ( - property: border-end-end-radius border-end-start-radius, + property: border-end-start-radius border-end-end-radius, class: rounded-bottom, values: map.merge( $radii, ( - null: map.get($radii, 5), + null: var(--rounded-size, var(--radius-5)), circle: 50%, pill: var(--radius-pill) ) ) ), "rounded-start": ( - property: border-start-start-radius border-start-end-radius, + property: border-start-start-radius border-end-start-radius, class: rounded-start, values: map.merge( $radii, ( - null: map.get($radii, 5), + null: var(--rounded-size, var(--radius-5)), circle: 50%, pill: var(--radius-pill) ) diff --git a/scss/mixins/_utilities.scss b/scss/mixins/_utilities.scss index 67045cdce0..2d78150d7b 100644 --- a/scss/mixins/_utilities.scss +++ b/scss/mixins/_utilities.scss @@ -1,8 +1,7 @@ @use "sass:list"; @use "sass:map"; @use "sass:meta"; - -// stylelint-disable scss/dollar-variable-pattern +@use "../layout/breakpoints" as bp; // Utility generator @@ -50,23 +49,42 @@ // text-decoration-color: ...; // } +// Helper mixin to emit CSS custom properties from a utility's `variables` key. +// When variables is a map, the provided static values are used on each class. +// When variables is a list or single identifier, each variable receives the current utility value. +@mixin generate-variables($utility, $value) { + @if map.has-key($utility, variables) { + $variables: map.get($utility, variables); + @if meta.type-of($variables) == "map" { + @each $var-key, $var-value in $variables { + --#{$var-key}: #{$var-value}; + } + } @else { + // Treat as a list (or single identifier) — each variable gets the utility value + @each $var-name in $variables { + --#{$var-name}: #{$value}; + } + } + } +} + // Helper mixin to generate CSS properties for both legacy and property map approaches -@mixin generate-properties($utility, $propertyMap, $properties, $value) { - @if $propertyMap != null { - // New Property-Value Mapping approach - @each $property, $defaultValue in $propertyMap { - // If value is a map, check if it has a key for this property - // Otherwise, use defaultValue (or $value if defaultValue is null) - $actualValue: $defaultValue; +@mixin generate-properties($utility, $property-map, $properties, $value) { + @if $property-map != null { + // Property-Value Mapping approach + @each $property, $default-value in $property-map { + // If value is a map, check if it has a key for this property. + // Otherwise, use default-value (or $value if default-value is null). + $actual-value: $default-value; @if meta.type-of($value) == "map" and map.has-key($value, $property) { - $actualValue: map.get($value, $property); - } @else if $defaultValue == null { - $actualValue: $value; + $actual-value: map.get($value, $property); + } @else if $default-value == null { + $actual-value: $value; } @if map.get($utility, important) { - #{$property}: $actualValue !important; // stylelint-disable-line declaration-no-important + #{$property}: $actual-value !important; // stylelint-disable-line declaration-no-important } @else { - #{$property}: $actualValue; + #{$property}: $actual-value; } } } @else { @@ -91,29 +109,39 @@ } // Warn on unknown keys (likely typos) - $valid-keys: property, values, class, selector, responsive, print, important, state, variables, child-selector; + $valid-keys: property, values, class, selector, responsive, print, dark, important, state, variables, child-selector, enabled; @each $key in map.keys($utility) { @if not list.index($valid-keys, $key) { @warn "Unknown utility key `#{$key}` found. Valid keys are: #{$valid-keys}"; } } + // Validate boolean keys + @each $bool-key in (responsive, print, dark, important, enabled) { + @if map.has-key($utility, $bool-key) { + $val: map.get($utility, $bool-key); + @if $val != true and $val != false { + @error "Utility key `#{$bool-key}` should be a boolean (true or false), got: #{$val}"; + } + } + } + // Determine if we're generating a class, or an attribute selector - $selectorType: "class"; + $selector-type: "class"; @if map.has-key($utility, selector) { - $selectorType: map.get($utility, selector); + $selector-type: map.get($utility, selector); // Validate selector type $valid-selectors: "class", "attr-starts", "attr-includes"; - @if not list.index($valid-selectors, $selectorType) { - @error "Invalid `selector` value `#{$selectorType}`. Must be one of: #{$valid-selectors}"; + @if not list.index($valid-selectors, $selector-type) { + @error "Invalid `selector` value `#{$selector-type}`. Must be one of: #{$valid-selectors}"; } } - // Then get the class name to use in a class (e.g., .class) or in a attribute selector (e.g., [class^="class"]) - $selectorClass: map.get($utility, class); + // Then get the class name to use in a class (e.g., .class) or in an attribute selector (e.g., [class^="class"]) + $selector-class: map.get($utility, class); // Attribute selectors require a `class` key - @if $selectorType != "class" and not map.has-key($utility, class) { - @error "Utility with `selector: #{$selectorType}` requires a `class` key."; + @if $selector-type != "class" and not map.has-key($utility, class) { + @error "Utility with `selector: #{$selector-type}` requires a `class` key."; } // Get the list or map of values and ensure it's a map @@ -132,29 +160,28 @@ @each $key, $value in $values { $properties: map.get($utility, property); - $propertyMap: null; - $customClass: ""; + $property-map: null; + $custom-class: ""; - // Check if property is a map (new Property-Value Mapping approach) + // Check if property is a map (Property-Value Mapping approach) @if meta.type-of($properties) == "map" { - $propertyMap: $properties; - $customClass: ""; + $property-map: $properties; @if map.has-key($utility, class) { - $customClass: map.get($utility, class); + $custom-class: map.get($utility, class); } } @else { - // Legacy approach: Multiple properties are possible, for example with vertical or horizontal margins or paddings + // Legacy approach: multiple properties are possible, for example with vertical or horizontal margins or paddings @if meta.type-of($properties) == "string" { $properties: list.append((), $properties); } // Use custom class if present, otherwise use the first value from the list of properties @if map.has-key($utility, class) { - $customClass: map.get($utility, class); + $custom-class: map.get($utility, class); } @else { - $customClass: list.nth($properties, 1); + $custom-class: list.nth($properties, 1); } - @if $customClass == null { - $customClass: ""; + @if $custom-class == null { + $custom-class: ""; } } @@ -165,42 +192,36 @@ } // Don't add a dash before value key if value key is null (e.g. with shadow class) - $customClassModifier: ""; + $custom-class-modifier: ""; @if $key { - @if $customClass == "" { - $customClassModifier: $key; + @if $custom-class == "" { + $custom-class-modifier: $key; } @else { - $customClassModifier: "-" + $key; + $custom-class-modifier: "-" + $key; } } // Build the class name fragment (without prefix or dot) for reuse in state variants - $className: ""; - @if $selectorType == "class" { - @if $customClass != "" { - $className: $customClass + $customClassModifier; - } @else if $selectorClass != null and $selectorClass != "" { - $className: $selectorClass + $customClassModifier; + $class-name: ""; + @if $selector-type == "class" { + @if $custom-class != "" { + $class-name: $custom-class + $custom-class-modifier; + } @else if $selector-class != null and $selector-class != "" { + $class-name: $selector-class + $custom-class-modifier; } @else { - $className: $customClassModifier; + $class-name: $custom-class-modifier; } } $selector: ""; - @if $selectorType == "class" { - $selector: ".#{$prefix + $className}"; - } @else if $selectorType == "attr-starts" { - $selector: "[class^=\"#{$selectorClass}\"]"; - } @else if $selectorType == "attr-includes" { - $selector: "[class*=\"#{$selectorClass}\"]"; + @if $selector-type == "class" { + $selector: ".#{$prefix + $class-name}"; + } @else if $selector-type == "attr-starts" { + $selector: "[class^=\"#{$selector-class}\"]"; + } @else if $selector-type == "attr-includes" { + $selector: "[class*=\"#{$selector-class}\"]"; } - // @debug $utility; - // @debug $selectorType; - // @debug $selector; - // @debug $properties; - // @debug $values; - // Apply child-selector wrapping if present (wraps in :where() for zero specificity) $child-sel: null; @if map.has-key($utility, child-selector) { @@ -213,52 +234,59 @@ } #{$final-selector} { - // Generate CSS custom properties (variables) if provided - // Variables receive the current utility value, then properties reference them - @if map.has-key($utility, variables) { - $variables: map.get($utility, variables); - @if meta.type-of($variables) == "list" { - // If variables is a list, each variable gets the utility value - @each $var-name in $variables { - --#{$var-name}: #{$value}; - } - } @else if meta.type-of($variables) == "map" { - // If variables is a map, use the provided values (for static variables) - @each $var-key, $var-value in $variables { - --#{$var-key}: #{$var-value}; - } - } - } - @include generate-properties($utility, $propertyMap, $properties, $value); + @include generate-variables($utility, $value); + @include generate-properties($utility, $property-map, $properties, $value); } // Generate state variants (e.g., hover:link-10 instead of link-10-hover) @if $state != () { @each $state-variant in $state { - $state-selector: ".#{$prefix}#{$state-variant}\\:#{$className}:#{$state-variant}"; + $state-selector: ".#{$prefix}#{$state-variant}\\:#{$class-name}:#{$state-variant}"; @if $child-sel { $state-selector: ":where(#{$state-selector} #{$child-sel})"; } #{$state-selector} { - // Generate CSS custom properties (variables) if provided - @if map.has-key($utility, variables) { - $variables: map.get($utility, variables); - @if meta.type-of($variables) == "list" { - // If variables is a list, each variable gets the utility value - @each $var-name in $variables { - --#{$var-name}: #{$value}; - } - } @else if meta.type-of($variables) == "map" { - // If variables is a map, use the provided values (for static variables) - @each $var-key, $var-value in $variables { - --#{$var-key}: #{$var-value}; - } - } - } - @include generate-properties($utility, $propertyMap, $properties, $value); + @include generate-variables($utility, $value); + @include generate-properties($utility, $property-map, $properties, $value); } } } } } + +// Generates all utility classes: base, responsive, print, and dark. +// Extracted so that tests can call this mixin directly with a custom $utilities map +// rather than having to mirror the loop conditions inline. +@mixin generate-utilities-loop($utilities, $breakpoints) { + // Base + responsive (one pass per breakpoint) + @each $breakpoint in map.keys($breakpoints) { + @include bp.media-breakpoint-up($breakpoint, $breakpoints) { + $prefix: bp.breakpoint-prefix($breakpoint, $breakpoints); + + @each $key, $utility in $utilities { + @if meta.type-of($utility) == "map" and map.get($utility, enabled) != false and (map.get($utility, responsive) or $prefix == "") { + @include generate-utility($utility, $prefix); + } + } + } + } + + // Print utilities + @media print { + @each $key, $utility in $utilities { + @if meta.type-of($utility) == "map" and map.get($utility, enabled) != false and map.get($utility, print) == true { + @include generate-utility($utility, "print\\:"); + } + } + } + + // Dark utilities + @media (prefers-color-scheme: dark) { + @each $key, $utility in $utilities { + @if meta.type-of($utility) == "map" and map.get($utility, enabled) != false and map.get($utility, dark) == true { + @include generate-utility($utility, "dark\\:"); + } + } + } +} diff --git a/scss/tests/jasmine.cjs b/scss/tests/jasmine.cjs index edfdf2906a..cdddaa25d1 100644 --- a/scss/tests/jasmine.cjs +++ b/scss/tests/jasmine.cjs @@ -8,7 +8,7 @@ module.exports = { spec_dir: 'scss', // Make Jasmine look for `.test.scss` files // spec_files: ['**/*.{test,spec}.scss'], - spec_files: ['**/_utilities.test.scss', '**/modules/_configuration.test.scss', '**/modules/_root-tokens.test.scss', '**/forms/_validation.test.scss'], + spec_files: ['**/_utilities.test.scss', '**/utilities/_api.test.scss', '**/modules/_configuration.test.scss', '**/modules/_root-tokens.test.scss', '**/forms/_validation.test.scss'], // Compile them into JS scripts running `sass-true` requires: [path.join(__dirname, 'sass-true/register.cjs')], // Ensure we use `require` so that the require.extensions works diff --git a/scss/tests/mixins/_utilities.test.scss b/scss/tests/mixins/_utilities.test.scss index bfcfcbbee5..83d8f3ab27 100644 --- a/scss/tests/mixins/_utilities.test.scss +++ b/scss/tests/mixins/_utilities.test.scss @@ -382,4 +382,213 @@ $true-terminal-output: false; } } } + + @include describe("selector") { + @include it("generates an attr-starts selector") { + @include test-generate-utility( + ( + selector: "attr-starts", + class: "ratio-", + property: aspect-ratio, + values: var(--ratio), + ) + ) { + [class^="ratio-"] { + aspect-ratio: var(--ratio); + } + } + } + + @include it("generates an attr-includes selector") { + @include test-generate-utility( + ( + selector: "attr-includes", + class: "ratio-", + property: aspect-ratio, + values: var(--ratio), + ) + ) { + [class*="ratio-"] { + aspect-ratio: var(--ratio); + } + } + } + } + + @include describe("child-selector") { + @include it("wraps the selector in :where() with the child selector appended") { + @include test-generate-utility( + ( + property: margin-inline-end, + class: space-x, + child-selector: "> :not(:last-child)", + values: (1: .25rem, 2: .5rem) + ) + ) { + :where(.space-x-1 > :not(:last-child)) { + margin-inline-end: .25rem; + } + + :where(.space-x-2 > :not(:last-child)) { + margin-inline-end: .5rem; + } + } + } + + @include it("wraps state variant selectors in :where() as well") { + @include test-generate-utility( + ( + property: margin-inline-end, + class: space-x, + child-selector: "> :not(:last-child)", + state: hover, + values: (1: .25rem) + ) + ) { + :where(.space-x-1 > :not(:last-child)) { + margin-inline-end: .25rem; + } + + :where(.hover\:space-x-1:hover > :not(:last-child)) { + margin-inline-end: .25rem; + } + } + } + } + + @include describe("variables") { + @include it("emits each list variable with the utility value") { + @include test-generate-utility( + ( + property: color, + class: link, + variables: link-color, + values: (10: 10%, 50: 50%) + ) + ) { + .link-10 { + --link-color: 10%; + color: 10%; + } + + .link-50 { + --link-color: 50%; + color: 50%; + } + } + } + + @include it("emits each map variable with its static value") { + @include test-generate-utility( + ( + property: text-decoration-color, + class: link-underline, + variables: ("link-underline-opacity": 1), + values: (primary: #06c, danger: #c00) + ) + ) { + .link-underline-primary { + --link-underline-opacity: 1; + text-decoration-color: #06c; + } + + .link-underline-danger { + --link-underline-opacity: 1; + text-decoration-color: #c00; + } + } + } + + @include it("emits variables in state variant blocks too") { + @include test-generate-utility( + ( + property: color, + class: link, + variables: link-color, + state: hover, + values: (10: 10%) + ) + ) { + .link-10 { + --link-color: 10%; + color: 10%; + } + + .hover\:link-10:hover { + --link-color: 10%; + color: 10%; + } + } + } + } + + @include describe("property map") { + @include it("uses null default to pass the utility value to a property") { + @include test-generate-utility( + ( + property: ( + "--bg": null, + "background-color": var(--bg) + ), + class: bg, + values: (primary: #06c, danger: #c00) + ) + ) { + .bg-primary { + --bg: #06c; + background-color: var(--bg); + } + + .bg-danger { + --bg: #c00; + background-color: var(--bg); + } + } + } + + @include it("uses a per-value map to override individual properties") { + @include test-generate-utility( + ( + property: ( + "font-size": 1rem, + "line-height": 1.5 + ), + class: text, + values: ( + "sm": ("font-size": .875rem, "line-height": 1.25), + "lg": ("font-size": 1.25rem, "line-height": 1.75), + ) + ) + ) { + .text-sm { + font-size: .875rem; + line-height: 1.25; + } + + .text-lg { + font-size: 1.25rem; + line-height: 1.75; + } + } + } + + @include it("applies !important to all mapped properties") { + @include test-generate-utility( + ( + property: ( + "--bg": null, + "background-color": var(--bg) + ), + class: bg, + important: true, + values: (primary: #06c) + ) + ) { + .bg-primary { + --bg: #06c !important; + background-color: var(--bg) !important; + } + } + } + } } diff --git a/scss/tests/utilities/_api.test.scss b/scss/tests/utilities/_api.test.scss index 00dbc4ee9f..e40ee9dc3b 100644 --- a/scss/tests/utilities/_api.test.scss +++ b/scss/tests/utilities/_api.test.scss @@ -1,125 +1,193 @@ // check-unused-imports-disable — test infrastructure imports. -@use "../../functions"; -@use "../../config"; -@use "../../maps"; -@use "../../mixins"; - -$utilities: (); - -@include describe("utilities/api") { - @include it("generates utilities for each breakpoints") { - $utilities: ( - margin: ( - property: margin, - important: true, - values: auto - ), - padding: ( - property: padding, - responsive: true, - important: true, - values: 1rem - ), - font-size: ( - property: font-size, - values: (large: 1.25rem), - important: true, - print: true - ) - ) !global; - - $breakpoints: ( - xs: 0, - sm: 333px, - md: 666px - ) !global; - - @include assert() { - @include output() { - @use "../../utilities/api"; - } +@use "sass:map"; +@use "../../config" as *; +@use "../../functions" as *; +@use "../../mixins/utilities" as *; + +// Integration-style tests for the utilities API loop. +// Each test calls generate-utilities-loop — the same mixin that _api.scss calls — so +// a logic change in the real loop is immediately caught here without any mirrored conditions. +// +// A minimal two-breakpoint map is used to keep output predictable and tests fast. +$test-breakpoints: ( + xs: 0, + sm: 576px, +); + +@include describe("utilities/api loop conditions") { + @include describe("enabled") { + @include it("skips utilities with enabled: false") { + $test-utilities: ( + opacity: ( + property: opacity, + values: (50: .5) + ), + float: ( + property: float, + enabled: false, + values: (start: inline-start, end: inline-end) + ) + ); - @include expect() { - // margin is not set to responsive - .margin-auto { - margin: auto !important; + @include assert() { + @include output() { + @include generate-utilities-loop($test-utilities, $test-breakpoints); } - // padding is, though - .padding-1rem { - padding: 1rem !important; + @include expect() { + .opacity-50 { + opacity: .5; + } } + } + } - .font-size-large { - font-size: 1.25rem !important; + @include it("outputs utilities without an explicit enabled key") { + $test-utilities: ( + opacity: ( + property: opacity, + values: (25: .25) + ) + ); + + @include assert() { + @include output() { + @include generate-utilities-loop($test-utilities, $test-breakpoints); } - @media (width >= 333px) { - .sm\:padding-1rem { - padding: 1rem !important; + @include expect() { + .opacity-25 { + opacity: .25; } } + } + } + } - @media (width >= 666px) { - .md\:padding-1rem { - padding: 1rem !important; - } + @include describe("responsive") { + @include it("generates responsive classes only for utilities with responsive: true") { + $test-utilities: ( + opacity: ( + property: opacity, + responsive: true, + values: (50: .5) + ), + float: ( + property: float, + values: (start: inline-start) + ) + ); + + @include assert() { + @include output() { + @include generate-utilities-loop($test-utilities, $test-breakpoints); } - @media print { - .print\:font-size-large { - font-size: 1.25rem !important; + @include expect() { + .opacity-50 { + opacity: .5; + } + + .float-start { + float: inline-start; + } + + @media (width >= 576px) { + .sm\:opacity-50 { + opacity: .5; + } } } } - } - } - @include it("generates utilities without !important when important: false or not set") { - $utilities: ( - opacity: ( - property: opacity, - values: ( - 0: 0, - 50: .5, - 100: 1 + @include it("does not generate responsive classes for utilities without responsive: true") { + $test-utilities: ( + float: ( + property: float, + values: (start: inline-start) ) - ), - text-align: ( - property: text-align, - important: false, - values: ( - start: left, - center: center - ) - ) - ) !global; + ); - @include assert() { - @include output() { - @import "../../utilities/api"; - } + @include assert() { + @include output() { + @include generate-utilities-loop($test-utilities, $test-breakpoints); + } - @include expect() { - .opacity-0 { - opacity: 0; + @include expect() { + .float-start { + float: inline-start; + } + // nothing at sm — responsive is not set } + } + } + } - .opacity-50 { - opacity: .5; + @include describe("dark") { + @include it("generates dark-prefixed classes for utilities with dark: true") { + $test-utilities: ( + opacity: ( + property: opacity, + values: (50: .5) + ), + bg: ( + property: background-color, + class: bg, + dark: true, + values: (white: #fff, black: #000) + ) + ); + + @include assert() { + @include output() { + @include generate-utilities-loop($test-utilities, $test-breakpoints); } - .opacity-100 { - opacity: 1; + @include expect() { + .opacity-50 { + opacity: .5; + } + + .bg-white { + background-color: #fff; + } + + .bg-black { + background-color: #000; + } + + @media (prefers-color-scheme: dark) { + .dark\:bg-white { + background-color: #fff; + } + + .dark\:bg-black { + background-color: #000; + } + } } + } + } - .text-align-start { - text-align: left; + @include it("does not generate dark classes for utilities without dark: true") { + $test-utilities: ( + opacity: ( + property: opacity, + values: (50: .5) + ) + ); + + @include assert() { + @include output() { + @include generate-utilities-loop($test-utilities, $test-breakpoints); } - .text-align-center { - text-align: center; + @include expect() { + .opacity-50 { + opacity: .5; + } + // nothing in @media (prefers-color-scheme: dark) — dark is not set } } } diff --git a/scss/utilities/_api.scss b/scss/utilities/_api.scss index 346eb22331..327573a398 100644 --- a/scss/utilities/_api.scss +++ b/scss/utilities/_api.scss @@ -1,37 +1,7 @@ -@use "sass:map"; -@use "sass:meta"; @use "../config" as *; -@use "../layout/breakpoints" as *; @use "../mixins/utilities" as *; @use "../utilities" as *; @layer utilities { - // Loop over each breakpoint - @each $breakpoint in map.keys($breakpoints) { - - // Generate media query if needed - @include media-breakpoint-up($breakpoint) { - $prefix: breakpoint-prefix($breakpoint, $breakpoints); - - // Loop over each utility property - @each $key, $utility in $utilities { - // The utility can be disabled with `false`, thus check if the utility is a map first - // Only proceed if responsive media queries are enabled or if it's the base media query - @if meta.type-of($utility) == "map" and (map.get($utility, responsive) or $prefix == "") { - @include generate-utility($utility, $prefix); - } - } - } - } - - // Print utilities - @media print { - @each $key, $utility in $utilities { - // The utility can be disabled with `false`, thus check if the utility is a map first - // Then check if the utility needs print styles - @if meta.type-of($utility) == "map" and map.get($utility, print) == true { - @include generate-utility($utility, "print\\:"); - } - } - } + @include generate-utilities-loop($utilities, $breakpoints); } diff --git a/site/src/components/PageMeta.astro b/site/src/components/PageMeta.astro index 46cf4fcf54..1e7971e31d 100644 --- a/site/src/components/PageMeta.astro +++ b/site/src/components/PageMeta.astro @@ -39,7 +39,7 @@ const deps = frontmatter.deps ?? [] const depsCount = deps.length --- -
+
{ frontmatter.js === 'required' && (
diff --git a/site/src/content/docs/getting-started/approach.mdx b/site/src/content/docs/getting-started/approach.mdx index 4e13579954..bfb6bd6c62 100644 --- a/site/src/content/docs/getting-started/approach.mdx +++ b/site/src/content/docs/getting-started/approach.mdx @@ -13,7 +13,7 @@ Bootstrap 6 brings with it a new philosophy of using Sass and CSS together to bu Previous major versions have treated Sass as both programmatic and visual customization with CSS variables adding a more complicated, and less complete, secondary layer. This has been greatly improved in v6. -On top of that, we use [PostCSS](https://postcss.org/) to prefix our CSS variables with the `bs-` prefix and add vendor prefixes to our CSS properties, as appropriate. +On top of that, we use [PostCSS](https://postcss.org/) to [prefix our CSS variables](https://github.com/twbs/postcss-prefix-custom-properties) with the `bs-` prefix and add vendor prefixes to our CSS properties, as appropriate. ### Sass diff --git a/site/src/content/docs/utilities/api.mdx b/site/src/content/docs/utilities/api.mdx index 4c6768967f..c84ffe4604 100644 --- a/site/src/content/docs/utilities/api.mdx +++ b/site/src/content/docs/utilities/api.mdx @@ -15,7 +15,7 @@ The `$utilities` map contains all our utilities and is later merged with your cu | Option | Type | Default value | Description | | --- | --- | --- | --- | -| [`property`](#property) | **Required** | – | Name of the property, this can be a string or an array of strings (e.g., horizontal paddings or margins). | +| [`property`](#property) | **Required** | – | Name of the property. Can be a string, a space-separated list of strings (e.g., horizontal paddings or margins), or a **map** of property-to-default-value pairs (see [Property-Value Mapping](#property-value-mapping)). | | [`values`](#values) | **Required** | – | List of values, or a map if you don’t want the class name to be the same as the value. If `null` is used as map key, `class` is not prepended to the class name. | | [`selector`](#selector) | Optional | `class` | Type of CSS selector in the generated CSS ruleset. Can be `class`, `attr-starts`, or `attr-includes`. | | [`child-selector`](#child-selector) | Optional | null | A child/descendant selector appended to the utility’s selector, wrapped in `:where()` for zero specificity. Use to target children instead of the element itself. | @@ -25,6 +25,8 @@ The `$utilities` map contains all our utilities and is later merged with your cu | [`responsive`](#responsive) | Optional | `false` | Boolean indicating if responsive classes should be generated. | | [`important`](#importance) | Optional | `false` | Boolean indicating if `!important` should be added to the utility’s CSS rules. | | [`print`](#print) | Optional | `false` | Boolean indicating if print classes need to be generated. | +| [`dark`](#dark) | Optional | `false` | Boolean indicating if `dark:` prefixed classes should be generated, scoped to `@media (prefers-color-scheme: dark)`. | +| [`enabled`](#enabled) | Optional | `true` | Set to `false` to suppress output for a utility while keeping its definition in the `$utilities` map. | ## API explained @@ -58,7 +60,7 @@ Which outputs the following: ### `property` -
Required
+
Required
The `property` key must be set for any utility, and it must contain a valid CSS property. This property is used in the generated utility’s ruleset. When the `class` key is omitted, it also serves as the default class name. Consider the `text-decoration` utility: @@ -79,9 +81,69 @@ Output: .text-decoration-line-through { text-decoration: line-through; } ``` +#### Property value mapping + +The `property` key also accepts a **map** of CSS property names to default values. This lets a single utility class set multiple properties at once, and lets values override individual properties when the value is itself a map. + +When a property's default value in the map is `null`, the utility value is used directly for that property. When a default value is set, it is used unless the utility's value map contains a matching key. + +A common pattern is pairing a CSS custom property (which receives the utility value) with a regular property that references it: + +```scss +$utilities: ( + "bg-color": ( + property: ( + "--bg": null, + "background-color": var(--bg) + ), + class: bg, + values: ( + primary: var(--blue-500), + danger: var(--red-500), + ) + ) +); +``` + +Output: + +```css +.bg-primary { --bs-bg: var(--bs-blue-500); background-color: var(--bs-bg); } +.bg-danger { --bs-bg: var(--bs-red-500); background-color: var(--bs-bg); } +``` + + + Source CSS variables are used without a `--bs-` prefix, which is added in our compiled CSS with [our PostCSS plugin](https://github.com/twbs/postcss-prefix-custom-properties). + + +You can also set per-value overrides. When `values` contains a nested map whose keys match properties in the `property` map, those values are used for the matching property: + +```scss +$utilities: ( + "text-size": ( + property: ( + "font-size": 1rem, + "line-height": 1.5 + ), + class: text, + values: ( + "sm": ("font-size": .875rem, "line-height": 1.25), + "lg": ("font-size": 1.25rem, "line-height": 1.75), + ) + ) +); +``` + +Output: + +```css +.text-sm { font-size: .875rem; line-height: 1.25; } +.text-lg { font-size: 1.25rem; line-height: 1.75; } +``` + ### `values` -
Required
+
Required
Use the `values` key to specify which values for the specified `property` should be used in the generated class names and rules. Can be a list or map (set in the utilities or in a Sass variable). @@ -293,7 +355,7 @@ $utilities: ( "link-opacity": ( property: color, class: link, - variables: (link-color), + variables: link-color, values: ( 10: 10%, 50: 50%, @@ -476,6 +538,52 @@ Output: } ``` +### Dark + +Enabling the `dark` option generates `dark:` prefixed classes scoped to `@media (prefers-color-scheme: dark)`. This follows the same prefix convention as responsive and print utilities. + +```scss +$utilities: ( + "bg-color": ( + property: background-color, + class: bg, + dark: true, + values: ( + white: #fff, + black: #000, + ) + ) +); +``` + +Output: + +```css +.bg-white { background-color: #fff; } +.bg-black { background-color: #000; } + +@media (prefers-color-scheme: dark) { + .dark\:bg-white { background-color: #fff; } + .dark\:bg-black { background-color: #000; } +} +``` + +### Enabled + +Set `enabled: false` to suppress output for a utility without removing it from the `$utilities` map. This is useful when you want to keep the utility's definition available for reference or downstream `map.get()` calls, but don't want it to emit any CSS. + +```scss +$utilities: map.merge( + $utilities, + ( + "float": map.merge( + map.get($utilities, "float"), + ( enabled: false ), + ), + ) +); +``` + ## Importance Utilities generated by the API no longer include `!important` by default in v6. This is because we now use CSS layers to ensure utilities override components and modifier classes as intended. You can enable `!important` on a per-utility basis by setting the `important` option to `true`. @@ -510,30 +618,40 @@ This will generate utilities with `!important`: Now that you’re familiar with how the utilities API works, learn how to add your own custom classes and modify our default utilities. +The examples below use two different import styles depending on the use case: + +- **`@use "bootstrap" with (...)`**—Use this when compiling the full Bootstrap framework from a single entrypoint. Configuration is passed via `with (...)` and Bootstrap handles merging in one step. +- **`@use "bootstrap/scss/utilities" as *` + `@use "bootstrap/scss/utilities/api"`**—Use this for focused, utility-only builds where you need to read from or merge the existing `$utilities` map before passing it to the API layer. Using `as *` avoids a namespace prefix so you can reference `$utilities` directly. + ### Override utilities -Override existing utilities by using the same key. For example, if you want additional responsive overflow utility classes, you can do this: +Override existing utilities by using the same key and passing the new definition via the `$utilities` configuration variable when loading Bootstrap. For example, take our existing overflow utility class definition: + + + +If you want to make those responsive, and adjust the values used, you can do this: ```scss -$utilities: ( - "overflow": ( - responsive: true, - property: overflow, - values: visible hidden scroll auto, - ), +// my-bootstrap.scss +@use "bootstrap" with ( + $utilities: ( + "overflow": ( + responsive: true, + // property: overflow, + values: visible hidden scroll auto, + ), + ) ); ``` ### Add utilities -New utilities can be added to the default `$utilities` map with a `map.merge`. Make sure our required Sass files and `_utilities.scss` are loaded first, then use `map.merge` to add your additional utilities. For example, here’s how to add a responsive `cursor` utility with three values. +New utilities can be added to the default `$utilities` map by passing them via the `$utilities` configuration variable when loading Bootstrap. Bootstrap merges your map on top of its defaults, so only supply the new utilities you want to add. ```scss -@use "bootstrap/scss/bootstrap" as *; - -$utilities: map.merge( - $utilities, - ( +// my-bootstrap.scss +@use "bootstrap" with ( + $utilities: ( "cursor": ( property: cursor, class: cursor, @@ -542,16 +660,16 @@ $utilities: map.merge( ) ) ); - -@use "bootstrap/scss/utilities/api"; ``` ### Modify utilities -Modify existing utilities in the default `$utilities` map with `map.get` and `map.merge` functions. In the example below, we’re adding an additional value to the `width` utilities. Start with an initial `map.merge` and then specify which utility you want to modify. From there, fetch the nested `"width"` map with `map.get` to access and modify the utility’s options and values. +Modify existing utilities by passing a modified copy of the utility via the `$utilities` configuration variable. Use `map.get` and `map.merge` to build the new value from the existing definition. In the example below, we’re adding an additional value to the `width` utilities: ```scss -@use "bootstrap/scss/bootstrap" as *; +// my-bootstrap.scss +@use "sass:map"; +@use "bootstrap/scss/utilities" as *; $utilities: map.merge( $utilities, @@ -568,7 +686,7 @@ $utilities: map.merge( ) ); -@use "bootstrap/scss/utilities/api"; +@use "bootstrap/scss/utilities/api" with ($utilities: $utilities); ``` #### Enable responsive @@ -576,7 +694,8 @@ $utilities: map.merge( You can enable responsive classes for an existing set of utilities that are not currently responsive by default. For example, to make the `border` classes responsive: ```scss -@use "bootstrap/scss/bootstrap" as *; +@use "sass:map"; +@use "bootstrap/scss/utilities" as *; $utilities: map.merge( $utilities, @@ -588,7 +707,7 @@ $utilities: map.merge( ) ); -@use "bootstrap/scss/utilities/api"; +@use "bootstrap/scss/utilities/api" with ($utilities: $utilities); ``` This will now generate responsive variations of `.border` and `.border-0` for each breakpoint. Your generated CSS will look like this: @@ -628,7 +747,8 @@ This will now generate responsive variations of `.border` and `.border-0` for ea Missing v4 utilities, or used to another naming convention? The utilities API can be used to override the resulting `class` of a given utility—for example, to rename `.ms-*` utilities to oldish `.ml-*`: ```scss -@use "bootstrap/scss/bootstrap" as *; +@use "sass:map"; +@use "bootstrap/scss/utilities" as *; $utilities: map.merge( $utilities, @@ -640,7 +760,7 @@ $utilities: map.merge( ) ); -@use "bootstrap/scss/utilities/api"; +@use "bootstrap/scss/utilities/api" with ($utilities: $utilities); ``` ### Remove utilities @@ -648,17 +768,19 @@ $utilities: map.merge( Remove any of the default utilities with the [`map.remove()` Sass function](https://sass-lang.com/documentation/modules/map/#remove). ```scss -@use "bootstrap/scss/bootstrap" as *; +@use "sass:map"; +@use "bootstrap/scss/utilities" as *; $utilities: map.remove($utilities, "width", "float"); -@use "bootstrap/scss/utilities/api"; +@use "bootstrap/scss/utilities/api" with ($utilities: $utilities); ``` You can also use the [`map.merge()` Sass function](https://sass-lang.com/documentation/modules/map/#merge) and set the group key to `null` to remove the utility. ```scss -@use "bootstrap/scss/bootstrap" as *; +@use "sass:map"; +@use "bootstrap/scss/utilities" as *; $utilities: map.merge( $utilities, @@ -667,7 +789,7 @@ $utilities: map.merge( ) ); -@use "bootstrap/scss/utilities/api"; +@use "bootstrap/scss/utilities/api" with ($utilities: $utilities); ``` ### Add, remove, modify @@ -675,7 +797,8 @@ $utilities: map.merge( You can add, remove, and modify many utilities all at once with the [`map.merge()` Sass function](https://sass-lang.com/documentation/modules/map/#merge). Here’s how you can combine the previous examples into one larger map. ```scss -@use "bootstrap/scss/bootstrap" as *; +@use "sass:map"; +@use "bootstrap/scss/utilities" as *; $utilities: map.merge( $utilities, @@ -697,5 +820,5 @@ $utilities: map.merge( ) ); -@use "bootstrap/scss/utilities/api"; +@use "bootstrap/scss/utilities/api" with ($utilities: $utilities); ``` diff --git a/site/src/content/docs/utilities/border-radius.mdx b/site/src/content/docs/utilities/border-radius.mdx index bbcb41098a..5e3b36373f 100644 --- a/site/src/content/docs/utilities/border-radius.mdx +++ b/site/src/content/docs/utilities/border-radius.mdx @@ -42,6 +42,20 @@ Use the scaling classes for larger or smaller rounded corners. Sizes range from `} /> +## Mixing + +Use `.rounded-size-{n}` to set the radius size without rounding all corners. Combine it with side classes like `.rounded-top` or `.rounded-end` to round only specific sides at that size. Because these use CSS variables for sizing, you may need to apply additional classes to nested elements to prevent inheritance. + + + + +`} /> + +When `.rounded-{n}` is used alongside a side class, the number sets the `--rounded-size` variable that the side class inherits. + + +`} /> + ## CSS ### Variables @@ -50,6 +64,8 @@ Radius tokens are generated from the `$spacers` map in `scss/_root.scss`. [Learn +The `--rounded-size` local CSS variable is set by `.rounded-{n}` and `.rounded-size-{n}` and consumed by the side utilities (`.rounded-top`, `.rounded-end`, `.rounded-bottom`, `.rounded-start`) as their default radius. You can also set it directly in your own CSS or via an inline style to control the side utilities from a single point. + ### Sass maps ### Sass mixins diff --git a/site/src/content/docs/utilities/text-wrapping.mdx b/site/src/content/docs/utilities/text-wrapping.mdx index e64ad49058..a3cca84464 100644 --- a/site/src/content/docs/utilities/text-wrapping.mdx +++ b/site/src/content/docs/utilities/text-wrapping.mdx @@ -40,18 +40,6 @@ Prevent long strings of text from breaking your components' layout by using `.te Text wrapping utilities are declared in our utilities API in `scss/_utilities.scss`. [Learn how to use the utilities API.]([[docsref:/utilities/api#using-the-api]]) -```scss -"white-space": ( - property: white-space, - class: text, - values: ( - wrap: normal, - nowrap: nowrap, - ) -), -"word-wrap": ( - property: word-wrap word-break, - class: text, - values: (break: break-word), -), -``` + + +