|
@use 'sass:meta'; |
|
@use 'sass:math'; |
|
@use 'sass:map'; |
|
@use 'sass:list'; |
|
|
|
// Named breakpoints |
|
$breakpoints: ( |
|
'xs': 0, |
|
's': 540, |
|
'sm': 768, |
|
'md': 1024, |
|
'lg': 1200, |
|
'xl': 1400, |
|
'2xl': 1600, |
|
'3xl': 1900, |
|
'4xl': 2500, |
|
) !default; |
|
|
|
// Bounds for the grow() function |
|
$min-screen-width: 320 !default; |
|
$max-screen-width: '4xl' !default; |
|
|
|
// Bounds for the c-grow() function |
|
$min-container-width: 320 !default; |
|
$max-container-width: 2500 !default; |
|
|
|
// --- End of user config --- |
|
|
|
$breakpoint-names: map.keys($breakpoints); |
|
|
|
/// Get the width number of a breakpoint |
|
/// |
|
/// @param {String} $name |
|
/// |
|
@function get-breakpoint-width($name) { |
|
@if map.has-key($breakpoints, $name) { |
|
@return map.get($breakpoints, $name); |
|
} @else { |
|
@error "No breakpoint named `#{$name}` found."; |
|
} |
|
} |
|
|
|
/// Get the name of the next larger breakpoint |
|
/// |
|
/// @param {String} $name |
|
/// @return {String|null} |
|
/// |
|
@function get-next-breakpoint-name($name) { |
|
@if not map.has-key($breakpoints, $name) { |
|
@error "No breakpoint named `#{$name}` found."; |
|
} |
|
|
|
$index: list.index($breakpoint-names, $name); |
|
@if $index < list.length($breakpoint-names) { |
|
@return list.nth($breakpoint-names, $index + 1); |
|
} @else { |
|
@return null; |
|
} |
|
} |
|
|
|
/// Get the length of a CSS unit |
|
/// |
|
/// @param {String} $unit |
|
/// @return {Number} |
|
/// |
|
@function get-unit-length($unit) { |
|
// prettier-ignore |
|
$units: ('cm': 1cm, 'mm': 1mm, 'q': 1q, 'in': 1in, 'pc': 1pc, 'pt': 1pt, 'px': 1px, 'em': 1em, 'ex': 1ex, 'cap': 1cap, 'ch': 1ch, 'ic': 1ic, 'lh': 1lh, 'rem': 1rem, 'rex': 1rex, 'rcap': 1rcap, 'rch': 1rch, 'ric': 1ric, 'rlh': 1rlh, 'vw': 1vw, 'vh': 1vh, 'vi': 1vi, 'vb': 1vb, 'vmin': 1vmin, 'vmax': 1vmax, 'svw': 1svw, 'svh': 1svh, 'svi': 1svi, 'svb': 1svb, 'svmin': 1svmin, 'svmax': 1svmax, 'lvw': 1lvw, 'lvh': 1lvh, 'lvi': 1lvi, 'lvb': 1lvb, 'lvmin': 1lvmin, 'lvmax': 1lvmax, 'dvw': 1dvw, 'dvh': 1dvh, 'dvi': 1dvi, 'dvb': 1dvb, 'dvmin': 1dvmin, 'dvmax': 1dvmax, 'cqw': 1cqw, 'cqh': 1cqh, 'cqi': 1cqi, 'cqb': 1cqb, 'cqmin': 1cqmin, 'cqmax': 1cqmax, '%': 1%, 'fr': 1fr, 'deg': 1deg, 'grad': 1grad, 'rad': 1rad, 'turn': 1turn, 's': 1s, 'ms': 1ms, 'hz': 1hz, 'khz': 1khz, 'dpi': 1dpi, 'dpcm': 1dpcm, 'dppx': 1dppx, 'x': 1x); |
|
|
|
@if not map.has-key($units, $unit) { |
|
@error 'Unsupported unit `#{$unit}`.'; |
|
} |
|
|
|
@return map.get($units, $unit); |
|
} |
|
|
|
/// Strip unit from a number |
|
/// @param {Number} $value |
|
/// @return {Number} |
|
/// |
|
@function strip-unit($value) { |
|
@return math.div($value, $value * 0 + 1); |
|
} |
|
|
|
/// Enforce a unit on a number |
|
/// |
|
/// Keeps input if it already has the correct unit, attached it on a unitless number |
|
/// and errors if the input has a different unit. |
|
/// |
|
/// @param {Number} $value |
|
/// @param {String} $unit The unit to enforce, e.g. 'px', 'rem', 'em', 'vw', 'vh' or 'number' for unitless |
|
/// @return {Number} |
|
/// |
|
@function force-unit($value, $unit) { |
|
@if $unit == 'number' { |
|
@return strip-unit($value); |
|
} |
|
|
|
@if math.is-unitless($value) { |
|
@return $value * get-unit-length($unit); |
|
} @else { |
|
@if math.unit($value) != $unit { |
|
@error "Value `#{$value}` does not have the expected unit `#{$unit}`."; |
|
} |
|
|
|
@return $value; |
|
} |
|
} |
|
|
|
/// Set a unit on a number if it does not have one |
|
/// |
|
/// @param {Number} $value |
|
/// @param {String} $unit The unit to enforce, e.g. 'px', 'rem', 'em', 'vw', 'vh' or 'number' for unitless |
|
/// @return {Number} |
|
/// |
|
@function with-unit($value, $unit) { |
|
@if math.is-unitless($value) { |
|
@return force-unit($value, $unit); |
|
} @else { |
|
@return $value; |
|
} |
|
} |
|
|
|
/// Resolve a given size (CSS length, number or breakpoint name) to a Sass with unit |
|
/// |
|
/// @param {String|Number} $size |
|
/// @return {Number} |
|
/// |
|
@function resolve-size($size) { |
|
@if meta.type-of($size) == 'string' { |
|
@if map.has-key($breakpoints, $size) { |
|
$size: map.get($breakpoints, $size); |
|
} @else { |
|
@error "No breakpoint named `#{$size}` found."; |
|
} |
|
} |
|
|
|
@return with-unit($size, 'px'); |
|
} |
|
|
|
/// Wrap contents in a media query to match the provided breakpoint (name or length) |
|
/// |
|
/// @param {String|Number} $lower |
|
/// @param {String|Number} $upper |
|
/// @param {Boolean} $invert |
|
/// @param {Boolean} $inclusive |
|
/// |
|
@mixin -match($lower, $upper: null, $invert: false, $inclusive: false) { |
|
$lower-with-unit: resolve-size($lower); |
|
|
|
@if $upper != null and $inclusive == true { |
|
@if meta.type-of($upper) != 'string' { |
|
@error "Inclusive upper bound only supports breakpoint names as string, got `#{$upper}`."; |
|
} |
|
|
|
$upper: get-next-breakpoint-name($upper); |
|
} |
|
|
|
$has-upper: $upper != null; |
|
|
|
$lower-comment: ''; |
|
@if meta.type-of($lower) == 'string' { |
|
$lower-comment: ' /* ' + $lower + ' */'; |
|
} |
|
|
|
@if strip-unit($lower-with-unit) == 0 and not $has-upper { |
|
@if not $invert { |
|
@content; |
|
} @else { |
|
@warn "no-match() with lower bound of 0 can not match anything."; |
|
} |
|
} @else if $has-upper { |
|
$invert-operator: ''; |
|
@if $invert { |
|
$invert-operator: 'not '; |
|
} |
|
|
|
$upper-with-unit: ''; |
|
@if $has-upper { |
|
$upper-with-unit: ' #{resolve-size($upper)}'; |
|
} |
|
|
|
$upper-operator: ''; |
|
@if $has-upper { |
|
$upper-operator: ' <'; |
|
} |
|
|
|
$upper-comment: ''; |
|
@if $has-upper and meta.type-of($upper) == 'string' { |
|
$upper-comment: ' /* ' + $upper + ' */'; |
|
} |
|
|
|
$lower-operator: ' <= '; |
|
@if strip-unit($lower-with-unit) == 0 { |
|
$lower-operator: ''; |
|
$lower-with-unit: ''; |
|
$lower-comment: ''; |
|
} |
|
|
|
@#{'media'} #{$invert-operator}(#{$lower-with-unit}#{$lower-comment}#{$lower-operator}width#{$upper-operator}#{$upper-with-unit}#{$upper-comment}) { |
|
@content; |
|
} |
|
} @else if $invert { |
|
@#{'media'} (width < #{$lower-with-unit}#{$lower-comment}) { |
|
@content; |
|
} |
|
} @else { |
|
@#{'media'} (width >= #{$lower-with-unit}#{$lower-comment}) { |
|
@content; |
|
} |
|
} |
|
} |
|
|
|
/// Wrap contents in a media query to match the provided breakpoint (name or length) |
|
/// |
|
/// @param {String|Number} $lower |
|
/// @param {String|Number} $upper |
|
/// @param {'none'|'lower'|'upper'|'both'} $inclusive |
|
/// |
|
@mixin match($lower, $upper: null, $inclusive: 'lower') { |
|
@include -match($lower, $upper, $inclusive: $inclusive) { |
|
@content; |
|
} |
|
} |
|
|
|
/// Wrap contents in a media query to match the provided breakpoint (name or length) |
|
/// |
|
/// @param {String} $breakpoint |
|
/// |
|
@mixin match-exact($breakpoint) { |
|
@if meta.type-of($breakpoint) != 'string' { |
|
@error "match-exact() only supports breakpoint names as string, got `#{$breakpoint}`."; |
|
} |
|
|
|
@include -match($breakpoint, $breakpoint, $inclusive: true) { |
|
@content; |
|
} |
|
} |
|
|
|
/// Wrap contents in a media query to match the provided breakpoint (name or length) |
|
/// |
|
/// @param {String|Number} $lower |
|
/// @param {String|Number} $upper |
|
/// @param {'none'|'lower'|'upper'|'both'} $inclusive |
|
/// |
|
@mixin no-match($lower, $upper: null, $inclusive: 'lower') { |
|
@include -match($lower, $upper, $inclusive: $inclusive, $invert: true) { |
|
@content; |
|
} |
|
} |
|
|
|
/// Assign value to a property |
|
/// If value is a map, keys are used as breakpoint names/lengths with according values |
|
/// |
|
/// @param {String} $property |
|
/// @param {*} $value |
|
/// @param {Boolean} $interpolate |
|
/// |
|
@mixin responsive($property, $value, $interpolate: false) { |
|
@if meta.type-of($value) != 'map' { |
|
#{$property}: with-unit($value, 'px'); |
|
} @else if not $interpolate { |
|
@each $breakpoint-key, $breakpoint-value in $value { |
|
@include match($breakpoint-key) { |
|
#{$property}: with-unit($breakpoint-value, 'px'); |
|
} |
|
} |
|
} @else { |
|
$map: $value; |
|
$interpolated-breakpoint-names: map.keys($map); |
|
$num-breakpoints: list.length($interpolated-breakpoint-names); |
|
|
|
@if ($num-breakpoints < 2) { |
|
@error "responsive() $map must receive at least two breakpoints"; |
|
} |
|
|
|
$index: 1; |
|
@each $breakpoint, $size in $map { |
|
@if $index < $num-breakpoints { |
|
$next-breakpoint: list.nth($interpolated-breakpoint-names, $index + 1); |
|
$next-size: map.get($map, $next-breakpoint); |
|
|
|
@include match($breakpoint) { |
|
#{$property}: -grow( |
|
$min-value: $size, |
|
$max-value: $next-size, |
|
$min-context-width: force-unit(resolve-size($breakpoint), 'px'), |
|
$max-context-width: force-unit(resolve-size($next-breakpoint), 'px'), |
|
$context-unit: 'vw' |
|
); |
|
} |
|
} |
|
|
|
$index: $index + 1; |
|
} |
|
} |
|
} |
|
|
|
/// Assign value to a property |
|
/// If value is a map, keys are used as container query lengths with according values |
|
/// |
|
/// @param {String} $property |
|
/// @param {*} $value |
|
/// @param {String} $container |
|
/// @param {Boolean} $interpolate |
|
/// |
|
@mixin c-responsive($property, $value, $container: '', $interpolate: false) { |
|
@if meta.type-of($value) != 'map' { |
|
#{$property}: with-unit($value, 'px'); |
|
} @else if not $interpolate { |
|
@each $breakpoint-key, $breakpoint-value in $value { |
|
@if meta.type-of($breakpoint-key) != 'number' { |
|
@error "c-responsive() only supports numeric keys in the map, got `#{$breakpoint-key}`."; |
|
} |
|
|
|
$container-size: resolve-size($breakpoint-key); |
|
|
|
@if strip-unit($container-size) == 0 { |
|
#{$property}: with-unit($breakpoint-value, 'px'); |
|
} @else { |
|
@container #{$container} (width >= #{$container-size}) { |
|
#{$property}: with-unit($breakpoint-value, 'px'); |
|
} |
|
} |
|
} |
|
} @else { |
|
@if $container != '' { |
|
@error "Using c-responsive() with $interpolate: true does not support custom $container names."; |
|
} |
|
|
|
$map: $value; |
|
$interpolated-breakpoint-names: map.keys($map); |
|
$num-breakpoints: list.length($interpolated-breakpoint-names); |
|
|
|
@if ($num-breakpoints < 2) { |
|
@error "c-responsive() $map must receive at least two breakpoints"; |
|
} |
|
|
|
$index: 1; |
|
@each $breakpoint, $size in $map { |
|
@if meta.type-of($breakpoint) != 'number' { |
|
@error "c-responsive() only supports numeric keys in the map, got `#{$breakpoint}`."; |
|
} |
|
|
|
$breakpoint-length: with-unit($breakpoint, 'px'); |
|
|
|
@if $index < $num-breakpoints { |
|
$next-breakpoint: list.nth($interpolated-breakpoint-names, $index + 1); |
|
$next-size: map.get($map, $next-breakpoint); |
|
$value: -grow( |
|
$min-value: $size, |
|
$max-value: $next-size, |
|
$min-context-width: $breakpoint, |
|
$max-context-width: $next-breakpoint, |
|
$context-unit: 'cqw', |
|
); |
|
|
|
@if strip-unit($breakpoint-length) == 0 { |
|
#{$property}: #{$value}; |
|
} @else { |
|
@container #{$container} (width >= #{$breakpoint-length}) { |
|
#{$property}: #{$value}; |
|
} |
|
} |
|
} |
|
|
|
$index: $index + 1; |
|
} |
|
} |
|
} |
|
|
|
/// Grow a length value between two bounds, scaling with the screen size |
|
/// |
|
@function grow($min-value, $max-value, $from: $min-screen-width, $to: $max-screen-width, $rem: 16) { |
|
$from-stripped: strip-unit(force-unit(resolve-size($from), 'px')); |
|
$to-stripped: strip-unit(force-unit(resolve-size($to), 'px')); |
|
|
|
$debug-comment-addition: ''; |
|
@if $from-stripped != strip-unit(force-unit(resolve-size($min-screen-width), 'px')) { |
|
$debug-comment-addition: '#{$debug-comment-addition}, $from: #{$from}'; |
|
} |
|
@if $to-stripped != strip-unit(force-unit(resolve-size($max-screen-width), 'px')) { |
|
$debug-comment-addition: '#{$debug-comment-addition}, $to: #{$to}'; |
|
} |
|
$debug-comment: #{'/* grow(#{$min-value}, #{$max-value}#{$debug-comment-addition}) */'}; |
|
|
|
@return #{$debug-comment} -grow( |
|
$min-value, |
|
$max-value, |
|
$from-stripped, |
|
$to-stripped, |
|
$context-unit: 'vw', |
|
$rem: $rem |
|
); |
|
} |
|
|
|
/// Grow a length value between two bounds, scaling with the container size |
|
/// |
|
@function c-grow( |
|
$min-value, |
|
$max-value, |
|
$from: $min-container-width, |
|
$to: $max-container-width, |
|
$rem: 16 |
|
) { |
|
$debug-comment-addition: ''; |
|
@if $from != $min-container-width { |
|
$debug-comment-addition: '#{$debug-comment-addition}, $from: #{$from}'; |
|
} |
|
@if $to != $max-container-width { |
|
$debug-comment-addition: '#{$debug-comment-addition}, $to: #{$to}'; |
|
} |
|
$debug-comment: #{'/* c-grow(#{$min-value}, #{$max-value}#{$debug-comment-addition})) */'}; |
|
|
|
@if meta.type-of($from) != 'number' or meta.type-of($to) != 'number' { |
|
@error "c-grow() $from and $to parameters must be numbers."; |
|
} |
|
|
|
@return #{$debug-comment} -grow( |
|
$min-value, |
|
$max-value, |
|
$from, |
|
$to, |
|
$context-unit: 'cqw', |
|
$rem: $rem |
|
); |
|
} |
|
|
|
/// Grow a length value between two bounds, scaling with the provided context unit |
|
/// |
|
@function -grow( |
|
$min-value, |
|
$max-value, |
|
$min-context-width, |
|
$max-context-width, |
|
$context-unit, |
|
$rem: 16 |
|
) { |
|
$min-value-stripped: strip-unit(force-unit($min-value, 'px')); |
|
$max-value-stripped: strip-unit(force-unit($max-value, 'px')); |
|
|
|
$min-context-width-stripped: strip-unit(force-unit($min-context-width, 'px')); |
|
$max-context-width-stripped: strip-unit(force-unit($max-context-width, 'px')); |
|
|
|
$min-value-rem: math.div($min-value-stripped, $rem); |
|
$max-value-rem: math.div($max-value-stripped, $rem); |
|
|
|
@if $min-value-stripped == $max-value-stripped { |
|
@return $min-value-rem * 1rem; |
|
} |
|
|
|
$min-width: math.div($min-context-width-stripped, $rem); |
|
$max-width: math.div($max-context-width-stripped, $rem); |
|
|
|
$slope: math.div($max-value-rem - $min-value-rem, $max-width - $min-width); |
|
$y-axis-intersection: ((-$min-width * $slope) + $min-value-rem) * 1rem; |
|
|
|
$y-axis-part: ''; |
|
@if strip-unit($y-axis-intersection) != 0 { |
|
$y-axis-part: '#{$y-axis-intersection} + '; |
|
} |
|
$clamp-min: #{math.min($min-value-rem, $max-value-rem)}rem; |
|
$clamp-max: #{math.max($min-value-rem, $max-value-rem)}rem; |
|
|
|
@return clamp(#{$clamp-min}, #{$y-axis-part}#{$slope * 100}#{$context-unit}, #{$clamp-max}); |
|
} |