diff --git a/core/scss/_bootstrap-config.scss b/core/scss/_bootstrap-config.scss index 8e93b399a..ec03a0be8 100644 --- a/core/scss/_bootstrap-config.scss +++ b/core/scss/_bootstrap-config.scss @@ -1,5 +1,7 @@ // Config @import "bootstrap/scss/functions"; +@import "bootstrap-functions"; + @import "bootstrap/scss/variables"; @import "bootstrap/scss/variables-dark"; @import "bootstrap/scss/maps"; diff --git a/core/scss/_bootstrap-functions.scss b/core/scss/_bootstrap-functions.scss index b1264410a..3ff11830d 100644 --- a/core/scss/_bootstrap-functions.scss +++ b/core/scss/_bootstrap-functions.scss @@ -1,6 +1,258 @@ - @use "sass:color"; +@use "sass:map"; +@use "sass:math"; +@use "sass:meta"; +@use "sass:string"; +@use "sass:list"; +/** + * Converts a color value to RGB format + * + * @param {Color} $value - The color value to convert + * @return {List} - RGB values as a list (red, green, blue) + * + * @example + * to-rgb(#ff0000) // Returns: (255, 0, 0) + */ @function to-rgb($value) { - @return color.red($value), color.green($value), color.blue($value); + @return color.channel($value, "red", $space: rgb), color.channel($value, "green", $space: rgb), color.channel($value, "blue", $space: rgb); +} + +/** + * Creates an opaque color by mixing a foreground color with a background color + * + * @param {Color} $background - The background color + * @param {Color} $foreground - The foreground color to make opaque + * @return {Color} - The resulting opaque color + * + * @example + * opaque(#ffffff, rgba(255, 0, 0, 0.5)) // Returns solid red color + */ +@function opaque($background, $foreground) { + @return color.mix(rgba($foreground, 1), $background, opacity($foreground) * 100%); +} + +/** + * Adjusts a color channel value for luminance calculation + * + * @param {Number} $channel - The color channel value (0-1) + * @return {Number} - The adjusted channel value + * + * @example + * adjust(0.5) // Returns adjusted value for luminance calculation + */ +@function adjust($channel) { + @return if($channel < 0.03928, math.div($channel, 12.92), pow(($channel + 0.055) / 1.055, 2.4)); +} + +/** + * Calculates the relative luminance of a color using WCAG formula + * + * @param {Color} $color - The color to calculate luminance for + * @return {Number} - The luminance value (0-1) + * + * @example + * luminance(#ffffff) // Returns: 1 + * luminance(#000000) // Returns: 0 + */ +@function luminance($color) { + $r: math.div(color.channel($color, "red", $space: rgb), 255); + $g: math.div(color.channel($color, "green", $space: rgb), 255); + $b: math.div(color.channel($color, "blue", $space: rgb), 255); + + $r: adjust($r); + $g: adjust($g); + $b: adjust($b); + + @return 0.2126 * $r + 0.7152 * $g + 0.0722 * $b; +} + +/** + * Determines the best contrasting color for a given background + * + * @param {Color} $background - The background color + * @param {Color} $light - The light color option (default: #ffffff) + * @param {Color} $dark - The dark color option (default: #000000) + * @return {Color} - The contrasting color (light or dark) + * + * @example + * color-contrast(#000000) // Returns: #ffffff + * color-contrast(#ffffff) // Returns: #000000 + */ +@function color-contrast($background, $light: #ffffff, $dark: #000000) { + // Get relative luminance using WCAG formula + + $bg-luminance: luminance($background); + + // If background is dark, return light color, otherwise dark + @return if($bg-luminance > 0.179, $dark, $light); +} + +/** + * Adds two values together, handling different data types + * + * @param {*} $value1 - First value to add + * @param {*} $value2 - Second value to add + * @param {Boolean} $return-calc - Whether to return calc() function (default: true) + * @return {*} - The result of addition or calc() expression + * + * @example + * add(10px, 5px) // Returns: 15px + * add(10px, 5px, false) // Returns: 10px + 5px + */ +@function add($value1, $value2, $return-calc: true) { + @if $value1 == null { + @return $value2; + } + + @if $value2 == null { + @return $value1; + } + + @if meta.type-of($value1) == number and meta.type-of($value2) == number and math.compatible($value1, $value2) { + @return $value1 + $value2; + } + + @return if($return-calc == true, calc(#{$value1} + #{$value2}), $value1 + string.unquote(" + ") + $value2); +} + +/** + * Subtracts the second value from the first value + * + * @param {*} $value1 - The value to subtract from + * @param {*} $value2 - The value to subtract + * @param {Boolean} $return-calc - Whether to return calc() function (default: true) + * @return {*} - The result of subtraction or calc() expression + * + * @example + * subtract(10px, 5px) // Returns: 5px + * subtract(10px, 5px, false) // Returns: 10px - 5px + */ +@function subtract($value1, $value2, $return-calc: true) { + @if $value1 == null and $value2 == null { + @return null; + } + + @if $value1 == null { + @return -$value2; + } + + @if $value2 == null { + @return $value1; + } + + @if meta.type-of($value1) == number and meta.type-of($value2) == number and math.compatible($value1, $value2) { + @return $value1 - $value2; + } + + @if meta.type-of($value2) != number { + $value2: string.unquote("(") + $value2 + string.unquote(")"); + } + + @return if($return-calc == true, calc(#{$value1} - #{$value2}), $value1 + unquote(" - ") + $value2); +} + +/** + * Divides the dividend by the divisor with specified precision + * + * @param {Number} $dividend - The number to be divided + * @param {Number} $divisor - The number to divide by + * @param {Number} $precision - The number of decimal places (default: 10) + * @return {Number} - The result of division + * + * @example + * divide(10, 2) // Returns: 5 + * divide(10px, 2) // Returns: 5px + * divide(10, 3) // Returns: 3.3333333333 + */ +@function divide($dividend, $divisor, $precision: 10) { + $sign: if($dividend > 0 and $divisor > 0 or $dividend < 0 and $divisor < 0, 1, -1); + $dividend: abs($dividend); + $divisor: abs($divisor); + @if $dividend == 0 { + @return 0; + } + @if $divisor == 0 { + @error "Cannot divide by 0"; + } + $remainder: $dividend; + $result: 0; + $factor: 10; + @while ($remainder > 0 and $precision >= 0) { + $quotient: 0; + @while ($remainder >= $divisor) { + $remainder: $remainder - $divisor; + $quotient: $quotient + 1; + } + $result: $result * 10 + $quotient; + $factor: $factor * 0.1; + $remainder: $remainder * 10; + $precision: $precision - 1; + @if ($precision < 0 and $remainder >= $divisor * 5) { + $result: $result + 1; + } + } + $result: $result * $factor * $sign; + $dividend-unit: math.unit($dividend); + $divisor-unit: math.unit($divisor); + $unit-map: ( + "px": 1px, + "rem": 1rem, + "em": 1em, + "%": 1%, + ); + @if ($dividend-unit != $divisor-unit and map.has-key($unit-map, $dividend-unit)) { + $result: $result * map.get($unit-map, $dividend-unit); + } + @return $result; +} + +/** + * Replaces all occurrences of a search string with a replace string + * + * @param {String} $string - The string to search in + * @param {String} $search - The string to search for + * @param {String} $replace - The string to replace with (default: "") + * @return {String} - The string with replacements made + * + * @example + * str-replace("hello world", "world", "universe") // Returns: "hello universe" + * str-replace("hello world", "o") // Returns: "hell wrld" + */ +@function str-replace($string, $search, $replace: "") { + $index: string.index($string, $search); + + @if $index { + @return string.slice($string, 1, $index - 1) + $replace + str-replace(string.slice($string, $index + string.length($search)), $search, $replace); + } + + @return $string; +} + +/** + * Applies a function to each key-value pair in a map + * + * @param {Map} $map - The map to iterate over + * @param {String} $func - The name of the function to apply + * @param {*} $args... - Additional arguments to pass to the function + * @return {Map} - A new map with the function applied to each value + * + * @example + * $colors: (primary: #007bff, secondary: #6c757d); + * map-loop($colors, darken, 10%) // Returns map with darkened colors + */ +@function map-loop($map, $func, $args...) { + $_map: (); + + @each $key, $value in $map { + // allow to pass the $key and $value of the map as an function argument + $_args: (); + @each $arg in $args { + $_args: list.append($_args, if($arg == "$key", $key, if($arg == "$value", $value, $arg))); + } + + $_map: map.merge($_map, ($key: meta.call(meta.get-function($func), $_args...))); + } + + @return $_map; } \ No newline at end of file diff --git a/core/scss/_bootstrap-override.scss b/core/scss/_bootstrap-override.scss index 6b7367b32..d5d7db173 100644 --- a/core/scss/_bootstrap-override.scss +++ b/core/scss/_bootstrap-override.scss @@ -70,9 +70,4 @@ // Override bootstrap core } -// -// TODO: remove when https://github.com/twbs/bootstrap/pull/37425/ will be released -// -@function opaque($background, $foreground) { - @return mix(rgba($foreground, 1), $background, opacity($foreground) * 100%); -} + diff --git a/core/scss/mixins/_functions.scss b/core/scss/mixins/_functions.scss index 35a937a89..5f845cb86 100644 --- a/core/scss/mixins/_functions.scss +++ b/core/scss/mixins/_functions.scss @@ -1,26 +1,46 @@ @use "sass:math"; +/** + * Creates a lighter version of a theme color by mixing it with a background color + * + * @param {Color} $color - The base color to lighten + * @param {Color} $background - The background color to mix with (default: #fff) + * @return {Color} - The lighter version of the color (10% mix) + * + * @example + * theme-color-lighter(#007bff) // Returns lighter blue + * theme-color-lighter(#dc3545, #f8f9fa) // Returns lighter red mixed with light gray + */ @function theme-color-lighter($color, $background: #fff) { @return mix($color, $background, 10%); } +/** + * Creates a darker version of a theme color by shading it + * + * @param {Color} $color - The base color to darken + * @return {Color} - The darker version of the color (10% shade) + * + * @example + * theme-color-darker(#007bff) // Returns darker blue + * theme-color-darker(#28a745) // Returns darker green + */ @function theme-color-darker($color) { @return shade-color($color, 10%); } -// -// Replace all occurrences of a substring within a string. -// -@function str-replace($string, $search, $replace: "") { - $index: str-index($string, $search); - - @if $index { - @return str-slice($string, 1, $index - 1) + $replace + str-replace(str-slice($string, $index + str-length($search)), $search, $replace); - } - - @return $string; -} - +/** + * Media query mixin for breakpoints smaller than the specified breakpoint + * + * @param {String} $name - The breakpoint name to target + * @param {Map} $breakpoints - The breakpoints map (default: $grid-breakpoints) + * @content - The CSS content to apply within the media query + * + * @example + * @include media-breakpoint-down-than(lg) { + * .container { max-width: 100%; } + * } + */ @mixin media-breakpoint-down-than($name, $breakpoints: $grid-breakpoints) { $prev: breakpoint-prev($name); @@ -39,6 +59,19 @@ } } +/** + * Gets the previous breakpoint name in the breakpoints map + * + * @param {String} $name - The current breakpoint name + * @param {Map} $breakpoints - The breakpoints map (default: $grid-breakpoints) + * @param {List} $breakpoint-names - List of breakpoint names (default: map-keys($breakpoints)) + * @return {String|null} - The previous breakpoint name or null if none exists + * + * @example + * breakpoint-prev(lg) // Returns: md + * breakpoint-prev(sm) // Returns: xs + * breakpoint-prev(xs) // Returns: null + */ @function breakpoint-prev($name, $breakpoints: $grid-breakpoints, $breakpoint-names: map-keys($breakpoints)) { $n: index($breakpoint-names, $name); @if not $n { @@ -47,9 +80,15 @@ @return if($n > 1, nth($breakpoint-names, $n - 1), null); } -// -// Escape SVG strings. -// +/** + * Escapes special characters in SVG strings for use in CSS + * + * @param {String} $string - The SVG string to escape + * @return {String} - The escaped SVG string + * + * @example + * escape-svg("data:image/svg+xml,...") // Returns escaped SVG + */ @function escape-svg($string) { @if str-index($string, "data:image/svg+xml") { @each $char, $encoded in $escaped-characters { @@ -70,6 +109,10 @@ * * @param {Number} $value - The value to be converted to a percentage. * @return {String} - The percentage representation of the value. + * + * @example + * to-percentage(0.5) // Returns: 50% + * to-percentage(0.25) // Returns: 25% */ @function to-percentage($value) { @return if(math.is-unitless($value), math.percentage($value), $value); @@ -80,7 +123,12 @@ * * @param {Color} $color - The base color to be made transparent. * @param {Number} $alpha - The level of transparency, ranging from 0 (fully transparent) to 1 (fully opaque). Default is 1. + * @param {Color} $background - The background color to mix with (default: transparent) * @return {Color} - The resulting color with the specified transparency. + * + * @example + * color-transparent(#007bff, 0.5) // Returns semi-transparent blue + * color-transparent(#dc3545, 0.8) // Returns mostly opaque red */ @function color-transparent($color, $alpha: 1, $background: transparent) { @if $alpha == 1 { @@ -90,6 +138,15 @@ } } +/** + * Converts an SVG string to a data URL format for use in CSS + * + * @param {String} $svg - The SVG markup to convert + * @return {String} - The data URL formatted SVG + * + * @example + * url-svg('...') // Returns: url('data:image/svg+xml;charset=UTF-8,...') + */ @function url-svg($svg) { $svg: str-replace($svg, '#', '%23'); $svg: str-replace($svg, '