From d07f664450ddaaebb44127a4bd057763d13d3f82 Mon Sep 17 00:00:00 2001 From: Feuerfuchs Date: Sun, 1 Nov 2020 20:55:14 +0100 Subject: Init --- src/_responsive.scss | 406 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 406 insertions(+) create mode 100644 src/_responsive.scss (limited to 'src/_responsive.scss') diff --git a/src/_responsive.scss b/src/_responsive.scss new file mode 100644 index 0000000..6f2a416 --- /dev/null +++ b/src/_responsive.scss @@ -0,0 +1,406 @@ +//// +/// responsive properties. +/// +/// The mixins and functions in this file allow you to scale any px- or rem-based value depending on +/// the available viewport width. One popular use case is the dynamic scaling of fonts. +/// +/// The code in this file is based on an article by Niklas Postulart: +/// http://niklaspostulart.de/2015/10/sass-responsive-type-mixin +/// +/// The following adjustments were made: +/// - Support any property passed by the user, not just font-size +/// - Allow multiple target viewports / values +/// - Provide a variant of the mixin which integrates include-media for media queries +/// +/// @group Responsive +/// +/// @access public +//// + +/// +/// If true, named viewports will be supported if a compatible $breakpoints map exists. +/// This is the case for [include-media](https://include-media.com/), for example. +/// +/// @type bool +/// +$iro-responsive-support-named-viewports: true !default; + +/// +/// Context ID used for responsive environment-related mixins. +/// +/// @type string +/// +$iro-responsive-context-id: 'responsive' !default; + +/// +/// Scale a property uniformly between a specific set of target viewports / values. +/// +/// @param {string | list} $props - Property or list of properties to set +/// @param {number} $responsive-map - A map with keys = viewports and values = target value +/// @param {bool} $fluid [true] - If enabled, property values will smoothly transition from one viewport to the next +/// @param {bool} $vertical [false] - If enabled, property viewport height will be used instead of viewport width +/// +/// @example scss - Responsive font-size between 2 viewports +/// .something { +/// @include iro-responsive-property(font-size, ( 320px: 20px, 720px: 30px )); +/// } +/// +/// // Generates: +/// +/// @media (min-width: 320px) and (max-width: 720px) { +/// .something { +/// font-size: calc(20px + 10 * ((100vw - 20px) / 400)); +/// } +/// } +/// +/// @media (max-width: 320px) { +/// .something { +/// font-size: 20px; +/// } +/// } +/// +/// @media (min-width: 720px) { +/// .something { +/// font-size: 30px; +/// } +/// } +/// +/// @example scss - Responsive font-size between 3 viewports +/// .something { +/// @include iro-responsive-property(font-size, ( 320px: 20px, 720px: 30px, 1280px: 40px )); +/// } +/// +/// // Generates: +/// +/// @media (min-width: 320px) and (max-width: 720px) { +/// .something { +/// font-size: calc(20px + 10 * ((100vw - 20px) / 400)); +/// } +/// } +/// +/// @media (min-width: 720px) and (max-width: 1280px) { +/// .something { +/// font-size: calc(30px + 10 * ((100vw - 30px) / 400)); +/// } +/// } +/// +/// @media (max-width: 320px) { +/// .something { +/// font-size: 20px; +/// } +/// } +/// +/// @media (min-width: 720px) { +/// .something { +/// font-size: 30px; +/// } +/// } +/// +@mixin iro-responsive-property($props, $responsive-map, $fluid: true, $vertical: false) { + @include iro-responsive-env(map-keys($responsive-map), $fluid, $vertical) { + @if type-of($props) == list { + @each $prop in $props { + #{$prop}: iro-responsive-set(map-values($responsive-map)); + } + } @else { + #{$props}: iro-responsive-set(map-values($responsive-map)); + } + } +} + +/// +/// Create a new responsive environment by specifying a set of viewports. +/// Inside a responsive environment, use the iro-responsive-set function to make a property scale automatically. +/// +/// @param {list} $viewports - Viewports sorted in ascending order +/// @param {bool} $fluid [true] - If enabled, property values will smoothly transition from one viewport to the next +/// @param {bool} $vertical [false] - If enabled, property viewport height will be used instead of viewport width +/// +/// @content +/// +/// @see {function} iro-responsive-set +/// +/// @example scss - Responsive font-size between 2 viewports +/// .something { +/// @include iro-responsive-env((320px, 720px)) { +/// font-size: iro-responsive-set(20px, 30px); +/// } +/// } +/// +/// // Generates: +/// +/// @media (min-width: 320px) and (max-width: 720px) { +/// .something { +/// font-size: calc(20px + 10 * ((100vw - 20px) / 400)); +/// } +/// } +/// +/// @media (max-width: 320px) { +/// .something { +/// font-size: 20px; +/// } +/// } +/// +/// @media (min-width: 720px) { +/// .something { +/// font-size: 30px; +/// } +/// } +/// +@mixin iro-responsive-env($viewports, $fluid: true, $vertical: false) { + @if length($viewports) <= 1 { + @error '$viewports must contain at least two viewports.'; + } + + $new-viewports: (); + + @each $viewport in $viewports { + @if $iro-responsive-support-named-viewports and global-variable-exists(breakpoints) { + @if map-has-key($breakpoints, $viewport) { + $viewport: map-get($breakpoints, $viewport); + } + } + + @if (type-of($viewport) != number) or unitless($viewport) { + @error '$viewports contains invalid viewports.'; + } + + $new-viewports: append($new-viewports, $viewport); + } + + $viewports: iro-quicksort($new-viewports); + + @if $new-viewports != $viewports { + @error '$viewports was not sorted in ascending order.'; + } + + @if $fluid { + $first-vp: nth($viewports, 1); + $last-vp: nth($viewports, length($viewports)); + + @include iro-context-push($iro-responsive-context-id, 'env', ( + 'viewports': $viewports, + 'mode': set, + 'index': 1, + 'fluid': $fluid, + 'vertical': $vertical, + )); + + @content; + + @include iro-context-pop($iro-responsive-context-id); + + @for $i from 1 to length($viewports) { + $prev-vp: nth($viewports, $i); + $next-vp: nth($viewports, $i + 1); + + @if not $vertical { + @media (min-width: $prev-vp) and (max-width: $next-vp) { + @include iro-context-push($iro-responsive-context-id, 'env', ( + 'viewports': $viewports, + 'mode': transition, + 'index': $i, + 'fluid': $fluid, + 'vertical': $vertical, + )); + + @content; + + @include iro-context-pop($iro-responsive-context-id); + } + } @else { + @media (min-height: $prev-vp) and (max-height: $next-vp) { + @include iro-context-push($iro-responsive-context-id, 'env', ( + 'viewports': $viewports, + 'mode': transition, + 'index': $i, + 'fluid': $fluid, + 'vertical': $vertical, + )); + + @content; + + @include iro-context-pop($iro-responsive-context-id); + } + } + } + + @if not $vertical { + @media (min-width: $last-vp) { + @include iro-context-push($iro-responsive-context-id, 'env', ( + 'viewports': $viewports, + 'mode': set, + 'index': length($viewports), + 'fluid': $fluid, + 'vertical': $vertical, + )); + + @content; + + @include iro-context-pop($iro-responsive-context-id); + } + } @else { + @media (min-height: $last-vp) { + @include iro-context-push($iro-responsive-context-id, 'env', ( + 'viewports': $viewports, + 'mode': set, + 'index': length($viewports), + 'fluid': $fluid, + 'vertical': $vertical, + )); + + @content; + + @include iro-context-pop($iro-responsive-context-id); + } + } + } @else { + @include iro-context-push($iro-responsive-context-id, 'env', ( + 'viewports': $viewports, + 'mode': set, + 'index': 1, + 'fluid': $fluid, + 'vertical': $vertical, + )); + + @content; + + @include iro-context-pop($iro-responsive-context-id); + + @for $i from 2 through length($viewports) { + $vp: nth($viewports, $i); + + @if not $vertical { + @media (min-width: $vp) { + @include iro-context-push($iro-responsive-context-id, 'env', ( + 'viewports': $viewports, + 'mode': set, + 'index': $i + )); + + @content; + + @include iro-context-pop($iro-responsive-context-id); + } + } @else { + @media (min-height: $vp) { + @include iro-context-push($iro-responsive-context-id, 'env', ( + 'viewports': $viewports, + 'mode': set, + 'index': $i + )); + + @content; + + @include iro-context-pop($iro-responsive-context-id); + } + } + } + } +} + +/// +/// Make a property scale automatically inside a responsive environment. +/// +/// @param {list} $values - Value for each viewport in the responsive environment. The first value will be used for the first viewport, the second value for the second viewport, and so on. +/// +/// @return {number|string} +/// +@function iro-responsive-set($values, $without-calc: false) { + $noop: iro-context-assert-stack-must-contain($iro-responsive-context-id, 'env'); + + $data: nth(iro-context-get($iro-responsive-context-id, 'env'), 2); + $viewports: map-get($data, 'viewports'); + $mode: map-get($data, 'mode'); + $fluid: map-get($data, 'fluid'); + $vertical: map-get($data, 'vertical'); + + @if length($values) != length($viewports) { + @error '$values must contain the same number of items as the responsive environment\'s $viewports.'; + } + + @if $mode == set { + @return nth($values, map-get($data, 'index')); + } @else { + $index: map-get($data, 'index'); + $prev-vp: nth($viewports, $index); + $next-vp: nth($viewports, $index + 1); + $prev-value: nth($values, $index); + $next-value: nth($values, $index + 1); + + @return iro-responsive-fluid-calc($prev-value, $next-value, $prev-vp, $next-vp, $vertical, $without-calc); + } +} + +/// +/// Generate the calc() function that uniformly scales a value from $min-value to $max-value depending +/// on the viewport width. +/// +/// @param {number} $min-value - Minimum value +/// @param {number} $max-value - Maximum value +/// @param {number} $min-viewport - Minimum viewport size +/// @param {number} $max-viewport - Maximum viewport size +/// @param {bool} $vertical [false] - If enabled, property viewport height will be used instead of viewport width +/// +/// @access private +/// +@function iro-responsive-fluid-calc($min-value, $max-value, $min-viewport, $max-viewport, $vertical: false, $without-calc: false) { + $value-unit: unit($min-value); + $max-value-unit: unit($max-value); + $viewport-unit: unit($min-viewport); + $max-viewport-unit: unit($max-viewport); + + @if $min-value == 0 { + $value-unit: $max-value-unit; + } + @if $max-value == 0 { + $max-value-unit: $value-unit; + } + @if $min-viewport == 0 { + $viewport-unit: $max-viewport-unit; + } + @if $max-viewport == 0 { + $max-viewport-unit: $viewport-unit; + } + + @if ($value-unit != $max-value-unit) or ($viewport-unit != $max-viewport-unit) { + @error 'Units of $min-value and $max-value, $min-viewport and $max-viewport must match.'; + } + + @if ($value-unit == rem) and ($viewport-unit == px) { + $min-viewport: iro-px-to-rem($min-viewport); + $max-viewport: iro-px-to-rem($max-viewport); + $viewport-unit: rem; + } @else if ($value-unit == px) and ($viewport-unit == rem) { + $min-value: iro-px-to-rem($min-value); + $max-value: iro-px-to-rem($max-value); + $value-unit: rem; + } + + @if $value-unit != $viewport-unit { + @error 'This combination of units is not supported.'; + } + + $value-diff: iro-strip-unit($max-value - $min-value); + $viewport-diff: iro-strip-unit($max-viewport - $min-viewport); + + $calc: ''; + + @if $min-value != 0 { + $calc: '#{$min-value} + '; + } + + @if not $vertical { + $calc: unquote('#{$calc}#{$value-diff} * ((100vw - #{$min-viewport}) / #{$viewport-diff})'); + } @else { + $calc: unquote('#{$calc}#{$value-diff} * ((100vh - #{$min-viewport}) / #{$viewport-diff})'); + } + + @if $without-calc { + @return $calc; + } @else { + @return calc(#{$calc}); + } +} + +@include iro-context-stack-create($iro-responsive-context-id); -- cgit v1.2.3-54-g00ecf