Launch Week Day 3: Introducing Organization Notifications in Socket.Learn More
Socket
Book a DemoSign in
Socket

chaestli

Package Overview
Dependencies
Maintainers
1
Versions
17
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

chaestli - npm Package Compare versions

Comparing version
2.0.1
to
3.0.0
+1
-1
_index.scss

@@ -1,1 +0,1 @@

@import './lib/chaestli';
@forward './lib/chaestli';

@@ -1,70 +0,48 @@

@import '../vendor/@dreipol/scss-utils/functions/str-split';
@import '../vendor/@dreipol/scss-utils/functions/list-expand-directions';
@use 'sass:map';
@use 'sass:list';
/// Return the current BEM root by separating the rightmost part and stripping it from any element or modifier
/// @return {string} - The current BEM root
/// Create a definitions object for containers
/// @param {Map} $cfg - The config map
/// @return {Map} - The merge result
///
@function grid--bem-root() {
$root-selector: nth(nth(&, 1), 1);
$root-selector: nth(dp-str-split($root-selector, '__'), 1);
$root-selector: nth(dp-str-split($root-selector, '--'), 1);
$root-selector: '.#{nth(dp-str-split($root-selector, '.'), 1)}';
@function create-container-config($cfg) {
$defaults: (
width: none,
edge: 0,
);
@if (str-index($root-selector, '.') != 1) {
@error 'Provided root selector `#{$root-selector}` is no valid class selector!';
}
@return $root-selector;
@return map.merge($defaults, $cfg);
}
/// Convert a BEM root selector and a list of modules into a selector list.
/// @param {string} $selector - A root module name (BEM) including a dot.
/// @param {string} $item-modules - One or multiple BEM submodule names without dashes.
/// @return {string} - A stringified list of bem modules
/// @example scss - Basic usage: input
/// $module-list: _grid--col-name-list('.form', 'label', 'input');
/// #{$module-list} { display: block; }
/// @output css - Basic usage: output
/// .form--label, .form--input { display: block; }
/// Create a definitions object for grids
/// @param {Map} $cfg - The config map
/// @return {Map} - The merge result
///
@function grid--col-name-list($selector, $item-modules...) {
$item-modules-list: '';
@function create-grid-config($cfg) {
$defaults: (
gutter: 0,
display: grid,
repeat-directive: 'auto-fill',
num-cols: 1,
);
@each $item-module in $item-modules {
$i: index($item-modules, $item-module);
$sep: if($i == 1, '', ', ');
$item-modules-list: ($item-modules-list + $sep + '#{$selector}#{$bem-element-separator}#{$item-module}');
}
@return $item-modules-list;
@return map.merge($defaults, $cfg);
}
/// Expand a given attribute into a 4-value gutter configuration
/// @param {number|List} $type - The property type that is being written. Example: 'padding'
/// Extract the grid gutters from the user options
/// @param {list|number} $gutter - Gutter value provided by the user
/// @return {Map} - A map containing the column and row gutters
///
@function grid--configurate-gutter($gutter) {
@function get-grid-gaps($gutter) {
@if (type-of($gutter) == 'list') {
@return dp-list-expand-directions($gutter);
} @else {
@return (null, $gutter, null, $gutter);
@return (
column: list.nth($gutter, 1),
row: list.nth($gutter, 2),
);
}
}
/// Write the desired grid-gutter values.
/// @param {string} $type - The property type that is being written. Example: 'padding'
/// @param {List} $gutter-list - A list of gutter sizes with a length of 4.
/// @param {number} $modifier - A modifier to the gutter size that is being written.
///
@mixin grid--write-gutter($type, $gutter-list, $modifier: 1) {
$direction-list: ('top', 'left', 'bottom', 'right');
@for $i from 1 through length($direction-list) {
$dir: nth($direction-list, $i);
$val: nth($gutter-list, $i);
@if ($val) {
#{$type}-#{$dir}: ($val * $modifier);
}
}
@return (
column: $gutter,
row: 0,
);
}

@@ -1,318 +0,106 @@

/* stylelint-disable scss/declaration-nested-properties */
////
/// Grid library for all your basic grid needs!
////
/// Dependencies and components
///
@import '../vendor/@dreipol/scss-mq/index';
@import '../vendor/@dreipol/scss-utils/mixins/at-root';
@import './chaestli__vars';
@import './chaestli__helpers';
@import './chaestli__basics';
@use 'sass:map';
@use './chaestli__helpers' as *;
/// Fix buggy support for percentage values of `flex-basis` in IE10 and IE11.
/// @link https://github.com/philipwalton/flexbugs#7-flex-basis-doesnt-account-for-box-sizingborder-box
/// Create a grid container defining the width of the main content and its edges
/// @param {Map} $definition - A single container structure definition
/// @example scss - Simple container: input
/// .grid {
/// .grid__container {
/// @include chaestli.container((edge: 16px, width: 800px));
/// }
/// }
///
@mixin grid--fix-ie-flex-basis() {
@include dp-at-root(html, '.ie11') {
flex-basis: auto;
@content;
}
@include dp-at-root(html, '.ie10') {
flex-basis: auto;
@content;
}
}
/// Add basic grid styling to elements with a certain naming convention adapted from bootstrap
/// @param {string} $cfg.root - A root module name (BEM) including a dot
/// @param {string} $cfg.mq - A media expression that is parseable by `include-media`
/// @param {string} $cfg.container - BEM element describing a container
/// @param {string} $cfg.row - BEM element describing a row
/// @param {List} $cfg.cols - One or multiple BEM element describing a col
/// @param {string|number} $cfg.container-size - The grid-container's size
/// @output
/// .grid .grid__container {
/// max-width: 800px;
/// margin-left: auto;
/// margin-left: auto;
/// padding-left: 16px;
/// margin-left: 16px;
/// }
///
@mixin grid--root($cfg: ()) {
// Merge default values with config
$defaults: (
root: grid--bem-root(),
mq: null,
container: 'container',
row: 'row',
cols: ('col'),
);
$cfg: map-merge($defaults, $cfg);
@mixin container($definition: ()) {
$definition: create-container-config($definition);
// Write config values into variables for code readability
$cfg-root: map-get($cfg, 'root');
$cfg-mq: map-get($cfg, 'mq');
$cfg-container: map-get($cfg, 'container');
$cfg-row: map-get($cfg, 'row');
$cfg-cols: map-get($cfg, 'cols');
// read config variables
$width: map.get($definition, 'width');
$edge: map.get($definition, 'edge');
// Calculate additionally needed values
$col-names: grid--col-name-list($cfg-root, $cfg-cols...);
// prepare variables to render
$has-width: type-of($width) == 'number';
$content-width: if($has-width, $width, none);
@include mq($cfg-mq) {
#{$cfg-root}#{$bem-element-separator}#{$cfg-container} {
@include grid--define-container;
}
#{$cfg-root}#{$bem-element-separator}#{$cfg-row} {
@include grid--define-row;
}
#{$col-names} {
@include grid--define-col;
}
}
// render css props
max-width: $content-width;
margin: {
left: auto;
right: auto;
};
padding: {
left: $edge;
right: $edge;
};
}
/// Move a column in any direction using its css margin property
/// @param {string} $cfg.direction - Either one of these values: `left` `right` `top` `bottom`
/// @param {unitless} $cfg.ratio - A unitless ratio value (0 < x <= 1)
/// @param {string} $cfg.mq - A media expression that is parseable by `include-media`
/// Enable the `display: grid;` on a container element. Notice that you can define the amount of columns your grid will have
/// and the vertical and horizontal gaps of its items
/// @param {Map} $definition - A single grid structure definition
///
@mixin grid--move-col($cfg) {
$defaults: (
direction: null,
ratio: null,
mq: null,
);
$cfg: map-merge($defaults, $cfg);
$cfg-direction: map-get($cfg, 'direction');
$cfg-ratio: map-get($cfg, 'ratio');
$cfg-mq: map-get($cfg, 'mq');
$percentage: (100% * $cfg-ratio);
@include mq($cfg-mq) {
margin-#{$cfg-direction}: $percentage;
}
}
/// Move a column from left to right
@mixin grid--push($cfg: ()) {
@include grid--move-col(map-merge($cfg, (direction: 'left')));
}
/// Move a column from right to left
@mixin grid--pull($cfg: ()) {
@include grid--move-col(map-merge($cfg, (direction: 'right')));
}
/// Define the maximum size of a grid by adding a `max-width` to the container
/// @param {string} $cfg.root - A root module name (BEM) including a dot
/// @param {string} $cfg.mq - A media expression that is parseable by `include-media`
/// @param {string} $cfg.container - BEM element describing a container
/// @param {string|number} $cfg.container-size - The grid-container's size
/// @example scss - Simple 12 columns grid with rows and cols gutter: input
/// .grid {
/// .grid__row {
/// @include chaestli.grid((gutter: (16px, 24px), num-cols: 12));
/// }
/// }
///
@mixin grid--constrain($cfg: ()) {
// Merge default values with config
$defaults: (
root: grid--bem-root(),
mq: null,
container: 'container',
container-size: none,
size-prop: 'max-width',
);
$cfg: map-merge($defaults, $cfg);
// Write config values into variables for code readability
$cfg-root: map-get($cfg, 'root');
$cfg-mq: map-get($cfg, 'mq');
$cfg-container: map-get($cfg, 'container');
$cfg-container-size: map-get($cfg, 'container-size');
$cfg-size-prop: map-get($cfg, 'size-prop');
@include mq($cfg-mq) {
#{$cfg-root}#{$bem-element-separator}#{$cfg-container} {
#{$cfg-size-prop}: $cfg-container-size;
}
}
}
/// Scaffold spacing in between cols and between cols and container
/// @param {string} $cfg.root - A root module name (BEM) including a dot
/// @param {string} $cfg.mq - A media expression that is parseable by `include-media`
/// @param {string} $cfg.container - BEM element describing a container
/// @param {string} $cfg.row - BEM element describing a row
/// @param {List} $cfg.cols - One or multiple BEM element describing a col
/// @param {number} $cfg.gutter - The desired inner gutter size in any spacial unit
/// @param {number} $cfg.edge - The desired edge clearance in any spacial unit
/// @output
/// .grid .grid__row {
/// display: grid;
/// grid-template-columns: repeat(auto-fill, minmax(calc(100% / 12 - 16px), 1fr));
/// column-gap: 16px;
/// row-gap: 24px;
/// }
///
@mixin grid--space($cfg: ()) {
// Merge default values with config
$defaults: (
root: grid--bem-root(),
container: 'container',
row: 'row',
cols: ('col'),
gutter: 0,
mq: null,
debug: null,
edge: null,
);
$cfg: map-merge($defaults, $cfg);
@mixin grid($definition: ()) {
$definition: create-grid-config($definition);
// Write config values into variables for code readability
$cfg-root: map-get($cfg, 'root');
$cfg-mq: map-get($cfg, 'mq');
$cfg-container: map-get($cfg, 'container');
$cfg-row: map-get($cfg, 'row');
$cfg-cols: map-get($cfg, 'cols');
$cfg-gutter: map-get($cfg, 'gutter');
$cfg-edge: map-get($cfg, 'edge');
$cfg-debug: map-get($cfg, 'debug');
// read config variables
$gutter: map.get($definition, 'gutter');
$display: map.get($definition, 'display');
$repeat-directive: map.get($definition, 'repeat-directive');
$num-cols: map.get($definition, 'num-cols');
// Calculate additionally needed values
$col-names: grid--col-name-list($cfg-root, $cfg-cols...);
$gutters: grid--configurate-gutter($cfg-gutter);
$edges: grid--configurate-gutter(if($cfg-edge, $cfg-edge, $cfg-gutter));
// prepare variables to render
$gaps: get-grid-gaps($gutter);
$column-gap: map.get($gaps, 'column');
$row-gap: map.get($gaps, 'row');
$column-size: if($column-gap > 0, calc(100% / #{$num-cols} - #{$column-gap}), calc(100% / #{$num-cols}));
@include mq($cfg-mq) {
#{$cfg-root}#{$bem-element-separator}#{$cfg-container} {
@include grid--write-gutter('padding', $edges);
#{$cfg-root}#{$bem-element-separator}#{$cfg-container}:not(.u-iknowwhatimdoing) {
// NOTE: Nested containers are not allowed, please nest rows/cols instead!
visibility: hidden !important; // stylelint-disable-line declaration-no-important
&::before {
visibility: visible;
content: 'Nested `grid--container` detected!';
font-size: 20px;
line-height: 1.2;
color: red; // stylelint-disable-line color-named
}
}
}
#{$cfg-root}#{$bem-element-separator}#{$cfg-row} {
@include grid--write-gutter('margin', $gutters, -0.5);
// enable the grid debugging
@if $cfg-debug {
@include grid--debugger((
edge: $cfg-edge,
gutter: $cfg-gutter
), $cfg-debug);
}
}
#{$col-names} {
@include grid--write-gutter('padding', $gutters, 0.5);
}
}
// render css props
display: $display;
grid-template-columns: repeat(#{$repeat-directive}, minmax($column-size, 1fr));
column-gap: $column-gap;
row-gap: $row-gap;
}
/// Define the col's size in flex notation
/// @param {unitless} $cfg.ratio - A unitless ratio value (0 < x <= 1)
/// @param {string} $cfg.mq - A media expression that is parseable by `include-media`
/// @param {string} $cfg.fallback - A property name that serves as fallback in IE10 and IE11
/// Just as syntax sugar aliasing the `grid-column` rule
/// @param {list|number} $start - column start
/// @param {list|number} $end - column end
/// @example scss - Simple column 3 span over 12 columns: input
/// .grid {
/// .grid__three-col {
/// @include chaestli.column(3 span, 12 span);
/// }
/// }
///
@mixin grid--span($cfg: ()) {
// Merge default values with config
$defaults: (
ratio: null,
mq: null,
fallback-prop: 'width',
);
$cfg: map-merge($defaults, $cfg);
// Write config values into variables for code readability
$cfg-ratio: map-get($cfg, 'ratio');
$cfg-mq: map-get($cfg, 'mq');
$cfg-fallback-prop: map-get($cfg, 'fallback-prop');
// Calculate additionally needed values
$percentage: (100% * $cfg-ratio);
@include mq($cfg-mq) {
flex: 0 0 $percentage;
@include grid--fix-ie-flex-basis {
#{$cfg-fallback-prop}: $percentage;
}
/// @output
/// .grid .grid__three-col {
/// grid-column: 3 span / 12 span;
/// }
@mixin column($start, $end: null) {
@if ($end) {
grid-column: #{$start} / #{$end};
} @else {
grid-column: $start;
}
}
/// Standalone version of `grid--span` with the same config
///
@mixin grid--col($cfg: ()) {
@include grid--define-col;
@include grid--span($cfg);
}
/// Allows the grid debugging coloring the rows background showing the columns and the edges
/// @param {number} $grid-cfg.gutter - The inner gutter size
/// @param {number} $grid-cfg.edge - The edge clearance in any spacial unit
/// @param {number} $cfg.columns - The amount of columns we want to debug
/// @param {number} $cfg.column-color - The color used to debug the columns
/// @param {number} $cfg.gutter-color - The color used to debug the gutter
/// @param {number} $cfg.edge-color - The color to highlight the external edges
///
@mixin grid--debugger(
$grid-cfg,
$cfg
) {
$defaults: (
column-color: rgba(0, 0, 0, 0.2),
edge-color: rgba(0, 0, 0, 0),
gutter-color: rgba(0, 0, 0, 0),
columns: null
);
@if not map-get($cfg, 'columns') {
@error 'Please define the columns amount to debug the grid';
}
$cfg: map-merge($defaults, $cfg);
$cfg-gutter: map-get($grid-cfg, 'gutter');
$cfg-edge: map-get($grid-cfg, 'edge');
$cfg-columns: map-get($cfg, 'columns');
$cfg-gutter-color: map-get($cfg, 'gutter-color');
$cfg-edge-color: map-get($cfg, 'edge-color');
$cfg-column-color: map-get($cfg, 'column-color');
$half-gutter: $cfg-gutter / 2;
$gradient-start: $half-gutter;
$gradient-end: calc(100% - #{$half-gutter});
position: relative;
background: {
image: linear-gradient(
to right,
$cfg-gutter-color $gradient-start,
$cfg-column-color $gradient-start,
$cfg-column-color $gradient-end,
$cfg-gutter-color $gradient-end
);
size: 1 / $cfg-columns * 100%;
position: left top;
clip: content-box;
origin: content-box;
};
&::before,
&::after {
content: '';
position: absolute;
top: 0;
height: 100%;
width: $cfg-edge;
background: $cfg-edge-color;
}
&::before {
right: calc(100% - #{$half-gutter});
}
&::after {
left: calc(100% - #{$half-gutter});
}
}
{
"name": "chaestli",
"version": "2.0.1",
"version": "3.0.0",
"description": "A scss grid framework made in Switzerland and brought to you by dreipol",

@@ -10,11 +10,8 @@ "main": "_index.scss",

"scripts": {
"test": "npm run build && npm run lint && npm run test-ruby && npm run test-node",
"test-ruby": "sass test.scss",
"test-node": "node-sass test.scss",
"test": "npm run lint && npx sass test.scss",
"lint": "stylelint _index.scss lib/**/*.scss",
"clean": "rm -rf vendor",
"demo": "node-sass --output-style=compressed -o=demo demo/scss/index.scss",
"prebuild": "npm run clean && mkdir vendor",
"build": "npm run prebuild && rsync -rm `pwd`/node_modules/* --include=\"*.scss\" --include=\"*/\" --exclude=\"*\" vendor",
"prepublish": "npm test"
"doc": "mv docs/demo demo && npx sassdoc lib --dest docs && mv demo docs/demo",
"demo": "npx sass --style=compressed docs/demo/scss/index.scss:docs/demo/index.css",
"watch-demo": "npx sass --watch --style=compressed docs/demo/scss/index.scss:docs/demo/index.css",
"prepublishOnly": "npm run doc && npm test"
},

@@ -26,3 +23,2 @@ "repository": {

"files": [
"vendor",
"lib",

@@ -48,17 +44,15 @@ "_index.scss"

"homepage": "https://github.com/dreipol/chaestli#readme",
"dependencies": {
"@dreipol/scss-mq": "^1.0.0",
"@dreipol/scss-utils": "^3.0.1"
},
"devDependencies": {
"@dreipol/stylelint-config": "^5.0.0",
"@dreipol/stylelint-config-bem-pattern": "^2.0.1",
"@dreipol/stylelint-config-order": "^3.0.0",
"@dreipol/stylelint-config-scss": "^2.0.1",
"node-sass": "4.13.1",
"stylelint": "^13.0.0",
"stylelint-order": "^4.0.0",
"stylelint-scss": "^3.14.2",
"@dreipol/scss-mq": "^2.0.0",
"@dreipol/stylelint-config": "^7.0.0",
"@dreipol/stylelint-config-bem-pattern": "^3.0.0",
"@dreipol/stylelint-config-order": "^4.0.0",
"@dreipol/stylelint-config-scss": "^3.0.0",
"node-sass": "4.14.1",
"sassdoc": "^2.7.3",
"stylelint": "^13.7.2",
"stylelint-order": "^4.1.0",
"stylelint-scss": "^3.18.0",
"stylelint-selector-bem-pattern": "^2.1.0"
}
}

@@ -0,1 +1,3 @@

<img src="https://raw.githubusercontent.com/dreipol/chaestli/main/logo.png" width="50%"/>
# chaestli

@@ -9,13 +11,51 @@ Dreipol scss grid system

# Installation
# Documentation
```bash
npm i chaestli -S
- [API](https://www.dreipol.dev/chaestli/)
- [Demo](https://www.dreipol.dev/chaestli/demo)
## Available mixins
Chaestli exports only 3 mixins (`chaestli.container`, `chaestli.grid`, `chaestli.column`) that combined together should be enough to scaffold your grid setup. For example:
```scss
@use 'node_modules/@dreipol/chaestli';
.grid {
.grid__container {
// a container having a max-with of 1024px with 16px lateral padding
@include chaestli.container(( width: 1024px, edge: 16px ));
}
.grid__row {
// a grid made of 12 columns with 8px lateral gutter
@include chaestli.grid(( gutter: 8px, num-cols: 12 ));
}
.grid__small-col {
// two columns over 12
@include chaestli.column(2 span, 12 span);
}
.grid__medium-col {
// for columns over 12
@include chaestli.column(4 span, 12 span);
}
.grid__big-col {
// eight columns over 12
@include chaestli.column(8 span, 12 span);
}
}
```
# Documentation
# Usage
While the documentation is under construction, you can find some barebone examples in the [demo page](https://dreipol.github.io/chaestli/demo)
## Installation
```bash
npm i chaestli -S
```
[travis-image]:https://img.shields.io/travis/dreipol/chaestli.svg?style=flat-square

@@ -22,0 +62,0 @@ [travis-url]:https://travis-ci.org/dreipol/chaestli

@import '../vendor/@dreipol/scss-utils/mixins/at-root';
@mixin grid--define-container {
display: flex;
flex-flow: column nowrap;
width: 100%;
margin: auto;
}
@mixin grid--define-row {
display: flex;
flex-flow: row wrap;
// NOTE: Fixes an image scaling issue of flex children in IE11.
// @link https://github.com/philipwalton/flexbugs#5-column-flex-items-dont-always-preserve-intrinsic-aspect-ratios
@include dp-at-root(html, '.ie11') {
min-height: 1px;
}
}
@mixin grid--define-col {
position: relative;
min-height: 1px;
max-width: 100%;
}
/// BEM presets
///
$bem-element-separator: '__' !default;
$bem-modifier-separator: '--' !default;
@import '../vendor/@dreipol/scss-utils/mixins/hide-visually';
@import '../vendor/@dreipol/scss-utils/mixins/at-root';
/// Add a hidden but readable hook to be used in html and js
/// @param {string} $root-class - Class that serves as root name for the elements containing the mq data
///
@mixin mq--bridge($root-class) {
#{$root-class}-type::before,
#{$root-class}-conditions::before,
#{$root-class}-expressions::before {
@include dp-hide-visually;
}
#{$root-class}-conditions {
@include dp-at-root(html, '.u-mq-info') {
&::before {
pointer-events: none;
z-index: 9999;
position: fixed;
bottom: 0;
left: 0;
display: block;
width: auto;
height: auto;
max-width: 50%;
max-height: none;
padding: 15px;
margin: 0;
clip: auto;
font-family: monospace;
font-size: 12px;
line-height: 1.1;
word-wrap: break-word;
color: #ffffff;
background-color: rgba(22, 25, 40, 0.9);
border-top-right-radius: 5px;
text-align: left;
}
&::before {
transition: visibility 0ms ease 2000ms;
visibility: hidden;
}
}
}
}
// stylelint-disable scss/no-duplicate-dollar-variables
@import '../vendor/@dreipol/scss-utils/functions/list-purge';
@import '../vendor/sass-to-js/sass/sass-to-js';
@import '../vendor/include-media-or/dist/include-media-or';
/// Check a list of conditions for it's validity based on the used operator as only `<=` and `>` make sense
/// @param {List} $conditions - List of conditions, possibly nested
///
@mixin mq--check-condition-validity($conditions) {
@each $condition in $conditions {
@if (type-of($condition) == 'list') {
@include mq--check-condition-validity($condition);
} @else if not map-has-key($media-expressions, $condition) {
$operator: get-expression-operator($condition);
@if not index(('<=', '≤', '>'), $operator) {
@error 'Operator `#{$operator}` is not allowed in condition `#{$condition}`';
}
}
}
}
/// Convert a map of media expressions into a list of all possible viewport combinations
/// @param {Map} $expressions - Map of media feature expressions, grouped by type
/// @return {Map} - Conjugated map of all possible media feature expressions
///
@function mq--conditions($expressions) {
$result: ((),);
@each $list-key, $list-value in $expressions {
$combinations: ();
$list-value: map-merge((null: null), $list-value);
@each $item-key, $item-value in $list-value {
@each $combination in $result {
$combinations: append($combinations, map-merge($combination, ($list-key: $item-key)));
}
}
$result: $combinations;
}
@return $result;
}
/// Convert a list of media expressions into a list of sass maps with name and query properties
/// @param {Map} $expressions - A flat list of media feature expressions
/// @return {List} - A list of maps containing all available separate media queries used in the project
///
@function mq--expressions($expressions) {
$result: ();
@each $type, $conditions in $expressions {
@each $name, $value in $conditions {
$result: append($result, (name: $name, query: $value, type: $type));
}
}
@return $result;
}
/// Writes the viewport information into a content attribute, contained by the media expression it describes
/// @param {string} $root-class - Class that serves as root name for the elements containing the mq data
///
@mixin mq--write($root-class) {
// Type
@each $type-key, $type-value in $media-type-expressions {
#{$root-class}-type::before {
@include mq($type-value) {
content: sassToJs($type-value);
}
}
}
// Conditions
@each $conditions in mq--conditions($media-feature-expressions) {
$media-conditions: dp-list-purge(map-values($conditions));
#{$root-class}-conditions::before {
@include mq($media-conditions...) {
content: sassToJs($conditions);
}
}
}
// Expressions
$expressions-list: mq--expressions(map-merge((type: $media-type-expressions), $media-feature-expressions));
#{$root-class}-expressions::before {
content: sassToJs($expressions-list);
}
}
@import '../vendor/@dreipol/scss-utils/functions/map-assign';
/// Computed breakpoints system, borrowed from bootstrap
///
$screen-xs-min: 0 !default;
$screen-sm-min: 544px !default;
$screen-md-min: 768px !default;
$screen-lg-min: 992px !default;
$screen-xl-min: 1280px !default;
$screen-xxl-min: 1600px !default;
$screen-xs-max: ($screen-sm-min - 1) !default;
$screen-sm-max: ($screen-md-min - 1) !default;
$screen-md-max: ($screen-lg-min - 1) !default;
$screen-lg-max: ($screen-xl-min - 1) !default;
$screen-xl-max: ($screen-xxl-min - 1) !default;
/// Break point definitions used by `include-media`
///
$breakpoints: (
xs: $screen-xs-max,
sm: $screen-sm-max,
md: $screen-md-max,
lg: $screen-lg-max,
xl: $screen-xl-max,
) !default;
/// Media type expressions
///
$media-type-expressions: (
screen: 'screen',
print: 'print',
) !default;
/// Media feature expressions
///
$media-feature-expressions: (
width: (
xs: '(max-width: #{$screen-xs-max})',
sm: ('(min-width: #{$screen-sm-min}) and (max-width: #{$screen-sm-max})'),
md: ('(min-width: #{$screen-md-min}) and (max-width: #{$screen-md-max})'),
lg: ('(min-width: #{$screen-lg-min}) and (max-width: #{$screen-lg-max})'),
xl: ('(min-width: #{$screen-xl-min}) and (max-width: #{$screen-xl-max})'),
xxl: '(min-width: #{$screen-xxl-min})',
),
orientation: (
portrait: '(orientation: portrait)',
landscape: '(orientation: landscape)',
),
resolution: (
res2x: ('(min-resolution: 2dppx)'),
res3x: ('(min-resolution: 3dppx)'),
),
) !default;
/// Media expressions, used by `include-media`
/// @example scss - Combine width expression `xs` with resolution expression `res2x`:
/// @include media('xs', 'res2x') { color: red; }
///
$media-expressions: dp-map-assign(
$media-type-expressions,
map-get($media-feature-expressions, width),
map-get($media-feature-expressions, orientation),
map-get($media-feature-expressions, resolution)
) !default;
////
/// Helper library to make `include-media` more useful in many ways
////
/// Dependencies and components
///
@import '../vendor/@dreipol/scss-utils/functions/list-purge';
@import './mq__vars';
@import './mq__bridge';
@import './mq__helpers';
/// Guard mixin to enable usage of `include-media` with `null` values and protect against wrong usage.
/// @param {...string} $conditions - Media query conditions
/// @example scss - Above breakpoint `md`:
/// @include media('>md') { color: red; }
/// @example scss - Below or equal breakpoint `lg`:
/// @include media('<=lg') { color: red; }
///
@mixin mq($conditions...) {
$purged-conditions: dp-list-purge($conditions);
@if (length($purged-conditions) == 0) {
@content;
} @else {
@include mq--check-condition-validity($purged-conditions);
@include media($purged-conditions...) {
@content;
}
}
}
/// Append mq bridge to dom object
/// @param {string} $root-class - Class that serves as root name for the elements containing the mq data
///
@mixin mq--spy($root-class) {
@include mq--bridge($root-class);
@include mq--write($root-class);
}
@import './functions/color-shade';
@import './functions/color-tint';
@import './functions/is-truthy';
@import './functions/list-concat';
@import './functions/list-contains';
@import './functions/list-expand-directions';
@import './functions/list-first';
@import './functions/list-insert-nth';
@import './functions/list-last';
@import './functions/list-last-index';
@import './functions/list-prepend';
@import './functions/list-purge';
@import './functions/list-remove';
@import './functions/list-remove-nth';
@import './functions/list-replace';
@import './functions/list-replace-nth';
@import './functions/list-slice';
@import './functions/map-assign';
@import './functions/map-deep-get';
@import './functions/selector-get-element-name';
@import './functions/str-replace';
@import './functions/str-split';
@import './functions/strip-unit';
@import './functions/to-string';
@import './functions/transition-props';
@import './functions';
@import './mixins';
@import './mixins/at-root';
@import './mixins/hide-text';
@import './mixins/has-focus';
@import './mixins/hide-visually';
@import './mixins/ios-native-scrolling';
@import './mixins/is-selectable';
@import './mixins/is-visible';
@import './mixins/last-row';
@import './mixins/overlay';
@import './mixins/placeholder';
@import './mixins/size';
@import './mixins/text-ellipsis';
@import './mixins/supports-hover';
@import './mixins/supports-touch';
/// Slightly darken a color by mixing it with black
/// @link http://sass-guidelin.es/#lightening-and-darkening-colors
/// @access public
/// @param {color} $color - Color to tint
/// @param {number} $percentage - Percentage of `$color` in returned color
/// @return {Color}
///
@function dp-color-shade($color, $percentage) {
@return mix(black, $color, $percentage);
}
/// Slightly lighten a color by mixing it with white
/// @link http://sass-guidelin.es/#lightening-and-darkening-colors
/// @access public
/// @param {color} $color - Color to tint
/// @param {number} $percentage - Percentage of `$color` in returned color
/// @return {Color}
///
@function dp-color-tint($color, $percentage) {
@return mix(white, $color, $percentage);
}
/// Check if a value has a truthy value
/// @param {*} $value - The input value
/// @return {boolean} - A flag indicating if the value is truthy
///
@function dp-is-truthy($value) {
@return if($value == null, false, $value and $value != null and $value != '' and $value != ());
}
@import './to-string';
/// Concatenate a list using a given separator
/// @link https://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/#miscellaneous
/// @param {List} $list - The list to be concatenated
/// @param {string} $glue - A separator string to be inserted between items
/// @return {string} - A string containing list items connected with a separator string
///
@function dp-list-concat($list, $glue: '') {
$result: null;
@if length($list) == 0 {
@return '';
}
@for $i from 1 through length($list) {
$e: nth($list, $i);
@if type-of($e) == list {
$result: unquote('#{$result}#{to-string($e, $glue, true)}');
} @else {
$result: if($i != length($list), unquote('#{$result}#{$e}#{$glue}'), unquote('#{$result}#{$e}'));
}
}
@return $result;
}
/// Checks if a given list contains a certain value
/// @param {List} $list - The haystack
/// @param {*} $val - The needle
///
@function dp-list-contains($list, $val) {
@if index($list, $val) {
@return true;
}
@return false;
}
// stylelint-disable scss/no-duplicate-dollar-variables
/// Expand a given list to 4 items, similar to css padding shorthand expressions.
/// Inspired by expansions used in css shortand expressions (eg. margin or padding).
/// @param {List} $list - List containing zero to four items
/// @return {List} - Updated list expanded to always four entries
///
@function dp-list-expand-directions($list) {
$result: ();
$len-list: length($list);
@if ($len-list == 0) {
$result: (null, null, null, null);
} @else if ($len-list == 1) {
$result: (nth($list, 1), nth($list, 1), nth($list, 1), nth($list, 1));
} @else if ($len-list == 2) {
$result: (nth($list, 1), nth($list, 2), nth($list, 1), nth($list, 2));
} @else if ($len-list == 3) {
$result: (nth($list, 1), nth($list, 2), nth($list, 3), nth($list, 2));
} @else {
$result: (nth($list, 1), nth($list, 2), nth($list, 3), nth($list, 4));
}
@return $result;
}
/// Return the first item of a list
/// @link https://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/#selecting-values-from-list
/// @param {List} $list - The input list
/// @return {*} - The list's first item
///
@function dp-list-first($list) {
@return nth($list, 1);
}
// stylelint-disable scss/no-duplicate-dollar-variables
/// Add a value to an arbitrary position
/// @link https://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/#adding-values-to-a-list
/// @param {List} $list - The input list
/// @param {number} $index - The value's target list position
/// @param {*} $value - The term to be added
/// @return {*} - The output list
///
@function dp-list-insert-nth($list, $index, $value) {
$result: null;
@if type-of($index) != number {
@warn '$index: #{quote($index)} is not a number for `insert-nth`.';
} @else if $index < 1 {
@warn 'List index 0 must be a non-zero integer for `insert-nth`';
} @else if $index > length($list) {
@warn 'List index is #{$index} but list is only #{length($list)} item long for `insert-nth`';
} @else {
$result: ();
@for $i from 1 through length($list) {
@if $i == $index {
$result: append($result, $value);
}
$result: append($result, nth($list, $i));
}
}
@return $result;
}
/// Return the last occurence of an item within a list
/// @link https://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/#selecting-values-from-list
/// @param {List} $list - The input list
/// @param {*} $value - The search term
/// @return {number} - The index of the search term's last occurrence within the list
///
@function dp-list-last-index($list, $value) {
$result: null;
@for $i from 1 through length($list) {
@if nth($list, $i) == $value {
$result: $i;
}
}
@return $result;
}
/// Return the last item of a list
/// @link https://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/#selecting-values-from-list
/// @param {List} $list - The input list
/// @return {*} - The list's last item
///
@function dp-list-last($list) {
@return nth($list, length($list));
}
/// Add a value to the front of a list
/// @link https://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/#adding-values-to-a-list
/// @param {List} $list - The input list
/// @param {*} $value - The term to be prepended
/// @return {*} - The output list
///
@function dp-list-prepend($list, $value) {
@return join($value, $list);
}
@import './is-truthy';
/// Removes all non-true values from a list
/// @link http://hugogiraudel.com/2013/10/09/advanced-sass-list-functions-again/
/// @param {List} $list - A list containing possibly falsy values
/// @return {List} - The trimmed list
///
@function dp-list-purge($list) {
$result: ();
@each $item in $list {
@if dp-is-truthy($item) {
$result: append($result, $item);
}
}
@return $result;
}
@import './list-replace-nth';
/// Remove a value at a certain index from a list
/// @link https://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/#removing-values-from-list
/// @param {List} $list - The input list
/// @param {number} $index - The target position to be removed
/// @return {*} - The list's first item
///
@function dp-list-remove-nth($list, $index) {
@return dp-list-replace-nth($list, $index);
}
@import './list-replace';
/// Remove a value from a list
/// @link https://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/#removing-values-from-list
/// @param {List} $list - The input list
/// @param {*} $value - The term to be removed
/// @return {*} - The list's first item
///
@function dp-list-remove($list, $value) {
@return dp-list-replace($list, $value);
}
// stylelint-disable scss/no-duplicate-dollar-variables
@import './is-truthy';
/// Replace a value within a list by index
/// @link https://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/#replacing-values-from-list
/// @param {List} $list - The input list
/// @param {number} $index - The value's target list position
/// @param {*} $value - The term to be inserted
/// @return {*} - The output list
///
@function dp-list-replace-nth($list, $index, $value: null) {
$result: null;
@if type-of($index) != number {
@warn '$index: #{quote($index)} is not a number for `replace-nth`';
} @else if $index == 0 {
@warn 'ist index 0 must be a non-zero integer for `replace-nth`.';
} @else if abs($index) > length($list) {
@warn 'ist index is #{$index} but list is only #{length($list)} item long for `replace-nth`.';
} @else {
$result: ();
$index: if($index < 0, length($list) + $index + 1, $index);
@for $i from 1 through length($list) {
@if $i == $index {
@if dp-is-truthy($value) {
$result: append($result, $value);
}
} @else {
$result: append($result, nth($list, $i));
}
}
}
@return $result;
}
@import './is-truthy';
/// Replace a value with another one in a list
/// @link https://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/#replacing-values-from-list
/// @param {List} $list - The input list
/// @param {*} $old-value - The term that is being replaced
/// @param {*} $value - The term to replace it with
/// @return {*} - The output list
///
@function dp-list-replace($list, $old-value, $value: null) {
$result: ();
@for $i from 1 through length($list) {
@if nth($list, $i) == $old-value {
@if dp-is-truthy($value) {
$result: append($result, $value);
}
} @else {
$result: append($result, nth($list, $i));
}
}
@return $result;
}
// stylelint-disable scss/no-duplicate-dollar-variables
/// Return a number of values from a list
/// @link https://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/#miscellaneous
/// @param {List} $list - The input list
/// @param {number} $start - The slice range's start index
/// @param {number} $end - The slice range's end index
/// @return {List} - A list of items
///
@function dp-list-slice($list, $start: 1, $end: length($list)) {
$result: null;
@if type-of($start) != number or type-of($end) != number {
@warn 'Either $start or $end are not a number for `slice`';
} @else if $start > $end {
@warn 'The start index has to be lesser than or equals to the end index for `slice`';
} @else if $start < 1 or $end < 1 {
@warn 'List indexes must be non-zero integers for `slice`';
} @else if $start > length($list) {
@warn 'List index is #{$start} but list is only #{length($list)} item long for `slice`';
} @else if $end > length($list) {
@warn 'List index is #{$end} but list is only #{length($list)} item long for `slice`';
} @else {
$result: ();
@for $i from $start through $end {
$result: append($result, nth($list, $i));
}
}
@return $result;
}
/// Takes multiple maps and merges them into one
/// @link http://stackoverflow.com/questions/27740063/merge-multiple-sass-maps
/// @param {...Map} $maps - Any number of maps
/// @return {Map} - The resulting map
///
@function dp-map-assign($maps...) {
$result: ();
@each $map in $maps {
$result: map-merge($result, $map);
}
@return $result;
}
/// Returns a value from a nested list (without failsafe if one of the keys does not exist)
/// @link https://css-tricks.com/snippets/sass/deep-getset-maps/
/// @param {Map} $map - Map
/// @param {...string} $keys - Key chain
/// @return {*} - Desired value
///
@function dp-map-deep-get($map, $keys...) {
@each $key in $keys {
$map: map-get($map, $key);
}
@return $map;
}
@import './str-split';
/// Remove classes, pseudo-classes and attributes from a selector part
/// @param {string} $selector - The selector part to be cleaned
/// @return {string}
///
@function dp-selector-get-element-name($selector) {
$result: $selector;
$markers: (' ', '.', ':', '[');
@each $marker in $markers {
$result: nth(dp-str-split($result, $marker), 1);
}
@return $result;
}
/// Replace `$search` with `$replace` in `$string`
/// @link https://css-tricks.com/snippets/sass/str-replace-function/
/// @param {string} $string - Initial string
/// @param {string} $search - Substring to replace
/// @param {string} $replace ('') - New value
/// @return {string} - Updated string
///
@function dp-str-replace($string, $search, $replace: '') {
$index: str-index($string, $search);
@if $index {
@return str-slice($string, 1, $index - 1) + $replace + dp-str-replace(str-slice($string, $index + str-length($search)), $search, $replace);
}
@return $string;
}
/// Split `$string` into several parts using `$delimiter`.
/// @param {string} $string - String to split
/// @param {string} $delimiter ('') - String to use as a delimiter to split `$string`
/// @return {List}
///
@function dp-str-split($string, $delimiter: '') {
$result: ();
$length: str-length($string);
@if str-length($delimiter) == 0 {
@for $i from 1 through $length {
$result: append($result, str-slice($string, $i, $i));
}
@return $result;
}
$running: true;
$remaining: $string;
@while $running {
$index: str-index($remaining, $delimiter);
@if $index {
$slice: str-slice($remaining, 1, $index - 1);
@if (str-length($slice) > 0) {
$result: append($result, $slice);
}
$remaining: str-slice($remaining, $index + str-length($delimiter));
} @else {
$running: false;
}
}
@return append($result, $remaining);
}
/// Remove the unit of a length
/// @param {number} $number - Number to remove unit from
/// @return {number} - Unitless number
///
@function dp-strip-unit($number) {
@if type-of($number) == 'number' and not unitless($number) {
@return $number / ($number * 0 + 1);
}
@return $number;
}
/// Cast an arbitrary value to a string
/// @link https://hugogiraudel.com/2014/01/27/casting-types-in-sass/#string
/// @param {*} $value - The original value
/// @return {string} - The cast string
///
@function dp-to-string($value) {
@return inspect($value);
}
/// Batch transition all properties with a single transition definition
/// @param {string} $trs-defintion - The transition definition to be applied
/// @param {...Map} $props - Any number of properties to be transitioned
/// @return {List} - A list to be used as a value for a transition property
///
@function dp-transition-props($trs-definition, $props...) {
$result: ();
@each $prop in $props {
$result: append($result, ($prop $trs-definition), 'comma');
}
@return $result;
}
@import '../functions/selector-get-element-name';
/// Use `@at-root` while adding classes (or attributes) `$classes` to an element node `$append-target`.
/// If the element was not found a standard `@at-root` is used.
/// @param {string} $append-target - $append-target string to append the classes (or attributes) to
/// @param {string} $appended-selector - A single element selector containing classes and/or attributes to be appended
///
@mixin dp-at-root($append-target, $appended-selector: '') {
$is-element-found: false;
$combined-selectors: '';
$selectors: nth(&, 1);
@if ($appended-selector) {
@each $selector in $selectors {
$pure-element-selector: dp-selector-get-element-name($selector);
@if (($pure-element-selector == $append-target) and (not $is-element-found)) {
$is-element-found: true;
$combined-selectors: '#{$combined-selectors} #{$selector}#{$appended-selector}';
} @else {
$combined-selectors: '#{$combined-selectors} #{$selector}';
}
}
}
@if ($is-element-found) {
@at-root #{$combined-selectors} {
@content;
}
} @else {
@at-root #{$append-target}#{$appended-selector} & {
@content;
}
}
}
@import './supports-hover';
@import './supports-touch';
/// Styles touch/hover states according to browser capabilities
/// @param {boolean} $toggle - State toggle; Available values: `true`, `false` or `default` (for both)
///
@mixin dp-has-focus() {
@include dp-supports-hover {
&:hover, &:focus {
@content;
}
}
@include dp-supports-touch {
&:active {
@content;
}
}
}
/// Visually hide text but leave it accessible for screen readers and search engines
///
@mixin dp-hide-text {
font: 0/0 a; // stylelint-disable font-family-no-missing-generic-family-keyword
text-shadow: none;
color: transparent;
}
/// Visually hide content but leave it accessible for screen readers and search engines
///
@mixin dp-hide-visually {
position: absolute;
overflow: hidden;
max-width: 1px;
max-height: 1px;
padding: 0;
border: 0;
margin: -1px;
clip: rect(0 0 0 0);
}
/// Add ios native overflow scrolling to an element
/// @param {string} $orientation - The scroll direction
///
@mixin dp-ios-native-scrolling($orientation: y) {
-webkit-overflow-scrolling: touch;
@if (not index((x, y), $orientation)) {
@error '`#{$orientation}` must be either `x` or `y`';
}
@if ($orientation == y) {
overflow-x: hidden;
overflow-y: scroll;
}
@if ($orientation == x) {
overflow-x: scroll;
overflow-y: hidden;
}
}
/// Controls the selection style of the current element and all its children
/// @param {boolean} $toggle - State toggle
///
@mixin dp-is-selectable($toggle) {
$c--label-selection: #accef7 !default; // provide a fallback value
@if $toggle {
& {
-webkit-touch-callout: text;
user-select: text;
&::selection, ::selection {
background-color: $c--label-selection;
}
}
} @else {
& {
-webkit-touch-callout: none;
user-select: none;
&::selection, ::selection {
background-color: transparent;
}
}
}
}
// stylelint-disable scss/no-duplicate-dollar-variables
/// Animates an element in or out, including setting their visibility correctly
/// The basic opacity transition can be extended with other properties or full transition descriptors
/// @param {boolean} $toggle - State toggle
/// @param {number} $speed - The targeted speed after which the element is hidden
/// @param {...string|...List} $transition-params - One or more strings or lists containing additional transition params
/// @example scss - Simple usage: input
/// .my-elem.my-elem__is-hidden { @include dp-is-visible(false, 300ms); }
/// .my-elem.my-elem__is-shown { @include dp-is-visible(true, 300ms); }
/// @output css - Simple usage: output
/// .my-elem.my-elem__is-hidden {
/// transition: visibility 0ms linear 300ms, opacity 300ms ease 0ms;
/// visibility: hidden;
/// opacity: 0;
/// }
/// .my-elem.my-elem__is-shown {
/// transition: visibility 0ms linear 0ms, opacity 300ms ease 0ms;
/// visibility: inherit;
/// opacity: 1;
/// }
///
@mixin dp-is-visible($toggle, $speed, $transition-params...) {
$transitions: ();
$has-params: length($transition-params) > 0;
@if type-of($speed) != 'number' {
@error 'Provided transition speed `#{$speed}` is not of type number!';
}
@if $has-params {
@for $i from 1 through length($transition-params) {
$param: nth($transition-params, $i);
@if type-of($param) == 'list' { // If $param contains a space (a.k.a. a whole string)
$transitions: append($transitions, unquote('#{$param}'), comma);
} @else {
$transitions: append($transitions, unquote('#{$param} #{$speed} ease 0ms'), comma);
}
}
} @else {
$transitions: append($transitions, unquote('opacity #{$speed} ease 0ms'), comma);
}
@if $toggle {
transition: join(unquote('visibility 0ms linear 0ms'), $transitions, comma);
visibility: inherit;
@if not $has-params {
opacity: 1;
}
} @else {
transition: join(unquote('visibility 0ms linear #{$speed}'), $transitions, comma);
visibility: hidden;
@if not $has-params {
opacity: 0;
}
}
}
/// Targets the last row in a grid of constant column count
/// @link http://keithclark.co.uk/articles/targeting-first-and-last-rows-in-css-grid-layouts/
/// @param {number} $num-cols - The number of columns within the context
///
@mixin dp-last-row($num-cols) {
$selector: nth(nth(&, 1), length(nth(&, 1)));
&:nth-child(#{$num-cols}n + 1):nth-last-child(-n + #{$num-cols}),
&:nth-child(#{$num-cols}n + 1):nth-last-child(-n + #{$num-cols}) ~ #{$selector} {
@content;
}
}
/// Add styles for a child to overlay its parent
/// @param {string} $pos - A declaration value for the `position` property
/// @param {string} $root-y - A vertical spacial property keyword
/// @param {string} $root-x - A horizontal spacial property keyword
///
@mixin dp-overlay($pos: absolute, $root-y: top, $root-x: left) {
position: $pos;
#{$root-y}: 0;
#{$root-x}: 0;
width: 100%;
height: 100%;
}
// stylelint-disable selector-no-vendor-prefix
/// Add styles to the placeholder element
///
@mixin dp-placeholder() {
&::-moz-placeholder { @content; }
&:-ms-input-placeholder { @content; }
&::-webkit-input-placeholder { @content; }
&::placeholder { @content; }
}
@mixin dp-size($sz) {
width: $sz;
height: $sz;
}
// https://docs.w3cub.com/css/@media/any-hover/
@mixin dp-supports-hover() {
/* one or more available input mechanism(s) can hover over elements with ease */
// The `-ms-high-contrast` directives are needed to target Internet Explorer 10 and 11
// @link https://stackoverflow.com/q/28417056/1602864
// TODO: Remove the `-ms-high-contrast` directives when IE will no longer be supported
@media (any-hover: hover), (-ms-high-contrast: active), (-ms-high-contrast: none) {
@content;
}
}
@mixin dp-supports-touch() {
/* one or more available input mechanism(s) cannot hover or there are no pointing input mechanisms */
@media (any-hover: none) {
@content;
}
}
@mixin dp-text-ellipsis() {
white-space: nowrap;
overflow: hidden;
max-width: 100%;
text-overflow: ellipsis;
}
@import './index';
@import './unprefixed/index';
.test {
$test-list: (foo, bar, baz);
$test-map: (foo: bar);
$test-color: #0000ff;
// Prefixed mixins
@include dp-has-focus;
@include dp-at-root('.test');
@include dp-hide-text;
@include dp-hide-visually;
@include dp-ios-native-scrolling;
@include dp-is-selectable(true);
@include dp-is-visible(true, 200);
@include dp-last-row(2);
@include dp-overlay;
@include dp-placeholder;
@include dp-size(200);
@include dp-text-ellipsis;
// Unprefixed mixins
@include has-focus;
@include at-root('.test');
@include hide-text;
@include hide-visually;
@include ios-native-scrolling;
@include is-selectable(true);
@include is-visible(true, 200);
@include last-row(2);
@include overlay;
@include placeholder;
@include size(200);
@include text-ellipsis;
@include supports-hover;
@include supports-touch;
// Prefixed functions
$test-prefixed: (
't-1': dp-color-shade($test-color, 10),
't-2': dp-color-tint($test-color, 10),
't-3': dp-is-truthy('foo'),
't-4': dp-list-concat($test-list, ','),
't-5': dp-list-contains($test-list, foo),
't-6': dp-list-expand-directions($test-list),
't-7': dp-list-first($test-list),
't-8': dp-list-insert-nth($test-list, 2, qux),
't-9': dp-list-last($test-list),
't-10': dp-list-last-index($test-list, bar),
't-11': dp-list-prepend($test-list, qux),
't-12': dp-list-purge($test-list),
't-13': dp-list-remove($test-list, foo),
't-14': dp-list-remove-nth($test-list, 1),
't-15': dp-list-replace($test-list, foo, qux),
't-16': dp-list-replace-nth($test-list, 1, qux),
't-17': dp-list-slice($test-list, 2, 3),
't-18': dp-map-assign($test-map),
't-19': dp-map-deep-get($test-map, 'color'),
't-20': dp-selector-get-element-name('.test'),
't-21': dp-str-replace('test', 'st', 'oo'),
't-22': dp-str-split('test'),
't-23': dp-strip-unit(3),
't-24': dp-to-string(foo),
't-25': dp-transition-props('all 300ms linear'),
't-26': dp-supports-hover(),
't-27': dp-supports-touch(),
);
// Unprefixed functions
$test-unprefixed: (
't-1': color-shade($test-color, 10),
't-2': color-tint($test-color, 10),
't-3': is-truthy('foo'),
't-4': list-concat($test-list, ','),
't-5': list-contains($test-list, foo),
't-6': list-expand-directions($test-list),
't-7': list-first($test-list),
't-8': list-insert-nth($test-list, 2, qux),
't-9': list-last($test-list),
't-10': list-last-index($test-list, bar),
't-11': list-prepend($test-list, qux),
't-12': list-purge($test-list),
't-13': list-remove($test-list, foo),
't-14': list-remove-nth($test-list, 1),
't-15': list-replace($test-list, foo, qux),
't-16': list-replace-nth($test-list, 1, qux),
't-17': list-slice($test-list, 2, 3),
't-18': map-assign($test-map),
't-19': map-deep-get($test-map, 'color'),
't-20': selector-get-element-name('.test'),
't-21': str-replace('test', 'st', 'oo'),
't-22': str-split('test'),
't-23': strip-unit(3),
't-24': to-string(foo),
't-25': transition-props('all 300ms linear'),
't-26': supports-hover(),
't-27': supports-touch(),
);
$test-fixtures: (
't-1': #0000e6,
't-2': #1a1aff,
't-3': true,
't-4': 'foo,bar,baz',
't-5': true,
't-6': (foo, bar, baz, bar),
't-7': foo,
't-8': foo qux bar baz,
't-9': baz,
't-10': 2,
't-11': (qux, foo, bar, baz),
't-12': foo bar baz,
't-13': bar baz,
't-14': bar baz,
't-15': qux bar baz,
't-16': qux bar baz,
't-17': bar baz,
't-18': (foo: bar),
't-19': null,
't-20': 'test',
't-21': 'teoo',
't-22': 't' 'e' 's' 't',
't-23': 3,
't-24': 'foo',
't-25': (),
);
@for $i from 1 through length($test-fixtures) {
$fixture: map-get($test-fixtures, 't-#{$i}');
$prefixed: map-get($test-prefixed, 't-#{$i}');
$unprefixed: map-get($test-unprefixed, 't-#{$i}');
@if ($prefixed != $unprefixed) {
@error 'Comparison test ##{$i} failing! - Prefixed: #{$prefixed} ~vs~ Unprefixed: #{$unprefixed}';
}
@if ($prefixed != $fixture) {
@error 'Comparison test ##{$i} failing! - Fixture: #{$fixture} ~vs~ Prefixed: #{$prefixed}';
}
}
}
@import '../functions';
@function color-shade($args...) {
@return dp-color-shade($args...);
}
@function color-tint($args...) {
@return dp-color-tint($args...);
}
@function is-truthy($args...) {
@return dp-is-truthy($args...);
}
@function list-concat($args...) {
@return dp-list-concat($args...);
}
@function list-contains($args...) {
@return dp-list-contains($args...);
}
@function list-expand-directions($args...) {
@return dp-list-expand-directions($args...);
}
@function list-first($args...) {
@return dp-list-first($args...);
}
@function list-insert-nth($args...) {
@return dp-list-insert-nth($args...);
}
@function list-last($args...) {
@return dp-list-last($args...);
}
@function list-last-index($args...) {
@return dp-list-last-index($args...);
}
@function list-prepend($args...) {
@return dp-list-prepend($args...);
}
@function list-purge($args...) {
@return dp-list-purge($args...);
}
@function list-remove($args...) {
@return dp-list-remove($args...);
}
@function list-remove-nth($args...) {
@return dp-list-remove-nth($args...);
}
@function list-replace($args...) {
@return dp-list-replace($args...);
}
@function list-replace-nth($args...) {
@return dp-list-replace-nth($args...);
}
@function list-slice($args...) {
@return dp-list-slice($args...);
}
@function map-assign($args...) {
@return dp-map-assign($args...);
}
@function map-deep-get($args...) {
@return dp-map-deep-get($args...);
}
@function selector-get-element-name($args...) {
@return dp-selector-get-element-name($args...);
}
@function str-replace($args...) {
@return dp-str-replace($args...);
}
@function str-split($args...) {
@return dp-str-split($args...);
}
@function strip-unit($args...) {
@return dp-strip-unit($args...);
}
@function to-string($args...) {
@return dp-to-string($args...);
}
@function transition-props($args...) {
@return dp-transition-props($args...);
}
@import './functions';
@import './mixins';
@import '../mixins';
@mixin at-root($args...) {
@include dp-at-root($args...) {
@content;
}
}
@mixin has-focus($args...) {
@include dp-has-focus($args...) {
@content;
}
}
@mixin hide-text($args...) {
@include dp-hide-text($args...);
}
@mixin hide-visually($args...) {
@include dp-hide-visually($args...);
}
@mixin ios-native-scrolling($args...) {
@include dp-ios-native-scrolling($args...);
}
@mixin is-selectable($args...) {
@include dp-is-selectable($args...);
}
@mixin is-visible($args...) {
@include dp-is-visible($args...);
}
@mixin last-row($args...) {
@include dp-last-row($args...) {
@content;
}
}
@mixin overlay($args...) {
@include dp-overlay($args...);
}
@mixin placeholder($args...) {
@include dp-placeholder($args...) {
@content;
}
}
@mixin size($args...) {
@include dp-size($args...);
}
@mixin text-ellipsis($args...) {
@include dp-text-ellipsis($args...);
}
@mixin supports-hover($args...) {
@include dp-supports-hover($args...);
}
@mixin supports-touch($args...) {
@include dp-supports-touch($args...);
}
@charset "UTF-8";
////
/// `include-media-or` - An `include-media` fork, enabling OR conjunctions and nested queries
/// Author: Rouven Bühlmann (@nirazul)
///
/// This project is licensed under the terms of the MIT license
////
////
/// include-media library public configuration
/// @author Eduardo Boucas
/// @access public
////
///
/// Creates a list of global breakpoints
///
/// @example scss - Creates a single breakpoint with the label `phone`
/// $breakpoints: ('phone': 320px);
///
$breakpoints: (
'phone': 320px,
'tablet': 768px,
'desktop': 1024px
) !default;
///
/// Creates a list of static expressions or media types
///
/// @example scss - Creates a single media type (screen)
/// $media-expressions: ('screen': 'screen');
///
/// @example scss - Creates a static expression with logical disjunction (OR operator)
/// $media-expressions: (
/// 'retina2x': '(-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi)'
/// );
///
$media-expressions: (
'screen': 'screen',
'print': 'print',
'handheld': 'handheld',
'landscape': '(orientation: landscape)',
'portrait': '(orientation: portrait)',
'retina2x': ('(-webkit-min-device-pixel-ratio: 2)', '(min-resolution: 192dpi)', '(min-resolution: 2dppx)'),
'retina3x': ('(-webkit-min-device-pixel-ratio: 3)', '(min-resolution: 350dpi)', '(min-resolution: 3dppx)')
) !default;
///
/// Defines a number to be added or subtracted from each unit when declaring breakpoints with exclusive intervals
///
/// @example scss - Interval for pixels is defined as `1` by default
/// @include media('>128px') {}
///
/// /* Generates: */
/// @media (min-width: 129px) {}
///
/// @example scss - Interval for ems is defined as `0.01` by default
/// @include media('>20em') {}
///
/// /* Generates: */
/// @media (min-width: 20.01em) {}
///
/// @example scss - Interval for rems is defined as `0.1` by default, to be used with `font-size: 62.5%;`
/// @include media('>2.0rem') {}
///
/// /* Generates: */
/// @media (min-width: 2.1rem) {}
///
$unit-intervals: (
'px': 1,
'em': 0.01,
'rem': 0.1,
'': 0
) !default;
///
/// Defines whether support for media queries is available, useful for creating separate stylesheets
/// for browsers that don't support media queries.
///
/// @example scss - Disables support for media queries
/// $im-media-support: false;
/// @include media('>=tablet') {
/// .foo {
/// color: tomato;
/// }
/// }
///
/// /* Generates: */
/// .foo {
/// color: tomato;
/// }
///
$im-media-support: true !default;
///
/// Selects which breakpoint to emulate when support for media queries is disabled. Media queries that start at or
/// intercept the breakpoint will be displayed, any others will be ignored.
///
/// @example scss - This media query will show because it intercepts the static breakpoint
/// $im-media-support: false;
/// $im-no-media-breakpoint: 'desktop';
/// @include media('>=tablet') {
/// .foo {
/// color: tomato;
/// }
/// }
///
/// /* Generates: */
/// .foo {
/// color: tomato;
/// }
///
/// @example scss - This media query will NOT show because it does not intercept the desktop breakpoint
/// $im-media-support: false;
/// $im-no-media-breakpoint: 'tablet';
/// @include media('>=desktop') {
/// .foo {
/// color: tomato;
/// }
/// }
///
/// /* No output */
///
$im-no-media-breakpoint: 'desktop' !default;
///
/// Selects which media expressions are allowed in an expression for it to be used when media queries
/// are not supported.
///
/// @example scss - This media query will show because it intercepts the static breakpoint and contains only accepted media expressions
/// $im-media-support: false;
/// $im-no-media-breakpoint: 'desktop';
/// $im-no-media-expressions: ('screen');
/// @include media('>=tablet', 'screen') {
/// .foo {
/// color: tomato;
/// }
/// }
///
/// /* Generates: */
/// .foo {
/// color: tomato;
/// }
///
/// @example scss - This media query will NOT show because it intercepts the static breakpoint but contains a media expression that is not accepted
/// $im-media-support: false;
/// $im-no-media-breakpoint: 'desktop';
/// $im-no-media-expressions: ('screen');
/// @include media('>=tablet', 'retina2x') {
/// .foo {
/// color: tomato;
/// }
/// }
///
/// /* No output */
///
$im-no-media-expressions: ('screen', 'portrait', 'landscape') !default;
////
/// Parsing engine
/// @author Hugo Giraudel
/// @access private
////
///
/// Get operator of an expression
///
/// @param {String} $expression - Expression to extract operator from
///
/// @return {String} - Any of `>=`, `>`, `<=`, `<`, `≥`, `≤`
///
@function get-expression-operator($expression) {
@each $operator in ('>=', '>', '<=', '<', '≥', '≤') {
@if str-index($expression, $operator) {
@return $operator;
}
}
// It is not possible to include a mixin inside a function, so we have to
// rely on the `im-log(..)` function rather than the `log(..)` mixin. Because
// functions cannot be called anywhere in Sass, we need to hack the call in
// a dummy variable, such as `$_`. If anybody ever raise a scoping issue with
// Sass 3.3, change this line in `@if im-log(..) {}` instead.
$_: im-log('No operator found in `#{$expression}`.');
}
///
/// Get dimension of an expression, based on a found operator
///
/// @param {String} $expression - Expression to extract dimension from
/// @param {String} $operator - Operator from `$expression`
///
/// @return {String} - `width` or `height` (or potentially anything else)
///
@function get-expression-dimension($expression, $operator) {
$operator-index: str-index($expression, $operator);
$parsed-dimension: str-slice($expression, 0, $operator-index - 1);
$dimension: 'width';
@if str-length($parsed-dimension) > 0 {
$dimension: $parsed-dimension;
}
@return $dimension;
}
///
/// Get dimension prefix based on an operator
///
/// @param {String} $operator - Operator
///
/// @return {String} - `min` or `max`
///
@function get-expression-prefix($operator) {
@return if(index(('<', '<=', '≤'), $operator), 'max', 'min');
}
///
/// Get value of an expression, based on a found operator
///
/// @param {String} $expression - Expression to extract value from
/// @param {String} $operator - Operator from `$expression`
///
/// @return {Number} - A numeric value
///
@function get-expression-value($expression, $operator) {
$operator-index: str-index($expression, $operator);
$value: str-slice($expression, $operator-index + str-length($operator));
@if map-has-key($breakpoints, $value) {
$value: map-get($breakpoints, $value);
} @else {
$value: to-number($value);
}
$interval: map-get($unit-intervals, unit($value));
@if not $interval {
// It is not possible to include a mixin inside a function, so we have to
// rely on the `im-log(..)` function rather than the `log(..)` mixin. Because
// functions cannot be called anywhere in Sass, we need to hack the call in
// a dummy variable, such as `$_`. If anybody ever raise a scoping issue with
// Sass 3.3, change this line in `@if im-log(..) {}` instead.
$_: im-log('Unknown unit `#{unit($value)}`.');
}
@if $operator == '>' {
$value: $value + $interval;
} @else if $operator == '<' {
$value: $value - $interval;
}
@return $value;
}
///
/// Parse an expression to return a valid media-query expression
///
/// @param {String} $expression - Expression to parse
///
/// @return {String} - Valid media query
///
@function parse-expression($expression) {
// If it is part of $media-expressions, it has no operator
// then there is no need to go any further, just return the value
@if map-has-key($media-expressions, $expression) {
@return map-get($media-expressions, $expression);
}
$operator: get-expression-operator($expression);
$dimension: get-expression-dimension($expression, $operator);
$prefix: get-expression-prefix($operator);
$value: get-expression-value($expression, $operator);
@return '(#{$prefix}-#{$dimension}: #{$value})';
}
///
/// Get a shallow list of resolved feature queries
///
/// @param {String} $queries - A nested list structure
/// @param {Boolean} $do-parse - Try to parse expressions
///
/// @return {List} - A one item deep nested list like `(a b, c d e)`
///
@function resolve-feature-queries($queries, $do-parse: true) {
$separator: list-separator($queries);
$result: if($separator == 'space', ((), ), ());
@each $query in $queries {
$chainable-queries: ();
@if (type-of($query) == 'list') {
// List item is a list itself
$chainable-queries: resolve-feature-queries($query);
} @else {
// List item is a string
$parsed-query: if($do-parse, parse-expression($query), $query);
@if (type-of($parsed-query) == 'list') {
// Parsed expression is a list
$chainable-queries: resolve-feature-queries($parsed-query, false);
} @else {
// Parsed expression is a string
$chainable-queries: ($parsed-query);
}
}
$result: append-feature-query($result, $chainable-queries, $separator);
}
@return $result;
}
///
/// Combine two query lists as a logical AND / OR operation
///
/// @param {List} $base-queries - The host list
/// @param {List} $append-queries - The list that is being appended
/// @param {String} $separator - Either `space` or `comma`
///
/// @return {List} - A one item deep nested list like `(a b, c d e)`
///
@function append-feature-query($base-queries, $append-queries, $separator) {
$result: if($separator == 'space', (), $base-queries);
@each $append-query in $append-queries {
@if ($separator == 'space') {
// Logical AND
@each $base-query in $base-queries {
$updated-query: join($base-query, $append-query, $separator);
$result: append($result, $updated-query, 'comma');
}
} @else {
// Logical OR
$result: append($result, $append-query, 'comma');
}
}
@return $result;
}
///
/// Parse a list of resolved expressions to return a valid media-query
///
/// @param {List} $queries - A one item deep nested list like `(a b, c d e)`
///
/// @return {String} - A valid media-query string
///
@function parse-media-query($queries) {
$result: null;
$flat-queries: ();
$separator: list-separator($queries);
$conjunction: if($separator == 'space', ' and ', ', ');
@if (type-of($queries) == 'string') {
@return $queries;
}
@each $query in $queries {
@if (type-of($query) == 'list') {
$flat-queries: append($flat-queries, parse-media-query($query));
} @else {
$flat-queries: append($flat-queries, $query);
}
}
@for $i from 1 through length($flat-queries) {
$e: nth($flat-queries, $i);
$result: unquote('#{$result}#{$e}#{if($i != length($flat-queries), $conjunction, '')}');
}
@return $result;
}
////
/// Cross-engine logging engine
/// @author Hugo Giraudel
/// @access private
////
///
/// Log a message either with `@error` if supported
/// else with `@warn`, using `feature-exists('at-error')`
/// to detect support.
///
/// @param {String} $message - Message to log
///
@function im-log($message) {
@if feature-exists('at-error') {
@error $message;
} @else {
@warn $message;
$_: noop();
}
@return $message;
}
///
/// Wrapper mixin for the log function so it can be used with a more friendly
/// API than `@if im-log('..') {}` or `$_: im-log('..')`. Basically, use the function
/// within functions because it is not possible to include a mixin in a function
/// and use the mixin everywhere else because it's much more elegant.
///
/// @param {String} $message - Message to log
///
@mixin log($message) {
@if im-log($message) {}
}
///
/// Function with no `@return` called next to `@warn` in Sass 3.3
/// to trigger a compiling error and stop the process.
///
@function noop() {}
///
/// Determines whether a list of conditions is intercepted by the static breakpoint.
///
/// @param {Arglist} $conditions - Media query conditions
///
/// @return {Boolean} - Returns true if the conditions are intercepted by the static breakpoint
///
@function im-intercepts-static-breakpoint($conditions...) {
$no-media-breakpoint-value: map-get($breakpoints, $im-no-media-breakpoint);
@if not $no-media-breakpoint-value {
@if im-log('`#{$im-no-media-breakpoint}` is not a valid breakpoint.') {}
}
@each $condition in $conditions {
@if not map-has-key($media-expressions, $condition) {
$operator: get-expression-operator($condition);
$prefix: get-expression-prefix($operator);
$value: get-expression-value($condition, $operator);
@if ($prefix == 'max' and $value <= $no-media-breakpoint-value) or
($prefix == 'min' and $value > $no-media-breakpoint-value) {
@return false;
}
} @else if not index($im-no-media-expressions, $condition) {
@return false;
}
}
@return true;
}
///
/// Slice `$list` between `$start` and `$end` indexes
///
/// @access private
///
/// @param {List} $list - List to slice
/// @param {Number} $start [1] - Start index
/// @param {Number} $end [length($list)] - End index
///
/// @return {List} Sliced list
///
@function slice($list, $start: 1, $end: length($list)) {
@if length($list) < 1 or $start > $end {
@return ();
}
$result: ();
@for $i from $start through $end {
$result: append($result, nth($list, $i));
}
@return $result;
}
////
/// String to number converter
/// @author Hugo Giraudel
/// @access private
////
///
/// Casts a string into a number
///
/// @param {String | Number} $value - Value to be parsed
///
/// @return {Number}
///
@function to-number($value) {
@if type-of($value) == 'number' {
@return $value;
} @else if type-of($value) != 'string' {
$_: im-log('Value for `to-number` should be a number or a string.');
}
$first-character: str-slice($value, 1, 1);
$result: 0;
$digits: 0;
$minus: ($first-character == '-');
$numbers: ('0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9);
// Remove +/- sign if present at first character
@if ($first-character == '+' or $first-character == '-') {
$value: str-slice($value, 2);
}
@for $i from 1 through str-length($value) {
$character: str-slice($value, $i, $i);
@if not (index(map-keys($numbers), $character) or $character == '.') {
@return to-length(if($minus, -$result, $result), str-slice($value, $i))
}
@if $character == '.' {
$digits: 1;
} @else if $digits == 0 {
$result: $result * 10 + map-get($numbers, $character);
} @else {
$digits: $digits * 10;
$result: $result + map-get($numbers, $character) / $digits;
}
}
@return if($minus, -$result, $result);
}
///
/// Add `$unit` to `$value`
///
/// @param {Number} $value - Value to add unit to
/// @param {String} $unit - String representation of the unit
///
/// @return {Number} - `$value` expressed in `$unit`
///
@function to-length($value, $unit) {
$units: ('px': 1px, 'cm': 1cm, 'mm': 1mm, '%': 1%, 'ch': 1ch, 'pc': 1pc, 'in': 1in, 'em': 1em, 'rem': 1rem, 'pt': 1pt, 'ex': 1ex, 'vw': 1vw, 'vh': 1vh, 'vmin': 1vmin, 'vmax': 1vmax);
@if not index(map-keys($units), $unit) {
$_: im-log('Invalid unit `#{$unit}`.');
}
@return $value * map-get($units, $unit);
}
///
/// This mixin aims at redefining the configuration just for the scope of
/// the call. It is helpful when having a component needing an extended
/// configuration such as custom breakpoints (referred to as tweakpoints)
/// for instance.
///
/// @author Hugo Giraudel
///
/// @param {Map} $tweakpoints [()] - Map of tweakpoints to be merged with `$breakpoints`
/// @param {Map} $tweak-media-expressions [()] - Map of tweaked media expressions to be merged with `$media-expression`
///
/// @example scss - Extend the global breakpoints with a tweakpoint
/// @include media-context(('custom': 678px)) {
/// .foo {
/// @include media('>phone', '<=custom') {
/// // ...
/// }
/// }
/// }
///
/// @example scss - Extend the global media expressions with a custom one
/// @include media-context($tweak-media-expressions: ('all': 'all')) {
/// .foo {
/// @include media('all', '>phone') {
/// // ...
/// }
/// }
/// }
///
/// @example scss - Extend both configuration maps
/// @include media-context(('custom': 678px), ('all': 'all')) {
/// .foo {
/// @include media('all', '>phone', '<=custom') {
/// // ...
/// }
/// }
/// }
///
@mixin media-context($tweakpoints: (), $tweak-media-expressions: ()) {
// Save global configuration
$global-breakpoints: $breakpoints;
$global-media-expressions: $media-expressions;
// Update global configuration
$breakpoints: map-merge($breakpoints, $tweakpoints) !global;
$media-expressions: map-merge($media-expressions, $tweak-media-expressions) !global;
@content;
// Restore global configuration
$breakpoints: $global-breakpoints !global;
$media-expressions: $global-media-expressions !global;
}
////
/// include-media public exposed API
/// @author Eduardo Boucas
/// @access public
////
///
/// Generates a media query based on a list of conditions
///
/// @param {Arglist} $conditions - Media query conditions
///
/// @example scss - With a single set breakpoint
/// @include media('>phone') { }
///
/// @example scss - With two set breakpoints
/// @include media('>phone', '<=tablet') { }
///
/// @example scss - With custom values
/// @include media('>=358px', '<850px') { }
///
/// @example scss - With set breakpoints with custom values
/// @include media('>desktop', '<=1350px') { }
///
/// @example scss - With a static expression
/// @include media('retina2x') { }
///
/// @example scss - Mixing everything
/// @include media('>=350px', '<tablet', 'retina3x') { }
///
@mixin media($conditions...) {
$is-list-mode: (length($conditions) == 1 and type-of(nth($conditions, 1)) == 'list');
@if ($im-media-support and length($conditions) == 0) or
(not $im-media-support and im-intercepts-static-breakpoint($conditions...) and not $is-list-mode) {
@content;
} @else if ($im-media-support and $is-list-mode) {
// List mode with AND / OR conditions
@media #{unquote(parse-media-query(resolve-feature-queries(nth($conditions, 1))))} {
@content;
}
} @else if ($im-media-support and length($conditions) > 0) {
// Legacy mode
@media #{unquote(parse-media-query(parse-expression(nth($conditions, 1))))} {
// Recursive call
@include media(slice($conditions, 2)...) {
@content;
}
}
}
}
@charset "UTF-8";
// _ _ _ _ _
// (_) | | | | | (_)
// _ _ __ ___| |_ _ __| | ___ _ __ ___ ___ __| |_ __ _
// | | '_ \ / __| | | | |/ _` |/ _ \ | '_ ` _ \ / _ \/ _` | |/ _` |
// | | | | | (__| | |_| | (_| | __/ | | | | | | __/ (_| | | (_| |
// |_|_| |_|\___|_|\__,_|\__,_|\___| |_| |_| |_|\___|\__,_|_|\__,_|
//
// Simple, elegant and maintainable media queries in Sass
// v1.4.9
//
// http://include-media.com
//
// Authors: Eduardo Boucas (@eduardoboucas)
// Hugo Giraudel (@hugogiraudel)
//
// This project is licensed under the terms of the MIT license
////
/// include-media library public configuration
/// @author Eduardo Boucas
/// @access public
////
///
/// Creates a list of global breakpoints
///
/// @example scss - Creates a single breakpoint with the label `phone`
/// $breakpoints: ('phone': 320px);
///
$breakpoints: (
'phone': 320px,
'tablet': 768px,
'desktop': 1024px
) !default;
///
/// Creates a list of static expressions or media types
///
/// @example scss - Creates a single media type (screen)
/// $media-expressions: ('screen': 'screen');
///
/// @example scss - Creates a static expression with logical disjunction (OR operator)
/// $media-expressions: (
/// 'retina2x': '(-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi)'
/// );
///
$media-expressions: (
'screen': 'screen',
'print': 'print',
'handheld': 'handheld',
'landscape': '(orientation: landscape)',
'portrait': '(orientation: portrait)',
'retina2x': '(-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi), (min-resolution: 2dppx)',
'retina3x': '(-webkit-min-device-pixel-ratio: 3), (min-resolution: 350dpi), (min-resolution: 3dppx)'
) !default;
///
/// Defines a number to be added or subtracted from each unit when declaring breakpoints with exclusive intervals
///
/// @example scss - Interval for pixels is defined as `1` by default
/// @include media('>128px') {}
///
/// /* Generates: */
/// @media (min-width: 129px) {}
///
/// @example scss - Interval for ems is defined as `0.01` by default
/// @include media('>20em') {}
///
/// /* Generates: */
/// @media (min-width: 20.01em) {}
///
/// @example scss - Interval for rems is defined as `0.1` by default, to be used with `font-size: 62.5%;`
/// @include media('>2.0rem') {}
///
/// /* Generates: */
/// @media (min-width: 2.1rem) {}
///
$unit-intervals: (
'px': 1,
'em': 0.01,
'rem': 0.1,
'': 0
) !default;
///
/// Defines whether support for media queries is available, useful for creating separate stylesheets
/// for browsers that don't support media queries.
///
/// @example scss - Disables support for media queries
/// $im-media-support: false;
/// @include media('>=tablet') {
/// .foo {
/// color: tomato;
/// }
/// }
///
/// /* Generates: */
/// .foo {
/// color: tomato;
/// }
///
$im-media-support: true !default;
///
/// Selects which breakpoint to emulate when support for media queries is disabled. Media queries that start at or
/// intercept the breakpoint will be displayed, any others will be ignored.
///
/// @example scss - This media query will show because it intercepts the static breakpoint
/// $im-media-support: false;
/// $im-no-media-breakpoint: 'desktop';
/// @include media('>=tablet') {
/// .foo {
/// color: tomato;
/// }
/// }
///
/// /* Generates: */
/// .foo {
/// color: tomato;
/// }
///
/// @example scss - This media query will NOT show because it does not intercept the desktop breakpoint
/// $im-media-support: false;
/// $im-no-media-breakpoint: 'tablet';
/// @include media('>=desktop') {
/// .foo {
/// color: tomato;
/// }
/// }
///
/// /* No output */
///
$im-no-media-breakpoint: 'desktop' !default;
///
/// Selects which media expressions are allowed in an expression for it to be used when media queries
/// are not supported.
///
/// @example scss - This media query will show because it intercepts the static breakpoint and contains only accepted media expressions
/// $im-media-support: false;
/// $im-no-media-breakpoint: 'desktop';
/// $im-no-media-expressions: ('screen');
/// @include media('>=tablet', 'screen') {
/// .foo {
/// color: tomato;
/// }
/// }
///
/// /* Generates: */
/// .foo {
/// color: tomato;
/// }
///
/// @example scss - This media query will NOT show because it intercepts the static breakpoint but contains a media expression that is not accepted
/// $im-media-support: false;
/// $im-no-media-breakpoint: 'desktop';
/// $im-no-media-expressions: ('screen');
/// @include media('>=tablet', 'retina2x') {
/// .foo {
/// color: tomato;
/// }
/// }
///
/// /* No output */
///
$im-no-media-expressions: ('screen', 'portrait', 'landscape') !default;
////
/// Cross-engine logging engine
/// @author Hugo Giraudel
/// @access private
////
///
/// Log a message either with `@error` if supported
/// else with `@warn`, using `feature-exists('at-error')`
/// to detect support.
///
/// @param {String} $message - Message to log
///
@function im-log($message) {
@if feature-exists('at-error') {
@error $message;
} @else {
@warn $message;
$_: noop();
}
@return $message;
}
///
/// Wrapper mixin for the log function so it can be used with a more friendly
/// API than `@if im-log('..') {}` or `$_: im-log('..')`. Basically, use the function
/// within functions because it is not possible to include a mixin in a function
/// and use the mixin everywhere else because it's much more elegant.
///
/// @param {String} $message - Message to log
///
@mixin log($message) {
@if im-log($message) {}
}
///
/// Function with no `@return` called next to `@warn` in Sass 3.3
/// to trigger a compiling error and stop the process.
///
@function noop() {}
///
/// Determines whether a list of conditions is intercepted by the static breakpoint.
///
/// @param {Arglist} $conditions - Media query conditions
///
/// @return {Boolean} - Returns true if the conditions are intercepted by the static breakpoint
///
@function im-intercepts-static-breakpoint($conditions...) {
$no-media-breakpoint-value: map-get($breakpoints, $im-no-media-breakpoint);
@if not $no-media-breakpoint-value {
@if im-log('`#{$im-no-media-breakpoint}` is not a valid breakpoint.') {}
}
@each $condition in $conditions {
@if not map-has-key($media-expressions, $condition) {
$operator: get-expression-operator($condition);
$prefix: get-expression-prefix($operator);
$value: get-expression-value($condition, $operator);
@if ($prefix == 'max' and $value <= $no-media-breakpoint-value) or
($prefix == 'min' and $value > $no-media-breakpoint-value) {
@return false;
}
} @else if not index($im-no-media-expressions, $condition) {
@return false;
}
}
@return true;
}
////
/// Parsing engine
/// @author Hugo Giraudel
/// @access private
////
///
/// Get operator of an expression
///
/// @param {String} $expression - Expression to extract operator from
///
/// @return {String} - Any of `>=`, `>`, `<=`, `<`, `≥`, `≤`
///
@function get-expression-operator($expression) {
@each $operator in ('>=', '>', '<=', '<', '≥', '≤') {
@if str-index($expression, $operator) {
@return $operator;
}
}
// It is not possible to include a mixin inside a function, so we have to
// rely on the `im-log(..)` function rather than the `log(..)` mixin. Because
// functions cannot be called anywhere in Sass, we need to hack the call in
// a dummy variable, such as `$_`. If anybody ever raise a scoping issue with
// Sass 3.3, change this line in `@if im-log(..) {}` instead.
$_: im-log('No operator found in `#{$expression}`.');
}
///
/// Get dimension of an expression, based on a found operator
///
/// @param {String} $expression - Expression to extract dimension from
/// @param {String} $operator - Operator from `$expression`
///
/// @return {String} - `width` or `height` (or potentially anything else)
///
@function get-expression-dimension($expression, $operator) {
$operator-index: str-index($expression, $operator);
$parsed-dimension: str-slice($expression, 0, $operator-index - 1);
$dimension: 'width';
@if str-length($parsed-dimension) > 0 {
$dimension: $parsed-dimension;
}
@return $dimension;
}
///
/// Get dimension prefix based on an operator
///
/// @param {String} $operator - Operator
///
/// @return {String} - `min` or `max`
///
@function get-expression-prefix($operator) {
@return if(index(('<', '<=', '≤'), $operator), 'max', 'min');
}
///
/// Get value of an expression, based on a found operator
///
/// @param {String} $expression - Expression to extract value from
/// @param {String} $operator - Operator from `$expression`
///
/// @return {Number} - A numeric value
///
@function get-expression-value($expression, $operator) {
$operator-index: str-index($expression, $operator);
$value: str-slice($expression, $operator-index + str-length($operator));
@if map-has-key($breakpoints, $value) {
$value: map-get($breakpoints, $value);
} @else {
$value: to-number($value);
}
$interval: map-get($unit-intervals, unit($value));
@if not $interval {
// It is not possible to include a mixin inside a function, so we have to
// rely on the `im-log(..)` function rather than the `log(..)` mixin. Because
// functions cannot be called anywhere in Sass, we need to hack the call in
// a dummy variable, such as `$_`. If anybody ever raise a scoping issue with
// Sass 3.3, change this line in `@if im-log(..) {}` instead.
$_: im-log('Unknown unit `#{unit($value)}`.');
}
@if $operator == '>' {
$value: $value + $interval;
} @else if $operator == '<' {
$value: $value - $interval;
}
@return $value;
}
///
/// Parse an expression to return a valid media-query expression
///
/// @param {String} $expression - Expression to parse
///
/// @return {String} - Valid media query
///
@function parse-expression($expression) {
// If it is part of $media-expressions, it has no operator
// then there is no need to go any further, just return the value
@if map-has-key($media-expressions, $expression) {
@return map-get($media-expressions, $expression);
}
$operator: get-expression-operator($expression);
$dimension: get-expression-dimension($expression, $operator);
$prefix: get-expression-prefix($operator);
$value: get-expression-value($expression, $operator);
@return '(#{$prefix}-#{$dimension}: #{$value})';
}
///
/// Slice `$list` between `$start` and `$end` indexes
///
/// @access private
///
/// @param {List} $list - List to slice
/// @param {Number} $start [1] - Start index
/// @param {Number} $end [length($list)] - End index
///
/// @return {List} Sliced list
///
@function slice($list, $start: 1, $end: length($list)) {
@if length($list) < 1 or $start > $end {
@return ();
}
$result: ();
@for $i from $start through $end {
$result: append($result, nth($list, $i));
}
@return $result;
}
////
/// String to number converter
/// @author Hugo Giraudel
/// @access private
////
///
/// Casts a string into a number
///
/// @param {String | Number} $value - Value to be parsed
///
/// @return {Number}
///
@function to-number($value) {
@if type-of($value) == 'number' {
@return $value;
} @else if type-of($value) != 'string' {
$_: im-log('Value for `to-number` should be a number or a string.');
}
$first-character: str-slice($value, 1, 1);
$result: 0;
$digits: 0;
$minus: ($first-character == '-');
$numbers: ('0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9);
// Remove +/- sign if present at first character
@if ($first-character == '+' or $first-character == '-') {
$value: str-slice($value, 2);
}
@for $i from 1 through str-length($value) {
$character: str-slice($value, $i, $i);
@if not (index(map-keys($numbers), $character) or $character == '.') {
@return to-length(if($minus, -$result, $result), str-slice($value, $i))
}
@if $character == '.' {
$digits: 1;
} @else if $digits == 0 {
$result: $result * 10 + map-get($numbers, $character);
} @else {
$digits: $digits * 10;
$result: $result + map-get($numbers, $character) / $digits;
}
}
@return if($minus, -$result, $result);
}
///
/// Add `$unit` to `$value`
///
/// @param {Number} $value - Value to add unit to
/// @param {String} $unit - String representation of the unit
///
/// @return {Number} - `$value` expressed in `$unit`
///
@function to-length($value, $unit) {
$units: ('px': 1px, 'cm': 1cm, 'mm': 1mm, '%': 1%, 'ch': 1ch, 'pc': 1pc, 'in': 1in, 'em': 1em, 'rem': 1rem, 'pt': 1pt, 'ex': 1ex, 'vw': 1vw, 'vh': 1vh, 'vmin': 1vmin, 'vmax': 1vmax);
@if not index(map-keys($units), $unit) {
$_: im-log('Invalid unit `#{$unit}`.');
}
@return $value * map-get($units, $unit);
}
///
/// This mixin aims at redefining the configuration just for the scope of
/// the call. It is helpful when having a component needing an extended
/// configuration such as custom breakpoints (referred to as tweakpoints)
/// for instance.
///
/// @author Hugo Giraudel
///
/// @param {Map} $tweakpoints [()] - Map of tweakpoints to be merged with `$breakpoints`
/// @param {Map} $tweak-media-expressions [()] - Map of tweaked media expressions to be merged with `$media-expression`
///
/// @example scss - Extend the global breakpoints with a tweakpoint
/// @include media-context(('custom': 678px)) {
/// .foo {
/// @include media('>phone', '<=custom') {
/// // ...
/// }
/// }
/// }
///
/// @example scss - Extend the global media expressions with a custom one
/// @include media-context($tweak-media-expressions: ('all': 'all')) {
/// .foo {
/// @include media('all', '>phone') {
/// // ...
/// }
/// }
/// }
///
/// @example scss - Extend both configuration maps
/// @include media-context(('custom': 678px), ('all': 'all')) {
/// .foo {
/// @include media('all', '>phone', '<=custom') {
/// // ...
/// }
/// }
/// }
///
@mixin media-context($tweakpoints: (), $tweak-media-expressions: ()) {
// Save global configuration
$global-breakpoints: $breakpoints;
$global-media-expressions: $media-expressions;
// Update global configuration
$breakpoints: map-merge($breakpoints, $tweakpoints) !global;
$media-expressions: map-merge($media-expressions, $tweak-media-expressions) !global;
@content;
// Restore global configuration
$breakpoints: $global-breakpoints !global;
$media-expressions: $global-media-expressions !global;
}
////
/// include-media public exposed API
/// @author Eduardo Boucas
/// @access public
////
///
/// Generates a media query based on a list of conditions
///
/// @param {Arglist} $conditions - Media query conditions
///
/// @example scss - With a single set breakpoint
/// @include media('>phone') { }
///
/// @example scss - With two set breakpoints
/// @include media('>phone', '<=tablet') { }
///
/// @example scss - With custom values
/// @include media('>=358px', '<850px') { }
///
/// @example scss - With set breakpoints with custom values
/// @include media('>desktop', '<=1350px') { }
///
/// @example scss - With a static expression
/// @include media('retina2x') { }
///
/// @example scss - Mixing everything
/// @include media('>=350px', '<tablet', 'retina3x') { }
///
@mixin media($conditions...) {
@if ($im-media-support and length($conditions) == 0) or
(not $im-media-support and im-intercepts-static-breakpoint($conditions...)) {
@content;
} @else if ($im-media-support and length($conditions) > 0) {
@media #{unquote(parse-expression(nth($conditions, 1)))} {
// Recursive call
@include media(slice($conditions, 2)...) {
@content;
}
}
}
}
////
/// include-media library public configuration
/// @author Eduardo Boucas
/// @access public
////
///
/// Creates a list of global breakpoints
///
/// @example scss - Creates a single breakpoint with the label `phone`
/// $breakpoints: ('phone': 320px);
///
$breakpoints: (
'phone': 320px,
'tablet': 768px,
'desktop': 1024px
) !default;
///
/// Creates a list of static expressions or media types
///
/// @example scss - Creates a single media type (screen)
/// $media-expressions: ('screen': 'screen');
///
/// @example scss - Creates a static expression with logical disjunction (OR operator)
/// $media-expressions: (
/// 'retina2x': '(-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi)'
/// );
///
$media-expressions: (
'screen': 'screen',
'print': 'print',
'handheld': 'handheld',
'landscape': '(orientation: landscape)',
'portrait': '(orientation: portrait)',
'retina2x': ('(-webkit-min-device-pixel-ratio: 2)', '(min-resolution: 192dpi)', '(min-resolution: 2dppx)'),
'retina3x': ('(-webkit-min-device-pixel-ratio: 3)', '(min-resolution: 350dpi)', '(min-resolution: 3dppx)')
) !default;
///
/// Defines a number to be added or subtracted from each unit when declaring breakpoints with exclusive intervals
///
/// @example scss - Interval for pixels is defined as `1` by default
/// @include media('>128px') {}
///
/// /* Generates: */
/// @media (min-width: 129px) {}
///
/// @example scss - Interval for ems is defined as `0.01` by default
/// @include media('>20em') {}
///
/// /* Generates: */
/// @media (min-width: 20.01em) {}
///
/// @example scss - Interval for rems is defined as `0.1` by default, to be used with `font-size: 62.5%;`
/// @include media('>2.0rem') {}
///
/// /* Generates: */
/// @media (min-width: 2.1rem) {}
///
$unit-intervals: (
'px': 1,
'em': 0.01,
'rem': 0.1,
'': 0
) !default;
///
/// Defines whether support for media queries is available, useful for creating separate stylesheets
/// for browsers that don't support media queries.
///
/// @example scss - Disables support for media queries
/// $im-media-support: false;
/// @include media('>=tablet') {
/// .foo {
/// color: tomato;
/// }
/// }
///
/// /* Generates: */
/// .foo {
/// color: tomato;
/// }
///
$im-media-support: true !default;
///
/// Selects which breakpoint to emulate when support for media queries is disabled. Media queries that start at or
/// intercept the breakpoint will be displayed, any others will be ignored.
///
/// @example scss - This media query will show because it intercepts the static breakpoint
/// $im-media-support: false;
/// $im-no-media-breakpoint: 'desktop';
/// @include media('>=tablet') {
/// .foo {
/// color: tomato;
/// }
/// }
///
/// /* Generates: */
/// .foo {
/// color: tomato;
/// }
///
/// @example scss - This media query will NOT show because it does not intercept the desktop breakpoint
/// $im-media-support: false;
/// $im-no-media-breakpoint: 'tablet';
/// @include media('>=desktop') {
/// .foo {
/// color: tomato;
/// }
/// }
///
/// /* No output */
///
$im-no-media-breakpoint: 'desktop' !default;
///
/// Selects which media expressions are allowed in an expression for it to be used when media queries
/// are not supported.
///
/// @example scss - This media query will show because it intercepts the static breakpoint and contains only accepted media expressions
/// $im-media-support: false;
/// $im-no-media-breakpoint: 'desktop';
/// $im-no-media-expressions: ('screen');
/// @include media('>=tablet', 'screen') {
/// .foo {
/// color: tomato;
/// }
/// }
///
/// /* Generates: */
/// .foo {
/// color: tomato;
/// }
///
/// @example scss - This media query will NOT show because it intercepts the static breakpoint but contains a media expression that is not accepted
/// $im-media-support: false;
/// $im-no-media-breakpoint: 'desktop';
/// $im-no-media-expressions: ('screen');
/// @include media('>=tablet', 'retina2x') {
/// .foo {
/// color: tomato;
/// }
/// }
///
/// /* No output */
///
$im-no-media-expressions: ('screen', 'portrait', 'landscape') !default;
////
/// include-media public exposed API
/// @author Eduardo Boucas
/// @access public
////
///
/// Generates a media query based on a list of conditions
///
/// @param {Arglist} $conditions - Media query conditions
///
/// @example scss - With a single set breakpoint
/// @include media('>phone') { }
///
/// @example scss - With two set breakpoints
/// @include media('>phone', '<=tablet') { }
///
/// @example scss - With custom values
/// @include media('>=358px', '<850px') { }
///
/// @example scss - With set breakpoints with custom values
/// @include media('>desktop', '<=1350px') { }
///
/// @example scss - With a static expression
/// @include media('retina2x') { }
///
/// @example scss - Mixing everything
/// @include media('>=350px', '<tablet', 'retina3x') { }
///
@mixin media($conditions...) {
$is-list-mode: (length($conditions) == 1 and type-of(nth($conditions, 1)) == 'list');
@if ($im-media-support and length($conditions) == 0) or
(not $im-media-support and im-intercepts-static-breakpoint($conditions...) and not $is-list-mode) {
@content;
} @else if ($im-media-support and $is-list-mode) {
// List mode with AND / OR conditions
@media #{unquote(parse-media-query(resolve-feature-queries(nth($conditions, 1))))} {
@content;
}
} @else if ($im-media-support and length($conditions) > 0) {
// Legacy mode
@media #{unquote(parse-media-query(parse-expression(nth($conditions, 1))))} {
// Recursive call
@include media(slice($conditions, 2)...) {
@content;
}
}
}
}
////
/// Parsing engine
/// @author Hugo Giraudel
/// @access private
////
///
/// Get operator of an expression
///
/// @param {String} $expression - Expression to extract operator from
///
/// @return {String} - Any of `>=`, `>`, `<=`, `<`, `≥`, `≤`
///
@function get-expression-operator($expression) {
@each $operator in ('>=', '>', '<=', '<', '≥', '≤') {
@if str-index($expression, $operator) {
@return $operator;
}
}
// It is not possible to include a mixin inside a function, so we have to
// rely on the `im-log(..)` function rather than the `log(..)` mixin. Because
// functions cannot be called anywhere in Sass, we need to hack the call in
// a dummy variable, such as `$_`. If anybody ever raise a scoping issue with
// Sass 3.3, change this line in `@if im-log(..) {}` instead.
$_: im-log('No operator found in `#{$expression}`.');
}
///
/// Get dimension of an expression, based on a found operator
///
/// @param {String} $expression - Expression to extract dimension from
/// @param {String} $operator - Operator from `$expression`
///
/// @return {String} - `width` or `height` (or potentially anything else)
///
@function get-expression-dimension($expression, $operator) {
$operator-index: str-index($expression, $operator);
$parsed-dimension: str-slice($expression, 0, $operator-index - 1);
$dimension: 'width';
@if str-length($parsed-dimension) > 0 {
$dimension: $parsed-dimension;
}
@return $dimension;
}
///
/// Get dimension prefix based on an operator
///
/// @param {String} $operator - Operator
///
/// @return {String} - `min` or `max`
///
@function get-expression-prefix($operator) {
@return if(index(('<', '<=', '≤'), $operator), 'max', 'min');
}
///
/// Get value of an expression, based on a found operator
///
/// @param {String} $expression - Expression to extract value from
/// @param {String} $operator - Operator from `$expression`
///
/// @return {Number} - A numeric value
///
@function get-expression-value($expression, $operator) {
$operator-index: str-index($expression, $operator);
$value: str-slice($expression, $operator-index + str-length($operator));
@if map-has-key($breakpoints, $value) {
$value: map-get($breakpoints, $value);
} @else {
$value: to-number($value);
}
$interval: map-get($unit-intervals, unit($value));
@if not $interval {
// It is not possible to include a mixin inside a function, so we have to
// rely on the `im-log(..)` function rather than the `log(..)` mixin. Because
// functions cannot be called anywhere in Sass, we need to hack the call in
// a dummy variable, such as `$_`. If anybody ever raise a scoping issue with
// Sass 3.3, change this line in `@if im-log(..) {}` instead.
$_: im-log('Unknown unit `#{unit($value)}`.');
}
@if $operator == '>' {
$value: $value + $interval;
} @else if $operator == '<' {
$value: $value - $interval;
}
@return $value;
}
///
/// Parse an expression to return a valid media-query expression
///
/// @param {String} $expression - Expression to parse
///
/// @return {String} - Valid media query
///
@function parse-expression($expression) {
// If it is part of $media-expressions, it has no operator
// then there is no need to go any further, just return the value
@if map-has-key($media-expressions, $expression) {
@return map-get($media-expressions, $expression);
}
$operator: get-expression-operator($expression);
$dimension: get-expression-dimension($expression, $operator);
$prefix: get-expression-prefix($operator);
$value: get-expression-value($expression, $operator);
@return '(#{$prefix}-#{$dimension}: #{$value})';
}
///
/// Get a shallow list of resolved feature queries
///
/// @param {String} $queries - A nested list structure
/// @param {Boolean} $do-parse - Try to parse expressions
///
/// @return {List} - A one item deep nested list like `(a b, c d e)`
///
@function resolve-feature-queries($queries, $do-parse: true) {
$separator: list-separator($queries);
$result: if($separator == 'space', ((), ), ());
@each $query in $queries {
$chainable-queries: ();
@if (type-of($query) == 'list') {
// List item is a list itself
$chainable-queries: resolve-feature-queries($query);
} @else {
// List item is a string
$parsed-query: if($do-parse, parse-expression($query), $query);
@if (type-of($parsed-query) == 'list') {
// Parsed expression is a list
$chainable-queries: resolve-feature-queries($parsed-query, false);
} @else {
// Parsed expression is a string
$chainable-queries: ($parsed-query);
}
}
$result: append-feature-query($result, $chainable-queries, $separator);
}
@return $result;
}
///
/// Combine two query lists as a logical AND / OR operation
///
/// @param {List} $base-queries - The host list
/// @param {List} $append-queries - The list that is being appended
/// @param {String} $separator - Either `space` or `comma`
///
/// @return {List} - A one item deep nested list like `(a b, c d e)`
///
@function append-feature-query($base-queries, $append-queries, $separator) {
$result: if($separator == 'space', (), $base-queries);
@each $append-query in $append-queries {
@if ($separator == 'space') {
// Logical AND
@each $base-query in $base-queries {
$updated-query: join($base-query, $append-query, $separator);
$result: append($result, $updated-query, 'comma');
}
} @else {
// Logical OR
$result: append($result, $append-query, 'comma');
}
}
@return $result;
}
///
/// Parse a list of resolved expressions to return a valid media-query
///
/// @param {List} $queries - A one item deep nested list like `(a b, c d e)`
///
/// @return {String} - A valid media-query string
///
@function parse-media-query($queries) {
$result: null;
$flat-queries: ();
$separator: list-separator($queries);
$conjunction: if($separator == 'space', ' and ', ', ');
@if (type-of($queries) == 'string') {
@return $queries;
}
@each $query in $queries {
@if (type-of($query) == 'list') {
$flat-queries: append($flat-queries, parse-media-query($query));
} @else {
$flat-queries: append($flat-queries, $query);
}
}
@for $i from 1 through length($flat-queries) {
$e: nth($flat-queries, $i);
$result: unquote('#{$result}#{$e}#{if($i != length($flat-queries), $conjunction, '')}');
}
@return $result;
}
////
/// Cross-engine logging engine
/// @author Hugo Giraudel
/// @access private
////
///
/// Log a message either with `@error` if supported
/// else with `@warn`, using `feature-exists('at-error')`
/// to detect support.
///
/// @param {String} $message - Message to log
///
@function im-log($message) {
@if feature-exists('at-error') {
@error $message;
} @else {
@warn $message;
$_: noop();
}
@return $message;
}
///
/// Wrapper mixin for the log function so it can be used with a more friendly
/// API than `@if im-log('..') {}` or `$_: im-log('..')`. Basically, use the function
/// within functions because it is not possible to include a mixin in a function
/// and use the mixin everywhere else because it's much more elegant.
///
/// @param {String} $message - Message to log
///
@mixin log($message) {
@if im-log($message) {}
}
///
/// Function with no `@return` called next to `@warn` in Sass 3.3
/// to trigger a compiling error and stop the process.
///
@function noop() {}
///
/// Determines whether a list of conditions is intercepted by the static breakpoint.
///
/// @param {Arglist} $conditions - Media query conditions
///
/// @return {Boolean} - Returns true if the conditions are intercepted by the static breakpoint
///
@function im-intercepts-static-breakpoint($conditions...) {
$no-media-breakpoint-value: map-get($breakpoints, $im-no-media-breakpoint);
@if not $no-media-breakpoint-value {
@if im-log('`#{$im-no-media-breakpoint}` is not a valid breakpoint.') {}
}
@each $condition in $conditions {
@if not map-has-key($media-expressions, $condition) {
$operator: get-expression-operator($condition);
$prefix: get-expression-prefix($operator);
$value: get-expression-value($condition, $operator);
@if ($prefix == 'max' and $value <= $no-media-breakpoint-value) or
($prefix == 'min' and $value > $no-media-breakpoint-value) {
@return false;
}
} @else if not index($im-no-media-expressions, $condition) {
@return false;
}
}
@return true;
}
///
/// Slice `$list` between `$start` and `$end` indexes
///
/// @access private
///
/// @param {List} $list - List to slice
/// @param {Number} $start [1] - Start index
/// @param {Number} $end [length($list)] - End index
///
/// @return {List} Sliced list
///
@function slice($list, $start: 1, $end: length($list)) {
@if length($list) < 1 or $start > $end {
@return ();
}
$result: ();
@for $i from $start through $end {
$result: append($result, nth($list, $i));
}
@return $result;
}
////
/// String to number converter
/// @author Hugo Giraudel
/// @access private
////
///
/// Casts a string into a number
///
/// @param {String | Number} $value - Value to be parsed
///
/// @return {Number}
///
@function to-number($value) {
@if type-of($value) == 'number' {
@return $value;
} @else if type-of($value) != 'string' {
$_: im-log('Value for `to-number` should be a number or a string.');
}
$first-character: str-slice($value, 1, 1);
$result: 0;
$digits: 0;
$minus: ($first-character == '-');
$numbers: ('0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9);
// Remove +/- sign if present at first character
@if ($first-character == '+' or $first-character == '-') {
$value: str-slice($value, 2);
}
@for $i from 1 through str-length($value) {
$character: str-slice($value, $i, $i);
@if not (index(map-keys($numbers), $character) or $character == '.') {
@return to-length(if($minus, -$result, $result), str-slice($value, $i))
}
@if $character == '.' {
$digits: 1;
} @else if $digits == 0 {
$result: $result * 10 + map-get($numbers, $character);
} @else {
$digits: $digits * 10;
$result: $result + map-get($numbers, $character) / $digits;
}
}
@return if($minus, -$result, $result);
}
///
/// Add `$unit` to `$value`
///
/// @param {Number} $value - Value to add unit to
/// @param {String} $unit - String representation of the unit
///
/// @return {Number} - `$value` expressed in `$unit`
///
@function to-length($value, $unit) {
$units: ('px': 1px, 'cm': 1cm, 'mm': 1mm, '%': 1%, 'ch': 1ch, 'pc': 1pc, 'in': 1in, 'em': 1em, 'rem': 1rem, 'pt': 1pt, 'ex': 1ex, 'vw': 1vw, 'vh': 1vh, 'vmin': 1vmin, 'vmax': 1vmax);
@if not index(map-keys($units), $unit) {
$_: im-log('Invalid unit `#{$unit}`.');
}
@return $value * map-get($units, $unit);
}
///
/// This mixin aims at redefining the configuration just for the scope of
/// the call. It is helpful when having a component needing an extended
/// configuration such as custom breakpoints (referred to as tweakpoints)
/// for instance.
///
/// @author Hugo Giraudel
///
/// @param {Map} $tweakpoints [()] - Map of tweakpoints to be merged with `$breakpoints`
/// @param {Map} $tweak-media-expressions [()] - Map of tweaked media expressions to be merged with `$media-expression`
///
/// @example scss - Extend the global breakpoints with a tweakpoint
/// @include media-context(('custom': 678px)) {
/// .foo {
/// @include media('>phone', '<=custom') {
/// // ...
/// }
/// }
/// }
///
/// @example scss - Extend the global media expressions with a custom one
/// @include media-context($tweak-media-expressions: ('all': 'all')) {
/// .foo {
/// @include media('all', '>phone') {
/// // ...
/// }
/// }
/// }
///
/// @example scss - Extend both configuration maps
/// @include media-context(('custom': 678px), ('all': 'all')) {
/// .foo {
/// @include media('all', '>phone', '<=custom') {
/// // ...
/// }
/// }
/// }
///
@mixin media-context($tweakpoints: (), $tweak-media-expressions: ()) {
// Save global configuration
$global-breakpoints: $breakpoints;
$global-media-expressions: $media-expressions;
// Update global configuration
$breakpoints: map-merge($breakpoints, $tweakpoints) !global;
$media-expressions: map-merge($media-expressions, $tweak-media-expressions) !global;
@content;
// Restore global configuration
$breakpoints: $global-breakpoints !global;
$media-expressions: $global-media-expressions !global;
}
/// Test `$function` with `$spec` test suite
///
/// @author Hugo Giraudel
///
/// @param {String} $function - Name of function to test
/// @param {Map} $spec - Test suite to run `$function` against
///
/// @return {Map}
/// * `function`: `$function`
/// * `length`: the length of `$tests`
/// * `pass`: number of passing tests out of `length`
/// * `fail`: number of failing tests out of `length`
/// * `tests`: list of maps containing:
/// * `input`: test input (key from `$tests`)
/// * `expected`: expected result from `input`
/// * `actual`: actual result from `input`
/// * `pass`: whether the test passed or not
/// * `fail`: whether the test failed or not
///
/// @example scss - Testing of a `double` function
/// @function double($value) { @return $value * 2 }
///
/// $test: test('double', (
/// 1: 2,
/// 2: 4,
/// 3: 6,
/// 4: 8,
/// ));
///
/// @example scss - Result of `double` tests
/// (
/// 'function': 'double',
/// 'length': 4,
/// 'pass': 4,
/// 'fail': 0,
/// 'tests': ( ... ),
/// )
///
/// @example scss - `tests` value from result of `double` tests
/// (
/// (
/// 'input': 1,
/// 'expected': 2,
/// 'actual': 2,
/// 'pass': true,
/// 'fail': false,
/// ),
/// // ...
/// )
@function test($function, $spec) {
$passing-tests: 0;
$tests: ();
@each $arguments, $expected-result in $spec {
$actual-result: call($function, $arguments...);
$passed: $actual-result == $expected-result;
$tests: append($tests, (
'input': $arguments,
'expected': $expected-result,
'actual': $actual-result,
'pass': $passed,
'fail': not $passed,
), 'comma');
@if $passed {
$passing-tests: $passing-tests + 1;
}
}
@return (
'function': $function,
'length': length($tests),
'tests': $tests,
'pass': $passing-tests,
'fail': length($tests) - $passing-tests,
);
}
/// Mixin decorating the result of `test(..)` function to throw it as an error
///
/// @author Hugo Giraudel
///
/// @param {Map} $data - Return of `test(..)` function
///
/// @example scss - Printing the result of `double` function test
/// @include run(test('double', $tests-double));
///
/// @example scss - Result of `double` function test
/// Started tests for function `double`
/// ----------
/// Test 1 out of 4... ✔
/// Test 2 out of 4... ✔
/// Test 3 out of 4... ✔
/// Test 4 out of 4... ✔
/// ----------
/// Finished: 0 test(s) failing out of 4
@mixin run($data) {
$output: '';
$length: map-get($data, 'length');
$tests: map-get($data, 'tests');
@each $test in $tests {
$output: $output
+ 'Test #{index($tests, $test)} out of #{$length}... '
+ if(map-get($test, 'pass'), '✔', '✘\a Expected : `#{map-get($test, "expected")}`\a Actual : `#{map-get($test, "actual")}`') + '\a ';
}
$message: 'Started tests for function `#{map-get($data, "function")}`\a '
+ '----------\a '
+ $output + '\a '
+ '----------\a '
+ 'Finished: #{map-get($data, "fail")} test(s) failing out of #{$length}';
@if map-get($data, "fail") > 0 {
@error $message;
} @else {
@warn $message;
}
}
@import 'parse-expression';
@import 'to-number';
@import '../dist/include-media';
@import 'helpers/SassyTester';
$tests-parse-expression: map-merge($media-expressions, (
'>phone': '(min-width: 321px)',
'<phone': '(max-width: 319px)',
'>=phone': '(min-width: 320px)',
'≥phone': '(min-width: 320px)',
'<=phone': '(max-width: 320px)',
'≤phone': '(max-width: 320px)',
'>tablet': '(min-width: 769px)',
'<tablet': '(max-width: 767px)',
'>=tablet': '(min-width: 768px)',
'≥tablet': '(min-width: 768px)',
'<=tablet': '(max-width: 768px)',
'≤tablet': '(max-width: 768px)',
'>desktop': '(min-width: 1025px)',
'<desktop': '(max-width: 1023px)',
'>=desktop': '(min-width: 1024px)',
'≥desktop': '(min-width: 1024px)',
'<=desktop': '(max-width: 1024px)',
'≤desktop': '(max-width: 1024px)'
));
@include run(test('parse-expression', $tests-parse-expression));
@import '../dist/include-media';
@import 'helpers/SassyTester';
$tests-parse-media-query: (
(('a' 'b'),): 'a and b',
(('a', 'b'),): 'a, b',
(('a' 'b', 'c' 'd'),): 'a and b, c and d',
(('a', 'b', 'c' 'd'),): 'a, b, c and d',
);
@include run(test('parse-media-query', $tests-parse-media-query));
@import '../dist/include-media';
@import 'helpers/SassyTester';
$tests-resolve-feature-queries: (
(('screen' '>desktop'), ): (
('screen' '(min-width: 1025px)'),
),
(('screen', '>desktop'), ): (
('screen'), ('(min-width: 1025px)'),
),
(('screen' ('>phone', '<desktop')), ): (
('screen' '(min-width: 321px)'), ('screen' '(max-width: 1023px)'),
),
(('retina2x' '>phone'), ): (
('(-webkit-min-device-pixel-ratio: 2)' '(min-width: 321px)'),
('(min-resolution: 192dpi)' '(min-width: 321px)'),
('(min-resolution: 2dppx)' '(min-width: 321px)'),
),
(('screen' ('height>300px' 'landscape')), ): (
('screen' '(min-height: 301px)' '(orientation: landscape)'),
),
((('screen', 'portrait') ('height>300px', 'landscape')), ): (
('screen' '(min-height: 301px)'),
('(orientation: portrait)' '(min-height: 301px)'),
('screen' '(orientation: landscape)'),
('(orientation: portrait)' '(orientation: landscape)'),
),
);
@include run(test('resolve-feature-queries', $tests-resolve-feature-queries));
@import '../dist/include-media';
@import 'helpers/SassyTester';
$tests-to-number: ();
@for $i from 1 through 10 {
$tests-to-number: map-merge($tests-to-number, (
'#{$i}': $i,
'#{-$i}': -$i,
'#{$i / 10}': ($i / 10),
'#{$i / 100}': ($i / 100),
'#{$i}px': $i * 1px,
'#{$i}em': $i * 1em,
'#{$i}rem': $i * 1rem,
'#{-$i}px': -$i * 1px,
));
}
@include run(test('to-number', $tests-to-number));
@import "./utils/formatters";
@import "./utils/list-to-js";
@import "./utils/map-to-js";
//---
// [$propName] - optional param (otherwise $value is taken as first)
// if provided- function returns {'propName': sassToJs($value)}
//
// $value [map, list] - Converts provided value to JSON string
// $value [bool, nulls, number, string, color] - Converts provided value to a proper format for JSON
//
// Data types in Sass: http://sass-lang.com/documentation/file.SASS_REFERENCE.html#data_types
//
// Examples of usage:
//
// .breakpoints-data{
// font-family: sassToJs($mediaBreakPointsMap);
// }
//
// .zoom-control:before{
// content: sassToJs("maxZoomStep", $zoomStep);
// }
//
//---
@function sassToJs($propName: null, $value: null) {
@if ($value == null) {
$value: $propName; // emulates sassToJs($value)
} @else {
@return '{#{sassToJs($propName)}:#{sassToJs($value)}}'; // returns {"propName": sassToJs($value)}
}
//--- VARS ---
$valueType: type-of($value);
$jsonPropValue: '';
//--- CHECKING TYPE ---
// BOOL
@if ($valueType == 'bool') {
$jsonPropValue: $value;
// NULLS
} @else if ($valueType == 'null') {
$jsonPropValue: 'null';
// NUMBER
} @else if ($valueType == 'number') {
// '100px' has also type "number" but after multiplying to "0" gives "0px"
@if (unitless($value)) {
// e.g. 100
$jsonPropValue: $value;
} @else {
// e.g. 100px
$jsonPropValue: sassToJs_formatAsString($value);
}
// MAP
} @else if ($valueType == 'map') {
$jsonPropValue: sassToJs_Map($value);
// LIST
} @else if ($valueType == 'list') {
$jsonPropValue: sassToJs_List($value);
// STRING, COLOR
} @else {
$jsonPropValue: sassToJs_formatAsString($value);
}
//--- RETURN ---
@return $jsonPropValue;
}
//---
// Formats provided argument as a string
// Might be used for strings, colors and Sass numbers like "100px"
//---
@function sassToJs_formatAsString($value) {
@return '"#{$value}"';
}
//----------
// Converts Sass list to JSON string
//----------
@function sassToJs_List($list) {
//--- VARS ---
$list-map-length: length($list);
$iteration: 0;
$json: '['; // open square bracket
// add each property from Sass list
@each $item in $list {
// ITEM VALUE
$json: $json + sassToJs($item);
// Add comma if not the last item
$iteration: $iteration + 1;
@if ($iteration < $list-map-length) {
$json: $json + ',';
}
}
$json: $json + ']'; // close square bracket
//--- RETURN ---
@return $json;
}
//----------
// Converts Sass map to JSON string
//----------
@function sassToJs_Map($list-map) {
//--- VARS ---
$list-map-length: length($list-map);
$iteration: 0;
$json: '{'; // open curly brackets
// add each property from Sass map
@each $propName, $propValue in $list-map {
// PROP NAME
$json: $json + sassToJs_formatAsString($propName) + ':';
// PROP VALUE
$json: $json + sassToJs($propValue);
// Add comma if not the last item
$iteration: $iteration + 1;
@if ($iteration < $list-map-length) {
$json: $json + ',';
}
}
$json: $json + '}'; // close curly brackets
//--- RETURN ---
@return $json;
}
@import './functions/color-shade';
@import './functions/color-tint';
@import './functions/is-truthy';
@import './functions/list-concat';
@import './functions/list-contains';
@import './functions/list-expand-directions';
@import './functions/list-first';
@import './functions/list-insert-nth';
@import './functions/list-last';
@import './functions/list-last-index';
@import './functions/list-prepend';
@import './functions/list-purge';
@import './functions/list-remove';
@import './functions/list-remove-nth';
@import './functions/list-replace';
@import './functions/list-replace-nth';
@import './functions/list-slice';
@import './functions/map-assign';
@import './functions/map-deep-get';
@import './functions/selector-get-element-name';
@import './functions/str-replace';
@import './functions/str-split';
@import './functions/strip-unit';
@import './functions/to-string';
@import './functions/transition-props';
@import './functions';
@import './mixins';
@import './mixins/at-root';
@import './mixins/hide-text';
@import './mixins/has-focus';
@import './mixins/hide-visually';
@import './mixins/ios-native-scrolling';
@import './mixins/is-selectable';
@import './mixins/is-visible';
@import './mixins/last-row';
@import './mixins/overlay';
@import './mixins/placeholder';
@import './mixins/size';
@import './mixins/text-ellipsis';
@import './mixins/supports-hover';
@import './mixins/supports-touch';
/// Slightly darken a color by mixing it with black
/// @link http://sass-guidelin.es/#lightening-and-darkening-colors
/// @access public
/// @param {color} $color - Color to tint
/// @param {number} $percentage - Percentage of `$color` in returned color
/// @return {Color}
///
@function dp-color-shade($color, $percentage) {
@return mix(black, $color, $percentage);
}
/// Slightly lighten a color by mixing it with white
/// @link http://sass-guidelin.es/#lightening-and-darkening-colors
/// @access public
/// @param {color} $color - Color to tint
/// @param {number} $percentage - Percentage of `$color` in returned color
/// @return {Color}
///
@function dp-color-tint($color, $percentage) {
@return mix(white, $color, $percentage);
}
/// Check if a value has a truthy value
/// @param {*} $value - The input value
/// @return {boolean} - A flag indicating if the value is truthy
///
@function dp-is-truthy($value) {
@return if($value == null, false, $value and $value != null and $value != '' and $value != ());
}
@import './to-string';
/// Concatenate a list using a given separator
/// @link https://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/#miscellaneous
/// @param {List} $list - The list to be concatenated
/// @param {string} $glue - A separator string to be inserted between items
/// @return {string} - A string containing list items connected with a separator string
///
@function dp-list-concat($list, $glue: '') {
$result: null;
@if length($list) == 0 {
@return '';
}
@for $i from 1 through length($list) {
$e: nth($list, $i);
@if type-of($e) == list {
$result: unquote('#{$result}#{to-string($e, $glue, true)}');
} @else {
$result: if($i != length($list), unquote('#{$result}#{$e}#{$glue}'), unquote('#{$result}#{$e}'));
}
}
@return $result;
}
/// Checks if a given list contains a certain value
/// @param {List} $list - The haystack
/// @param {*} $val - The needle
///
@function dp-list-contains($list, $val) {
@if index($list, $val) {
@return true;
}
@return false;
}
// stylelint-disable scss/no-duplicate-dollar-variables
/// Expand a given list to 4 items, similar to css padding shorthand expressions.
/// Inspired by expansions used in css shortand expressions (eg. margin or padding).
/// @param {List} $list - List containing zero to four items
/// @return {List} - Updated list expanded to always four entries
///
@function dp-list-expand-directions($list) {
$result: ();
$len-list: length($list);
@if ($len-list == 0) {
$result: (null, null, null, null);
} @else if ($len-list == 1) {
$result: (nth($list, 1), nth($list, 1), nth($list, 1), nth($list, 1));
} @else if ($len-list == 2) {
$result: (nth($list, 1), nth($list, 2), nth($list, 1), nth($list, 2));
} @else if ($len-list == 3) {
$result: (nth($list, 1), nth($list, 2), nth($list, 3), nth($list, 2));
} @else {
$result: (nth($list, 1), nth($list, 2), nth($list, 3), nth($list, 4));
}
@return $result;
}
/// Return the first item of a list
/// @link https://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/#selecting-values-from-list
/// @param {List} $list - The input list
/// @return {*} - The list's first item
///
@function dp-list-first($list) {
@return nth($list, 1);
}
// stylelint-disable scss/no-duplicate-dollar-variables
/// Add a value to an arbitrary position
/// @link https://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/#adding-values-to-a-list
/// @param {List} $list - The input list
/// @param {number} $index - The value's target list position
/// @param {*} $value - The term to be added
/// @return {*} - The output list
///
@function dp-list-insert-nth($list, $index, $value) {
$result: null;
@if type-of($index) != number {
@warn '$index: #{quote($index)} is not a number for `insert-nth`.';
} @else if $index < 1 {
@warn 'List index 0 must be a non-zero integer for `insert-nth`';
} @else if $index > length($list) {
@warn 'List index is #{$index} but list is only #{length($list)} item long for `insert-nth`';
} @else {
$result: ();
@for $i from 1 through length($list) {
@if $i == $index {
$result: append($result, $value);
}
$result: append($result, nth($list, $i));
}
}
@return $result;
}
/// Return the last occurence of an item within a list
/// @link https://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/#selecting-values-from-list
/// @param {List} $list - The input list
/// @param {*} $value - The search term
/// @return {number} - The index of the search term's last occurrence within the list
///
@function dp-list-last-index($list, $value) {
$result: null;
@for $i from 1 through length($list) {
@if nth($list, $i) == $value {
$result: $i;
}
}
@return $result;
}
/// Return the last item of a list
/// @link https://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/#selecting-values-from-list
/// @param {List} $list - The input list
/// @return {*} - The list's last item
///
@function dp-list-last($list) {
@return nth($list, length($list));
}
/// Add a value to the front of a list
/// @link https://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/#adding-values-to-a-list
/// @param {List} $list - The input list
/// @param {*} $value - The term to be prepended
/// @return {*} - The output list
///
@function dp-list-prepend($list, $value) {
@return join($value, $list);
}
@import './is-truthy';
/// Removes all non-true values from a list
/// @link http://hugogiraudel.com/2013/10/09/advanced-sass-list-functions-again/
/// @param {List} $list - A list containing possibly falsy values
/// @return {List} - The trimmed list
///
@function dp-list-purge($list) {
$result: ();
@each $item in $list {
@if dp-is-truthy($item) {
$result: append($result, $item);
}
}
@return $result;
}
@import './list-replace-nth';
/// Remove a value at a certain index from a list
/// @link https://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/#removing-values-from-list
/// @param {List} $list - The input list
/// @param {number} $index - The target position to be removed
/// @return {*} - The list's first item
///
@function dp-list-remove-nth($list, $index) {
@return dp-list-replace-nth($list, $index);
}
@import './list-replace';
/// Remove a value from a list
/// @link https://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/#removing-values-from-list
/// @param {List} $list - The input list
/// @param {*} $value - The term to be removed
/// @return {*} - The list's first item
///
@function dp-list-remove($list, $value) {
@return dp-list-replace($list, $value);
}
// stylelint-disable scss/no-duplicate-dollar-variables
@import './is-truthy';
/// Replace a value within a list by index
/// @link https://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/#replacing-values-from-list
/// @param {List} $list - The input list
/// @param {number} $index - The value's target list position
/// @param {*} $value - The term to be inserted
/// @return {*} - The output list
///
@function dp-list-replace-nth($list, $index, $value: null) {
$result: null;
@if type-of($index) != number {
@warn '$index: #{quote($index)} is not a number for `replace-nth`';
} @else if $index == 0 {
@warn 'ist index 0 must be a non-zero integer for `replace-nth`.';
} @else if abs($index) > length($list) {
@warn 'ist index is #{$index} but list is only #{length($list)} item long for `replace-nth`.';
} @else {
$result: ();
$index: if($index < 0, length($list) + $index + 1, $index);
@for $i from 1 through length($list) {
@if $i == $index {
@if dp-is-truthy($value) {
$result: append($result, $value);
}
} @else {
$result: append($result, nth($list, $i));
}
}
}
@return $result;
}
@import './is-truthy';
/// Replace a value with another one in a list
/// @link https://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/#replacing-values-from-list
/// @param {List} $list - The input list
/// @param {*} $old-value - The term that is being replaced
/// @param {*} $value - The term to replace it with
/// @return {*} - The output list
///
@function dp-list-replace($list, $old-value, $value: null) {
$result: ();
@for $i from 1 through length($list) {
@if nth($list, $i) == $old-value {
@if dp-is-truthy($value) {
$result: append($result, $value);
}
} @else {
$result: append($result, nth($list, $i));
}
}
@return $result;
}
// stylelint-disable scss/no-duplicate-dollar-variables
/// Return a number of values from a list
/// @link https://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/#miscellaneous
/// @param {List} $list - The input list
/// @param {number} $start - The slice range's start index
/// @param {number} $end - The slice range's end index
/// @return {List} - A list of items
///
@function dp-list-slice($list, $start: 1, $end: length($list)) {
$result: null;
@if type-of($start) != number or type-of($end) != number {
@warn 'Either $start or $end are not a number for `slice`';
} @else if $start > $end {
@warn 'The start index has to be lesser than or equals to the end index for `slice`';
} @else if $start < 1 or $end < 1 {
@warn 'List indexes must be non-zero integers for `slice`';
} @else if $start > length($list) {
@warn 'List index is #{$start} but list is only #{length($list)} item long for `slice`';
} @else if $end > length($list) {
@warn 'List index is #{$end} but list is only #{length($list)} item long for `slice`';
} @else {
$result: ();
@for $i from $start through $end {
$result: append($result, nth($list, $i));
}
}
@return $result;
}
/// Takes multiple maps and merges them into one
/// @link http://stackoverflow.com/questions/27740063/merge-multiple-sass-maps
/// @param {...Map} $maps - Any number of maps
/// @return {Map} - The resulting map
///
@function dp-map-assign($maps...) {
$result: ();
@each $map in $maps {
$result: map-merge($result, $map);
}
@return $result;
}
/// Returns a value from a nested list (without failsafe if one of the keys does not exist)
/// @link https://css-tricks.com/snippets/sass/deep-getset-maps/
/// @param {Map} $map - Map
/// @param {...string} $keys - Key chain
/// @return {*} - Desired value
///
@function dp-map-deep-get($map, $keys...) {
@each $key in $keys {
$map: map-get($map, $key);
}
@return $map;
}
@import './str-split';
/// Remove classes, pseudo-classes and attributes from a selector part
/// @param {string} $selector - The selector part to be cleaned
/// @return {string}
///
@function dp-selector-get-element-name($selector) {
$result: $selector;
$markers: (' ', '.', ':', '[');
@each $marker in $markers {
$result: nth(dp-str-split($result, $marker), 1);
}
@return $result;
}
/// Replace `$search` with `$replace` in `$string`
/// @link https://css-tricks.com/snippets/sass/str-replace-function/
/// @param {string} $string - Initial string
/// @param {string} $search - Substring to replace
/// @param {string} $replace ('') - New value
/// @return {string} - Updated string
///
@function dp-str-replace($string, $search, $replace: '') {
$index: str-index($string, $search);
@if $index {
@return str-slice($string, 1, $index - 1) + $replace + dp-str-replace(str-slice($string, $index + str-length($search)), $search, $replace);
}
@return $string;
}
/// Split `$string` into several parts using `$delimiter`.
/// @param {string} $string - String to split
/// @param {string} $delimiter ('') - String to use as a delimiter to split `$string`
/// @return {List}
///
@function dp-str-split($string, $delimiter: '') {
$result: ();
$length: str-length($string);
@if str-length($delimiter) == 0 {
@for $i from 1 through $length {
$result: append($result, str-slice($string, $i, $i));
}
@return $result;
}
$running: true;
$remaining: $string;
@while $running {
$index: str-index($remaining, $delimiter);
@if $index {
$slice: str-slice($remaining, 1, $index - 1);
@if (str-length($slice) > 0) {
$result: append($result, $slice);
}
$remaining: str-slice($remaining, $index + str-length($delimiter));
} @else {
$running: false;
}
}
@return append($result, $remaining);
}
/// Remove the unit of a length
/// @param {number} $number - Number to remove unit from
/// @return {number} - Unitless number
///
@function dp-strip-unit($number) {
@if type-of($number) == 'number' and not unitless($number) {
@return $number / ($number * 0 + 1);
}
@return $number;
}
/// Cast an arbitrary value to a string
/// @link https://hugogiraudel.com/2014/01/27/casting-types-in-sass/#string
/// @param {*} $value - The original value
/// @return {string} - The cast string
///
@function dp-to-string($value) {
@return inspect($value);
}
/// Batch transition all properties with a single transition definition
/// @param {string} $trs-defintion - The transition definition to be applied
/// @param {...Map} $props - Any number of properties to be transitioned
/// @return {List} - A list to be used as a value for a transition property
///
@function dp-transition-props($trs-definition, $props...) {
$result: ();
@each $prop in $props {
$result: append($result, ($prop $trs-definition), 'comma');
}
@return $result;
}
@import '../functions/selector-get-element-name';
/// Use `@at-root` while adding classes (or attributes) `$classes` to an element node `$append-target`.
/// If the element was not found a standard `@at-root` is used.
/// @param {string} $append-target - $append-target string to append the classes (or attributes) to
/// @param {string} $appended-selector - A single element selector containing classes and/or attributes to be appended
///
@mixin dp-at-root($append-target, $appended-selector: '') {
$is-element-found: false;
$combined-selectors: '';
$selectors: nth(&, 1);
@if ($appended-selector) {
@each $selector in $selectors {
$pure-element-selector: dp-selector-get-element-name($selector);
@if (($pure-element-selector == $append-target) and (not $is-element-found)) {
$is-element-found: true;
$combined-selectors: '#{$combined-selectors} #{$selector}#{$appended-selector}';
} @else {
$combined-selectors: '#{$combined-selectors} #{$selector}';
}
}
}
@if ($is-element-found) {
@at-root #{$combined-selectors} {
@content;
}
} @else {
@at-root #{$append-target}#{$appended-selector} & {
@content;
}
}
}
@import './supports-hover';
@import './supports-touch';
/// Styles touch/hover states according to browser capabilities
/// @param {boolean} $toggle - State toggle; Available values: `true`, `false` or `default` (for both)
///
@mixin dp-has-focus() {
@include dp-supports-hover {
&:hover, &:focus {
@content;
}
}
@include dp-supports-touch {
&:active {
@content;
}
}
}
/// Visually hide text but leave it accessible for screen readers and search engines
///
@mixin dp-hide-text {
font: 0/0 a; // stylelint-disable font-family-no-missing-generic-family-keyword
text-shadow: none;
color: transparent;
}
/// Visually hide content but leave it accessible for screen readers and search engines
///
@mixin dp-hide-visually {
position: absolute;
overflow: hidden;
max-width: 1px;
max-height: 1px;
padding: 0;
border: 0;
margin: -1px;
clip: rect(0 0 0 0);
}
/// Add ios native overflow scrolling to an element
/// @param {string} $orientation - The scroll direction
///
@mixin dp-ios-native-scrolling($orientation: y) {
-webkit-overflow-scrolling: touch;
@if (not index((x, y), $orientation)) {
@error '`#{$orientation}` must be either `x` or `y`';
}
@if ($orientation == y) {
overflow-x: hidden;
overflow-y: scroll;
}
@if ($orientation == x) {
overflow-x: scroll;
overflow-y: hidden;
}
}
/// Controls the selection style of the current element and all its children
/// @param {boolean} $toggle - State toggle
///
@mixin dp-is-selectable($toggle) {
$c--label-selection: #accef7 !default; // provide a fallback value
@if $toggle {
& {
-webkit-touch-callout: text;
user-select: text;
&::selection, ::selection {
background-color: $c--label-selection;
}
}
} @else {
& {
-webkit-touch-callout: none;
user-select: none;
&::selection, ::selection {
background-color: transparent;
}
}
}
}
// stylelint-disable scss/no-duplicate-dollar-variables
/// Animates an element in or out, including setting their visibility correctly
/// The basic opacity transition can be extended with other properties or full transition descriptors
/// @param {boolean} $toggle - State toggle
/// @param {number} $speed - The targeted speed after which the element is hidden
/// @param {...string|...List} $transition-params - One or more strings or lists containing additional transition params
/// @example scss - Simple usage: input
/// .my-elem.my-elem__is-hidden { @include dp-is-visible(false, 300ms); }
/// .my-elem.my-elem__is-shown { @include dp-is-visible(true, 300ms); }
/// @output css - Simple usage: output
/// .my-elem.my-elem__is-hidden {
/// transition: visibility 0ms linear 300ms, opacity 300ms ease 0ms;
/// visibility: hidden;
/// opacity: 0;
/// }
/// .my-elem.my-elem__is-shown {
/// transition: visibility 0ms linear 0ms, opacity 300ms ease 0ms;
/// visibility: inherit;
/// opacity: 1;
/// }
///
@mixin dp-is-visible($toggle, $speed, $transition-params...) {
$transitions: ();
$has-params: length($transition-params) > 0;
@if type-of($speed) != 'number' {
@error 'Provided transition speed `#{$speed}` is not of type number!';
}
@if $has-params {
@for $i from 1 through length($transition-params) {
$param: nth($transition-params, $i);
@if type-of($param) == 'list' { // If $param contains a space (a.k.a. a whole string)
$transitions: append($transitions, unquote('#{$param}'), comma);
} @else {
$transitions: append($transitions, unquote('#{$param} #{$speed} ease 0ms'), comma);
}
}
} @else {
$transitions: append($transitions, unquote('opacity #{$speed} ease 0ms'), comma);
}
@if $toggle {
transition: join(unquote('visibility 0ms linear 0ms'), $transitions, comma);
visibility: inherit;
@if not $has-params {
opacity: 1;
}
} @else {
transition: join(unquote('visibility 0ms linear #{$speed}'), $transitions, comma);
visibility: hidden;
@if not $has-params {
opacity: 0;
}
}
}
/// Targets the last row in a grid of constant column count
/// @link http://keithclark.co.uk/articles/targeting-first-and-last-rows-in-css-grid-layouts/
/// @param {number} $num-cols - The number of columns within the context
///
@mixin dp-last-row($num-cols) {
$selector: nth(nth(&, 1), length(nth(&, 1)));
&:nth-child(#{$num-cols}n + 1):nth-last-child(-n + #{$num-cols}),
&:nth-child(#{$num-cols}n + 1):nth-last-child(-n + #{$num-cols}) ~ #{$selector} {
@content;
}
}
/// Add styles for a child to overlay its parent
/// @param {string} $pos - A declaration value for the `position` property
/// @param {string} $root-y - A vertical spacial property keyword
/// @param {string} $root-x - A horizontal spacial property keyword
///
@mixin dp-overlay($pos: absolute, $root-y: top, $root-x: left) {
position: $pos;
#{$root-y}: 0;
#{$root-x}: 0;
width: 100%;
height: 100%;
}
// stylelint-disable selector-no-vendor-prefix
/// Add styles to the placeholder element
///
@mixin dp-placeholder() {
&::-moz-placeholder { @content; }
&:-ms-input-placeholder { @content; }
&::-webkit-input-placeholder { @content; }
&::placeholder { @content; }
}
@mixin dp-size($sz) {
width: $sz;
height: $sz;
}
// https://docs.w3cub.com/css/@media/any-hover/
@mixin dp-supports-hover() {
/* one or more available input mechanism(s) can hover over elements with ease */
// The `-ms-high-contrast` directives are needed to target Internet Explorer 10 and 11
// @link https://stackoverflow.com/q/28417056/1602864
// TODO: Remove the `-ms-high-contrast` directives when IE will no longer be supported
@media (any-hover: hover), (-ms-high-contrast: active), (-ms-high-contrast: none) {
@content;
}
}
@mixin dp-supports-touch() {
/* one or more available input mechanism(s) cannot hover or there are no pointing input mechanisms */
@media (any-hover: none) {
@content;
}
}
@mixin dp-text-ellipsis() {
white-space: nowrap;
overflow: hidden;
max-width: 100%;
text-overflow: ellipsis;
}
@import './index';
@import './unprefixed/index';
.test {
$test-list: (foo, bar, baz);
$test-map: (foo: bar);
$test-color: #0000ff;
// Prefixed mixins
@include dp-has-focus;
@include dp-at-root('.test');
@include dp-hide-text;
@include dp-hide-visually;
@include dp-ios-native-scrolling;
@include dp-is-selectable(true);
@include dp-is-visible(true, 200);
@include dp-last-row(2);
@include dp-overlay;
@include dp-placeholder;
@include dp-size(200);
@include dp-text-ellipsis;
// Unprefixed mixins
@include has-focus;
@include at-root('.test');
@include hide-text;
@include hide-visually;
@include ios-native-scrolling;
@include is-selectable(true);
@include is-visible(true, 200);
@include last-row(2);
@include overlay;
@include placeholder;
@include size(200);
@include text-ellipsis;
@include supports-hover;
@include supports-touch;
// Prefixed functions
$test-prefixed: (
't-1': dp-color-shade($test-color, 10),
't-2': dp-color-tint($test-color, 10),
't-3': dp-is-truthy('foo'),
't-4': dp-list-concat($test-list, ','),
't-5': dp-list-contains($test-list, foo),
't-6': dp-list-expand-directions($test-list),
't-7': dp-list-first($test-list),
't-8': dp-list-insert-nth($test-list, 2, qux),
't-9': dp-list-last($test-list),
't-10': dp-list-last-index($test-list, bar),
't-11': dp-list-prepend($test-list, qux),
't-12': dp-list-purge($test-list),
't-13': dp-list-remove($test-list, foo),
't-14': dp-list-remove-nth($test-list, 1),
't-15': dp-list-replace($test-list, foo, qux),
't-16': dp-list-replace-nth($test-list, 1, qux),
't-17': dp-list-slice($test-list, 2, 3),
't-18': dp-map-assign($test-map),
't-19': dp-map-deep-get($test-map, 'color'),
't-20': dp-selector-get-element-name('.test'),
't-21': dp-str-replace('test', 'st', 'oo'),
't-22': dp-str-split('test'),
't-23': dp-strip-unit(3),
't-24': dp-to-string(foo),
't-25': dp-transition-props('all 300ms linear'),
't-26': dp-supports-hover(),
't-27': dp-supports-touch(),
);
// Unprefixed functions
$test-unprefixed: (
't-1': color-shade($test-color, 10),
't-2': color-tint($test-color, 10),
't-3': is-truthy('foo'),
't-4': list-concat($test-list, ','),
't-5': list-contains($test-list, foo),
't-6': list-expand-directions($test-list),
't-7': list-first($test-list),
't-8': list-insert-nth($test-list, 2, qux),
't-9': list-last($test-list),
't-10': list-last-index($test-list, bar),
't-11': list-prepend($test-list, qux),
't-12': list-purge($test-list),
't-13': list-remove($test-list, foo),
't-14': list-remove-nth($test-list, 1),
't-15': list-replace($test-list, foo, qux),
't-16': list-replace-nth($test-list, 1, qux),
't-17': list-slice($test-list, 2, 3),
't-18': map-assign($test-map),
't-19': map-deep-get($test-map, 'color'),
't-20': selector-get-element-name('.test'),
't-21': str-replace('test', 'st', 'oo'),
't-22': str-split('test'),
't-23': strip-unit(3),
't-24': to-string(foo),
't-25': transition-props('all 300ms linear'),
't-26': supports-hover(),
't-27': supports-touch(),
);
$test-fixtures: (
't-1': #0000e6,
't-2': #1a1aff,
't-3': true,
't-4': 'foo,bar,baz',
't-5': true,
't-6': (foo, bar, baz, bar),
't-7': foo,
't-8': foo qux bar baz,
't-9': baz,
't-10': 2,
't-11': (qux, foo, bar, baz),
't-12': foo bar baz,
't-13': bar baz,
't-14': bar baz,
't-15': qux bar baz,
't-16': qux bar baz,
't-17': bar baz,
't-18': (foo: bar),
't-19': null,
't-20': 'test',
't-21': 'teoo',
't-22': 't' 'e' 's' 't',
't-23': 3,
't-24': 'foo',
't-25': (),
);
@for $i from 1 through length($test-fixtures) {
$fixture: map-get($test-fixtures, 't-#{$i}');
$prefixed: map-get($test-prefixed, 't-#{$i}');
$unprefixed: map-get($test-unprefixed, 't-#{$i}');
@if ($prefixed != $unprefixed) {
@error 'Comparison test ##{$i} failing! - Prefixed: #{$prefixed} ~vs~ Unprefixed: #{$unprefixed}';
}
@if ($prefixed != $fixture) {
@error 'Comparison test ##{$i} failing! - Fixture: #{$fixture} ~vs~ Prefixed: #{$prefixed}';
}
}
}
@import '../functions';
@function color-shade($args...) {
@return dp-color-shade($args...);
}
@function color-tint($args...) {
@return dp-color-tint($args...);
}
@function is-truthy($args...) {
@return dp-is-truthy($args...);
}
@function list-concat($args...) {
@return dp-list-concat($args...);
}
@function list-contains($args...) {
@return dp-list-contains($args...);
}
@function list-expand-directions($args...) {
@return dp-list-expand-directions($args...);
}
@function list-first($args...) {
@return dp-list-first($args...);
}
@function list-insert-nth($args...) {
@return dp-list-insert-nth($args...);
}
@function list-last($args...) {
@return dp-list-last($args...);
}
@function list-last-index($args...) {
@return dp-list-last-index($args...);
}
@function list-prepend($args...) {
@return dp-list-prepend($args...);
}
@function list-purge($args...) {
@return dp-list-purge($args...);
}
@function list-remove($args...) {
@return dp-list-remove($args...);
}
@function list-remove-nth($args...) {
@return dp-list-remove-nth($args...);
}
@function list-replace($args...) {
@return dp-list-replace($args...);
}
@function list-replace-nth($args...) {
@return dp-list-replace-nth($args...);
}
@function list-slice($args...) {
@return dp-list-slice($args...);
}
@function map-assign($args...) {
@return dp-map-assign($args...);
}
@function map-deep-get($args...) {
@return dp-map-deep-get($args...);
}
@function selector-get-element-name($args...) {
@return dp-selector-get-element-name($args...);
}
@function str-replace($args...) {
@return dp-str-replace($args...);
}
@function str-split($args...) {
@return dp-str-split($args...);
}
@function strip-unit($args...) {
@return dp-strip-unit($args...);
}
@function to-string($args...) {
@return dp-to-string($args...);
}
@function transition-props($args...) {
@return dp-transition-props($args...);
}
@import './functions';
@import './mixins';
@import '../mixins';
@mixin at-root($args...) {
@include dp-at-root($args...) {
@content;
}
}
@mixin has-focus($args...) {
@include dp-has-focus($args...) {
@content;
}
}
@mixin hide-text($args...) {
@include dp-hide-text($args...);
}
@mixin hide-visually($args...) {
@include dp-hide-visually($args...);
}
@mixin ios-native-scrolling($args...) {
@include dp-ios-native-scrolling($args...);
}
@mixin is-selectable($args...) {
@include dp-is-selectable($args...);
}
@mixin is-visible($args...) {
@include dp-is-visible($args...);
}
@mixin last-row($args...) {
@include dp-last-row($args...) {
@content;
}
}
@mixin overlay($args...) {
@include dp-overlay($args...);
}
@mixin placeholder($args...) {
@include dp-placeholder($args...) {
@content;
}
}
@mixin size($args...) {
@include dp-size($args...);
}
@mixin text-ellipsis($args...) {
@include dp-text-ellipsis($args...);
}
@mixin supports-hover($args...) {
@include dp-supports-hover($args...);
}
@mixin supports-touch($args...) {
@include dp-supports-touch($args...);
}
@charset "UTF-8";
////
/// `include-media-or` - An `include-media` fork, enabling OR conjunctions and nested queries
/// Author: Rouven Bühlmann (@nirazul)
///
/// This project is licensed under the terms of the MIT license
////
////
/// include-media library public configuration
/// @author Eduardo Boucas
/// @access public
////
///
/// Creates a list of global breakpoints
///
/// @example scss - Creates a single breakpoint with the label `phone`
/// $breakpoints: ('phone': 320px);
///
$breakpoints: (
'phone': 320px,
'tablet': 768px,
'desktop': 1024px
) !default;
///
/// Creates a list of static expressions or media types
///
/// @example scss - Creates a single media type (screen)
/// $media-expressions: ('screen': 'screen');
///
/// @example scss - Creates a static expression with logical disjunction (OR operator)
/// $media-expressions: (
/// 'retina2x': '(-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi)'
/// );
///
$media-expressions: (
'screen': 'screen',
'print': 'print',
'handheld': 'handheld',
'landscape': '(orientation: landscape)',
'portrait': '(orientation: portrait)',
'retina2x': ('(-webkit-min-device-pixel-ratio: 2)', '(min-resolution: 192dpi)', '(min-resolution: 2dppx)'),
'retina3x': ('(-webkit-min-device-pixel-ratio: 3)', '(min-resolution: 350dpi)', '(min-resolution: 3dppx)')
) !default;
///
/// Defines a number to be added or subtracted from each unit when declaring breakpoints with exclusive intervals
///
/// @example scss - Interval for pixels is defined as `1` by default
/// @include media('>128px') {}
///
/// /* Generates: */
/// @media (min-width: 129px) {}
///
/// @example scss - Interval for ems is defined as `0.01` by default
/// @include media('>20em') {}
///
/// /* Generates: */
/// @media (min-width: 20.01em) {}
///
/// @example scss - Interval for rems is defined as `0.1` by default, to be used with `font-size: 62.5%;`
/// @include media('>2.0rem') {}
///
/// /* Generates: */
/// @media (min-width: 2.1rem) {}
///
$unit-intervals: (
'px': 1,
'em': 0.01,
'rem': 0.1,
'': 0
) !default;
///
/// Defines whether support for media queries is available, useful for creating separate stylesheets
/// for browsers that don't support media queries.
///
/// @example scss - Disables support for media queries
/// $im-media-support: false;
/// @include media('>=tablet') {
/// .foo {
/// color: tomato;
/// }
/// }
///
/// /* Generates: */
/// .foo {
/// color: tomato;
/// }
///
$im-media-support: true !default;
///
/// Selects which breakpoint to emulate when support for media queries is disabled. Media queries that start at or
/// intercept the breakpoint will be displayed, any others will be ignored.
///
/// @example scss - This media query will show because it intercepts the static breakpoint
/// $im-media-support: false;
/// $im-no-media-breakpoint: 'desktop';
/// @include media('>=tablet') {
/// .foo {
/// color: tomato;
/// }
/// }
///
/// /* Generates: */
/// .foo {
/// color: tomato;
/// }
///
/// @example scss - This media query will NOT show because it does not intercept the desktop breakpoint
/// $im-media-support: false;
/// $im-no-media-breakpoint: 'tablet';
/// @include media('>=desktop') {
/// .foo {
/// color: tomato;
/// }
/// }
///
/// /* No output */
///
$im-no-media-breakpoint: 'desktop' !default;
///
/// Selects which media expressions are allowed in an expression for it to be used when media queries
/// are not supported.
///
/// @example scss - This media query will show because it intercepts the static breakpoint and contains only accepted media expressions
/// $im-media-support: false;
/// $im-no-media-breakpoint: 'desktop';
/// $im-no-media-expressions: ('screen');
/// @include media('>=tablet', 'screen') {
/// .foo {
/// color: tomato;
/// }
/// }
///
/// /* Generates: */
/// .foo {
/// color: tomato;
/// }
///
/// @example scss - This media query will NOT show because it intercepts the static breakpoint but contains a media expression that is not accepted
/// $im-media-support: false;
/// $im-no-media-breakpoint: 'desktop';
/// $im-no-media-expressions: ('screen');
/// @include media('>=tablet', 'retina2x') {
/// .foo {
/// color: tomato;
/// }
/// }
///
/// /* No output */
///
$im-no-media-expressions: ('screen', 'portrait', 'landscape') !default;
////
/// Parsing engine
/// @author Hugo Giraudel
/// @access private
////
///
/// Get operator of an expression
///
/// @param {String} $expression - Expression to extract operator from
///
/// @return {String} - Any of `>=`, `>`, `<=`, `<`, `≥`, `≤`
///
@function get-expression-operator($expression) {
@each $operator in ('>=', '>', '<=', '<', '≥', '≤') {
@if str-index($expression, $operator) {
@return $operator;
}
}
// It is not possible to include a mixin inside a function, so we have to
// rely on the `im-log(..)` function rather than the `log(..)` mixin. Because
// functions cannot be called anywhere in Sass, we need to hack the call in
// a dummy variable, such as `$_`. If anybody ever raise a scoping issue with
// Sass 3.3, change this line in `@if im-log(..) {}` instead.
$_: im-log('No operator found in `#{$expression}`.');
}
///
/// Get dimension of an expression, based on a found operator
///
/// @param {String} $expression - Expression to extract dimension from
/// @param {String} $operator - Operator from `$expression`
///
/// @return {String} - `width` or `height` (or potentially anything else)
///
@function get-expression-dimension($expression, $operator) {
$operator-index: str-index($expression, $operator);
$parsed-dimension: str-slice($expression, 0, $operator-index - 1);
$dimension: 'width';
@if str-length($parsed-dimension) > 0 {
$dimension: $parsed-dimension;
}
@return $dimension;
}
///
/// Get dimension prefix based on an operator
///
/// @param {String} $operator - Operator
///
/// @return {String} - `min` or `max`
///
@function get-expression-prefix($operator) {
@return if(index(('<', '<=', '≤'), $operator), 'max', 'min');
}
///
/// Get value of an expression, based on a found operator
///
/// @param {String} $expression - Expression to extract value from
/// @param {String} $operator - Operator from `$expression`
///
/// @return {Number} - A numeric value
///
@function get-expression-value($expression, $operator) {
$operator-index: str-index($expression, $operator);
$value: str-slice($expression, $operator-index + str-length($operator));
@if map-has-key($breakpoints, $value) {
$value: map-get($breakpoints, $value);
} @else {
$value: to-number($value);
}
$interval: map-get($unit-intervals, unit($value));
@if not $interval {
// It is not possible to include a mixin inside a function, so we have to
// rely on the `im-log(..)` function rather than the `log(..)` mixin. Because
// functions cannot be called anywhere in Sass, we need to hack the call in
// a dummy variable, such as `$_`. If anybody ever raise a scoping issue with
// Sass 3.3, change this line in `@if im-log(..) {}` instead.
$_: im-log('Unknown unit `#{unit($value)}`.');
}
@if $operator == '>' {
$value: $value + $interval;
} @else if $operator == '<' {
$value: $value - $interval;
}
@return $value;
}
///
/// Parse an expression to return a valid media-query expression
///
/// @param {String} $expression - Expression to parse
///
/// @return {String} - Valid media query
///
@function parse-expression($expression) {
// If it is part of $media-expressions, it has no operator
// then there is no need to go any further, just return the value
@if map-has-key($media-expressions, $expression) {
@return map-get($media-expressions, $expression);
}
$operator: get-expression-operator($expression);
$dimension: get-expression-dimension($expression, $operator);
$prefix: get-expression-prefix($operator);
$value: get-expression-value($expression, $operator);
@return '(#{$prefix}-#{$dimension}: #{$value})';
}
///
/// Get a shallow list of resolved feature queries
///
/// @param {String} $queries - A nested list structure
/// @param {Boolean} $do-parse - Try to parse expressions
///
/// @return {List} - A one item deep nested list like `(a b, c d e)`
///
@function resolve-feature-queries($queries, $do-parse: true) {
$separator: list-separator($queries);
$result: if($separator == 'space', ((), ), ());
@each $query in $queries {
$chainable-queries: ();
@if (type-of($query) == 'list') {
// List item is a list itself
$chainable-queries: resolve-feature-queries($query);
} @else {
// List item is a string
$parsed-query: if($do-parse, parse-expression($query), $query);
@if (type-of($parsed-query) == 'list') {
// Parsed expression is a list
$chainable-queries: resolve-feature-queries($parsed-query, false);
} @else {
// Parsed expression is a string
$chainable-queries: ($parsed-query);
}
}
$result: append-feature-query($result, $chainable-queries, $separator);
}
@return $result;
}
///
/// Combine two query lists as a logical AND / OR operation
///
/// @param {List} $base-queries - The host list
/// @param {List} $append-queries - The list that is being appended
/// @param {String} $separator - Either `space` or `comma`
///
/// @return {List} - A one item deep nested list like `(a b, c d e)`
///
@function append-feature-query($base-queries, $append-queries, $separator) {
$result: if($separator == 'space', (), $base-queries);
@each $append-query in $append-queries {
@if ($separator == 'space') {
// Logical AND
@each $base-query in $base-queries {
$updated-query: join($base-query, $append-query, $separator);
$result: append($result, $updated-query, 'comma');
}
} @else {
// Logical OR
$result: append($result, $append-query, 'comma');
}
}
@return $result;
}
///
/// Parse a list of resolved expressions to return a valid media-query
///
/// @param {List} $queries - A one item deep nested list like `(a b, c d e)`
///
/// @return {String} - A valid media-query string
///
@function parse-media-query($queries) {
$result: null;
$flat-queries: ();
$separator: list-separator($queries);
$conjunction: if($separator == 'space', ' and ', ', ');
@if (type-of($queries) == 'string') {
@return $queries;
}
@each $query in $queries {
@if (type-of($query) == 'list') {
$flat-queries: append($flat-queries, parse-media-query($query));
} @else {
$flat-queries: append($flat-queries, $query);
}
}
@for $i from 1 through length($flat-queries) {
$e: nth($flat-queries, $i);
$result: unquote('#{$result}#{$e}#{if($i != length($flat-queries), $conjunction, '')}');
}
@return $result;
}
////
/// Cross-engine logging engine
/// @author Hugo Giraudel
/// @access private
////
///
/// Log a message either with `@error` if supported
/// else with `@warn`, using `feature-exists('at-error')`
/// to detect support.
///
/// @param {String} $message - Message to log
///
@function im-log($message) {
@if feature-exists('at-error') {
@error $message;
} @else {
@warn $message;
$_: noop();
}
@return $message;
}
///
/// Wrapper mixin for the log function so it can be used with a more friendly
/// API than `@if im-log('..') {}` or `$_: im-log('..')`. Basically, use the function
/// within functions because it is not possible to include a mixin in a function
/// and use the mixin everywhere else because it's much more elegant.
///
/// @param {String} $message - Message to log
///
@mixin log($message) {
@if im-log($message) {}
}
///
/// Function with no `@return` called next to `@warn` in Sass 3.3
/// to trigger a compiling error and stop the process.
///
@function noop() {}
///
/// Determines whether a list of conditions is intercepted by the static breakpoint.
///
/// @param {Arglist} $conditions - Media query conditions
///
/// @return {Boolean} - Returns true if the conditions are intercepted by the static breakpoint
///
@function im-intercepts-static-breakpoint($conditions...) {
$no-media-breakpoint-value: map-get($breakpoints, $im-no-media-breakpoint);
@if not $no-media-breakpoint-value {
@if im-log('`#{$im-no-media-breakpoint}` is not a valid breakpoint.') {}
}
@each $condition in $conditions {
@if not map-has-key($media-expressions, $condition) {
$operator: get-expression-operator($condition);
$prefix: get-expression-prefix($operator);
$value: get-expression-value($condition, $operator);
@if ($prefix == 'max' and $value <= $no-media-breakpoint-value) or
($prefix == 'min' and $value > $no-media-breakpoint-value) {
@return false;
}
} @else if not index($im-no-media-expressions, $condition) {
@return false;
}
}
@return true;
}
///
/// Slice `$list` between `$start` and `$end` indexes
///
/// @access private
///
/// @param {List} $list - List to slice
/// @param {Number} $start [1] - Start index
/// @param {Number} $end [length($list)] - End index
///
/// @return {List} Sliced list
///
@function slice($list, $start: 1, $end: length($list)) {
@if length($list) < 1 or $start > $end {
@return ();
}
$result: ();
@for $i from $start through $end {
$result: append($result, nth($list, $i));
}
@return $result;
}
////
/// String to number converter
/// @author Hugo Giraudel
/// @access private
////
///
/// Casts a string into a number
///
/// @param {String | Number} $value - Value to be parsed
///
/// @return {Number}
///
@function to-number($value) {
@if type-of($value) == 'number' {
@return $value;
} @else if type-of($value) != 'string' {
$_: im-log('Value for `to-number` should be a number or a string.');
}
$first-character: str-slice($value, 1, 1);
$result: 0;
$digits: 0;
$minus: ($first-character == '-');
$numbers: ('0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9);
// Remove +/- sign if present at first character
@if ($first-character == '+' or $first-character == '-') {
$value: str-slice($value, 2);
}
@for $i from 1 through str-length($value) {
$character: str-slice($value, $i, $i);
@if not (index(map-keys($numbers), $character) or $character == '.') {
@return to-length(if($minus, -$result, $result), str-slice($value, $i))
}
@if $character == '.' {
$digits: 1;
} @else if $digits == 0 {
$result: $result * 10 + map-get($numbers, $character);
} @else {
$digits: $digits * 10;
$result: $result + map-get($numbers, $character) / $digits;
}
}
@return if($minus, -$result, $result);
}
///
/// Add `$unit` to `$value`
///
/// @param {Number} $value - Value to add unit to
/// @param {String} $unit - String representation of the unit
///
/// @return {Number} - `$value` expressed in `$unit`
///
@function to-length($value, $unit) {
$units: ('px': 1px, 'cm': 1cm, 'mm': 1mm, '%': 1%, 'ch': 1ch, 'pc': 1pc, 'in': 1in, 'em': 1em, 'rem': 1rem, 'pt': 1pt, 'ex': 1ex, 'vw': 1vw, 'vh': 1vh, 'vmin': 1vmin, 'vmax': 1vmax);
@if not index(map-keys($units), $unit) {
$_: im-log('Invalid unit `#{$unit}`.');
}
@return $value * map-get($units, $unit);
}
///
/// This mixin aims at redefining the configuration just for the scope of
/// the call. It is helpful when having a component needing an extended
/// configuration such as custom breakpoints (referred to as tweakpoints)
/// for instance.
///
/// @author Hugo Giraudel
///
/// @param {Map} $tweakpoints [()] - Map of tweakpoints to be merged with `$breakpoints`
/// @param {Map} $tweak-media-expressions [()] - Map of tweaked media expressions to be merged with `$media-expression`
///
/// @example scss - Extend the global breakpoints with a tweakpoint
/// @include media-context(('custom': 678px)) {
/// .foo {
/// @include media('>phone', '<=custom') {
/// // ...
/// }
/// }
/// }
///
/// @example scss - Extend the global media expressions with a custom one
/// @include media-context($tweak-media-expressions: ('all': 'all')) {
/// .foo {
/// @include media('all', '>phone') {
/// // ...
/// }
/// }
/// }
///
/// @example scss - Extend both configuration maps
/// @include media-context(('custom': 678px), ('all': 'all')) {
/// .foo {
/// @include media('all', '>phone', '<=custom') {
/// // ...
/// }
/// }
/// }
///
@mixin media-context($tweakpoints: (), $tweak-media-expressions: ()) {
// Save global configuration
$global-breakpoints: $breakpoints;
$global-media-expressions: $media-expressions;
// Update global configuration
$breakpoints: map-merge($breakpoints, $tweakpoints) !global;
$media-expressions: map-merge($media-expressions, $tweak-media-expressions) !global;
@content;
// Restore global configuration
$breakpoints: $global-breakpoints !global;
$media-expressions: $global-media-expressions !global;
}
////
/// include-media public exposed API
/// @author Eduardo Boucas
/// @access public
////
///
/// Generates a media query based on a list of conditions
///
/// @param {Arglist} $conditions - Media query conditions
///
/// @example scss - With a single set breakpoint
/// @include media('>phone') { }
///
/// @example scss - With two set breakpoints
/// @include media('>phone', '<=tablet') { }
///
/// @example scss - With custom values
/// @include media('>=358px', '<850px') { }
///
/// @example scss - With set breakpoints with custom values
/// @include media('>desktop', '<=1350px') { }
///
/// @example scss - With a static expression
/// @include media('retina2x') { }
///
/// @example scss - Mixing everything
/// @include media('>=350px', '<tablet', 'retina3x') { }
///
@mixin media($conditions...) {
$is-list-mode: (length($conditions) == 1 and type-of(nth($conditions, 1)) == 'list');
@if ($im-media-support and length($conditions) == 0) or
(not $im-media-support and im-intercepts-static-breakpoint($conditions...) and not $is-list-mode) {
@content;
} @else if ($im-media-support and $is-list-mode) {
// List mode with AND / OR conditions
@media #{unquote(parse-media-query(resolve-feature-queries(nth($conditions, 1))))} {
@content;
}
} @else if ($im-media-support and length($conditions) > 0) {
// Legacy mode
@media #{unquote(parse-media-query(parse-expression(nth($conditions, 1))))} {
// Recursive call
@include media(slice($conditions, 2)...) {
@content;
}
}
}
}
@charset "UTF-8";
// _ _ _ _ _
// (_) | | | | | (_)
// _ _ __ ___| |_ _ __| | ___ _ __ ___ ___ __| |_ __ _
// | | '_ \ / __| | | | |/ _` |/ _ \ | '_ ` _ \ / _ \/ _` | |/ _` |
// | | | | | (__| | |_| | (_| | __/ | | | | | | __/ (_| | | (_| |
// |_|_| |_|\___|_|\__,_|\__,_|\___| |_| |_| |_|\___|\__,_|_|\__,_|
//
// Simple, elegant and maintainable media queries in Sass
// v1.4.9
//
// http://include-media.com
//
// Authors: Eduardo Boucas (@eduardoboucas)
// Hugo Giraudel (@hugogiraudel)
//
// This project is licensed under the terms of the MIT license
////
/// include-media library public configuration
/// @author Eduardo Boucas
/// @access public
////
///
/// Creates a list of global breakpoints
///
/// @example scss - Creates a single breakpoint with the label `phone`
/// $breakpoints: ('phone': 320px);
///
$breakpoints: (
'phone': 320px,
'tablet': 768px,
'desktop': 1024px
) !default;
///
/// Creates a list of static expressions or media types
///
/// @example scss - Creates a single media type (screen)
/// $media-expressions: ('screen': 'screen');
///
/// @example scss - Creates a static expression with logical disjunction (OR operator)
/// $media-expressions: (
/// 'retina2x': '(-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi)'
/// );
///
$media-expressions: (
'screen': 'screen',
'print': 'print',
'handheld': 'handheld',
'landscape': '(orientation: landscape)',
'portrait': '(orientation: portrait)',
'retina2x': '(-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi), (min-resolution: 2dppx)',
'retina3x': '(-webkit-min-device-pixel-ratio: 3), (min-resolution: 350dpi), (min-resolution: 3dppx)'
) !default;
///
/// Defines a number to be added or subtracted from each unit when declaring breakpoints with exclusive intervals
///
/// @example scss - Interval for pixels is defined as `1` by default
/// @include media('>128px') {}
///
/// /* Generates: */
/// @media (min-width: 129px) {}
///
/// @example scss - Interval for ems is defined as `0.01` by default
/// @include media('>20em') {}
///
/// /* Generates: */
/// @media (min-width: 20.01em) {}
///
/// @example scss - Interval for rems is defined as `0.1` by default, to be used with `font-size: 62.5%;`
/// @include media('>2.0rem') {}
///
/// /* Generates: */
/// @media (min-width: 2.1rem) {}
///
$unit-intervals: (
'px': 1,
'em': 0.01,
'rem': 0.1,
'': 0
) !default;
///
/// Defines whether support for media queries is available, useful for creating separate stylesheets
/// for browsers that don't support media queries.
///
/// @example scss - Disables support for media queries
/// $im-media-support: false;
/// @include media('>=tablet') {
/// .foo {
/// color: tomato;
/// }
/// }
///
/// /* Generates: */
/// .foo {
/// color: tomato;
/// }
///
$im-media-support: true !default;
///
/// Selects which breakpoint to emulate when support for media queries is disabled. Media queries that start at or
/// intercept the breakpoint will be displayed, any others will be ignored.
///
/// @example scss - This media query will show because it intercepts the static breakpoint
/// $im-media-support: false;
/// $im-no-media-breakpoint: 'desktop';
/// @include media('>=tablet') {
/// .foo {
/// color: tomato;
/// }
/// }
///
/// /* Generates: */
/// .foo {
/// color: tomato;
/// }
///
/// @example scss - This media query will NOT show because it does not intercept the desktop breakpoint
/// $im-media-support: false;
/// $im-no-media-breakpoint: 'tablet';
/// @include media('>=desktop') {
/// .foo {
/// color: tomato;
/// }
/// }
///
/// /* No output */
///
$im-no-media-breakpoint: 'desktop' !default;
///
/// Selects which media expressions are allowed in an expression for it to be used when media queries
/// are not supported.
///
/// @example scss - This media query will show because it intercepts the static breakpoint and contains only accepted media expressions
/// $im-media-support: false;
/// $im-no-media-breakpoint: 'desktop';
/// $im-no-media-expressions: ('screen');
/// @include media('>=tablet', 'screen') {
/// .foo {
/// color: tomato;
/// }
/// }
///
/// /* Generates: */
/// .foo {
/// color: tomato;
/// }
///
/// @example scss - This media query will NOT show because it intercepts the static breakpoint but contains a media expression that is not accepted
/// $im-media-support: false;
/// $im-no-media-breakpoint: 'desktop';
/// $im-no-media-expressions: ('screen');
/// @include media('>=tablet', 'retina2x') {
/// .foo {
/// color: tomato;
/// }
/// }
///
/// /* No output */
///
$im-no-media-expressions: ('screen', 'portrait', 'landscape') !default;
////
/// Cross-engine logging engine
/// @author Hugo Giraudel
/// @access private
////
///
/// Log a message either with `@error` if supported
/// else with `@warn`, using `feature-exists('at-error')`
/// to detect support.
///
/// @param {String} $message - Message to log
///
@function im-log($message) {
@if feature-exists('at-error') {
@error $message;
} @else {
@warn $message;
$_: noop();
}
@return $message;
}
///
/// Wrapper mixin for the log function so it can be used with a more friendly
/// API than `@if im-log('..') {}` or `$_: im-log('..')`. Basically, use the function
/// within functions because it is not possible to include a mixin in a function
/// and use the mixin everywhere else because it's much more elegant.
///
/// @param {String} $message - Message to log
///
@mixin log($message) {
@if im-log($message) {}
}
///
/// Function with no `@return` called next to `@warn` in Sass 3.3
/// to trigger a compiling error and stop the process.
///
@function noop() {}
///
/// Determines whether a list of conditions is intercepted by the static breakpoint.
///
/// @param {Arglist} $conditions - Media query conditions
///
/// @return {Boolean} - Returns true if the conditions are intercepted by the static breakpoint
///
@function im-intercepts-static-breakpoint($conditions...) {
$no-media-breakpoint-value: map-get($breakpoints, $im-no-media-breakpoint);
@if not $no-media-breakpoint-value {
@if im-log('`#{$im-no-media-breakpoint}` is not a valid breakpoint.') {}
}
@each $condition in $conditions {
@if not map-has-key($media-expressions, $condition) {
$operator: get-expression-operator($condition);
$prefix: get-expression-prefix($operator);
$value: get-expression-value($condition, $operator);
@if ($prefix == 'max' and $value <= $no-media-breakpoint-value) or
($prefix == 'min' and $value > $no-media-breakpoint-value) {
@return false;
}
} @else if not index($im-no-media-expressions, $condition) {
@return false;
}
}
@return true;
}
////
/// Parsing engine
/// @author Hugo Giraudel
/// @access private
////
///
/// Get operator of an expression
///
/// @param {String} $expression - Expression to extract operator from
///
/// @return {String} - Any of `>=`, `>`, `<=`, `<`, `≥`, `≤`
///
@function get-expression-operator($expression) {
@each $operator in ('>=', '>', '<=', '<', '≥', '≤') {
@if str-index($expression, $operator) {
@return $operator;
}
}
// It is not possible to include a mixin inside a function, so we have to
// rely on the `im-log(..)` function rather than the `log(..)` mixin. Because
// functions cannot be called anywhere in Sass, we need to hack the call in
// a dummy variable, such as `$_`. If anybody ever raise a scoping issue with
// Sass 3.3, change this line in `@if im-log(..) {}` instead.
$_: im-log('No operator found in `#{$expression}`.');
}
///
/// Get dimension of an expression, based on a found operator
///
/// @param {String} $expression - Expression to extract dimension from
/// @param {String} $operator - Operator from `$expression`
///
/// @return {String} - `width` or `height` (or potentially anything else)
///
@function get-expression-dimension($expression, $operator) {
$operator-index: str-index($expression, $operator);
$parsed-dimension: str-slice($expression, 0, $operator-index - 1);
$dimension: 'width';
@if str-length($parsed-dimension) > 0 {
$dimension: $parsed-dimension;
}
@return $dimension;
}
///
/// Get dimension prefix based on an operator
///
/// @param {String} $operator - Operator
///
/// @return {String} - `min` or `max`
///
@function get-expression-prefix($operator) {
@return if(index(('<', '<=', '≤'), $operator), 'max', 'min');
}
///
/// Get value of an expression, based on a found operator
///
/// @param {String} $expression - Expression to extract value from
/// @param {String} $operator - Operator from `$expression`
///
/// @return {Number} - A numeric value
///
@function get-expression-value($expression, $operator) {
$operator-index: str-index($expression, $operator);
$value: str-slice($expression, $operator-index + str-length($operator));
@if map-has-key($breakpoints, $value) {
$value: map-get($breakpoints, $value);
} @else {
$value: to-number($value);
}
$interval: map-get($unit-intervals, unit($value));
@if not $interval {
// It is not possible to include a mixin inside a function, so we have to
// rely on the `im-log(..)` function rather than the `log(..)` mixin. Because
// functions cannot be called anywhere in Sass, we need to hack the call in
// a dummy variable, such as `$_`. If anybody ever raise a scoping issue with
// Sass 3.3, change this line in `@if im-log(..) {}` instead.
$_: im-log('Unknown unit `#{unit($value)}`.');
}
@if $operator == '>' {
$value: $value + $interval;
} @else if $operator == '<' {
$value: $value - $interval;
}
@return $value;
}
///
/// Parse an expression to return a valid media-query expression
///
/// @param {String} $expression - Expression to parse
///
/// @return {String} - Valid media query
///
@function parse-expression($expression) {
// If it is part of $media-expressions, it has no operator
// then there is no need to go any further, just return the value
@if map-has-key($media-expressions, $expression) {
@return map-get($media-expressions, $expression);
}
$operator: get-expression-operator($expression);
$dimension: get-expression-dimension($expression, $operator);
$prefix: get-expression-prefix($operator);
$value: get-expression-value($expression, $operator);
@return '(#{$prefix}-#{$dimension}: #{$value})';
}
///
/// Slice `$list` between `$start` and `$end` indexes
///
/// @access private
///
/// @param {List} $list - List to slice
/// @param {Number} $start [1] - Start index
/// @param {Number} $end [length($list)] - End index
///
/// @return {List} Sliced list
///
@function slice($list, $start: 1, $end: length($list)) {
@if length($list) < 1 or $start > $end {
@return ();
}
$result: ();
@for $i from $start through $end {
$result: append($result, nth($list, $i));
}
@return $result;
}
////
/// String to number converter
/// @author Hugo Giraudel
/// @access private
////
///
/// Casts a string into a number
///
/// @param {String | Number} $value - Value to be parsed
///
/// @return {Number}
///
@function to-number($value) {
@if type-of($value) == 'number' {
@return $value;
} @else if type-of($value) != 'string' {
$_: im-log('Value for `to-number` should be a number or a string.');
}
$first-character: str-slice($value, 1, 1);
$result: 0;
$digits: 0;
$minus: ($first-character == '-');
$numbers: ('0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9);
// Remove +/- sign if present at first character
@if ($first-character == '+' or $first-character == '-') {
$value: str-slice($value, 2);
}
@for $i from 1 through str-length($value) {
$character: str-slice($value, $i, $i);
@if not (index(map-keys($numbers), $character) or $character == '.') {
@return to-length(if($minus, -$result, $result), str-slice($value, $i))
}
@if $character == '.' {
$digits: 1;
} @else if $digits == 0 {
$result: $result * 10 + map-get($numbers, $character);
} @else {
$digits: $digits * 10;
$result: $result + map-get($numbers, $character) / $digits;
}
}
@return if($minus, -$result, $result);
}
///
/// Add `$unit` to `$value`
///
/// @param {Number} $value - Value to add unit to
/// @param {String} $unit - String representation of the unit
///
/// @return {Number} - `$value` expressed in `$unit`
///
@function to-length($value, $unit) {
$units: ('px': 1px, 'cm': 1cm, 'mm': 1mm, '%': 1%, 'ch': 1ch, 'pc': 1pc, 'in': 1in, 'em': 1em, 'rem': 1rem, 'pt': 1pt, 'ex': 1ex, 'vw': 1vw, 'vh': 1vh, 'vmin': 1vmin, 'vmax': 1vmax);
@if not index(map-keys($units), $unit) {
$_: im-log('Invalid unit `#{$unit}`.');
}
@return $value * map-get($units, $unit);
}
///
/// This mixin aims at redefining the configuration just for the scope of
/// the call. It is helpful when having a component needing an extended
/// configuration such as custom breakpoints (referred to as tweakpoints)
/// for instance.
///
/// @author Hugo Giraudel
///
/// @param {Map} $tweakpoints [()] - Map of tweakpoints to be merged with `$breakpoints`
/// @param {Map} $tweak-media-expressions [()] - Map of tweaked media expressions to be merged with `$media-expression`
///
/// @example scss - Extend the global breakpoints with a tweakpoint
/// @include media-context(('custom': 678px)) {
/// .foo {
/// @include media('>phone', '<=custom') {
/// // ...
/// }
/// }
/// }
///
/// @example scss - Extend the global media expressions with a custom one
/// @include media-context($tweak-media-expressions: ('all': 'all')) {
/// .foo {
/// @include media('all', '>phone') {
/// // ...
/// }
/// }
/// }
///
/// @example scss - Extend both configuration maps
/// @include media-context(('custom': 678px), ('all': 'all')) {
/// .foo {
/// @include media('all', '>phone', '<=custom') {
/// // ...
/// }
/// }
/// }
///
@mixin media-context($tweakpoints: (), $tweak-media-expressions: ()) {
// Save global configuration
$global-breakpoints: $breakpoints;
$global-media-expressions: $media-expressions;
// Update global configuration
$breakpoints: map-merge($breakpoints, $tweakpoints) !global;
$media-expressions: map-merge($media-expressions, $tweak-media-expressions) !global;
@content;
// Restore global configuration
$breakpoints: $global-breakpoints !global;
$media-expressions: $global-media-expressions !global;
}
////
/// include-media public exposed API
/// @author Eduardo Boucas
/// @access public
////
///
/// Generates a media query based on a list of conditions
///
/// @param {Arglist} $conditions - Media query conditions
///
/// @example scss - With a single set breakpoint
/// @include media('>phone') { }
///
/// @example scss - With two set breakpoints
/// @include media('>phone', '<=tablet') { }
///
/// @example scss - With custom values
/// @include media('>=358px', '<850px') { }
///
/// @example scss - With set breakpoints with custom values
/// @include media('>desktop', '<=1350px') { }
///
/// @example scss - With a static expression
/// @include media('retina2x') { }
///
/// @example scss - Mixing everything
/// @include media('>=350px', '<tablet', 'retina3x') { }
///
@mixin media($conditions...) {
@if ($im-media-support and length($conditions) == 0) or
(not $im-media-support and im-intercepts-static-breakpoint($conditions...)) {
@content;
} @else if ($im-media-support and length($conditions) > 0) {
@media #{unquote(parse-expression(nth($conditions, 1)))} {
// Recursive call
@include media(slice($conditions, 2)...) {
@content;
}
}
}
}
////
/// include-media library public configuration
/// @author Eduardo Boucas
/// @access public
////
///
/// Creates a list of global breakpoints
///
/// @example scss - Creates a single breakpoint with the label `phone`
/// $breakpoints: ('phone': 320px);
///
$breakpoints: (
'phone': 320px,
'tablet': 768px,
'desktop': 1024px
) !default;
///
/// Creates a list of static expressions or media types
///
/// @example scss - Creates a single media type (screen)
/// $media-expressions: ('screen': 'screen');
///
/// @example scss - Creates a static expression with logical disjunction (OR operator)
/// $media-expressions: (
/// 'retina2x': '(-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi)'
/// );
///
$media-expressions: (
'screen': 'screen',
'print': 'print',
'handheld': 'handheld',
'landscape': '(orientation: landscape)',
'portrait': '(orientation: portrait)',
'retina2x': ('(-webkit-min-device-pixel-ratio: 2)', '(min-resolution: 192dpi)', '(min-resolution: 2dppx)'),
'retina3x': ('(-webkit-min-device-pixel-ratio: 3)', '(min-resolution: 350dpi)', '(min-resolution: 3dppx)')
) !default;
///
/// Defines a number to be added or subtracted from each unit when declaring breakpoints with exclusive intervals
///
/// @example scss - Interval for pixels is defined as `1` by default
/// @include media('>128px') {}
///
/// /* Generates: */
/// @media (min-width: 129px) {}
///
/// @example scss - Interval for ems is defined as `0.01` by default
/// @include media('>20em') {}
///
/// /* Generates: */
/// @media (min-width: 20.01em) {}
///
/// @example scss - Interval for rems is defined as `0.1` by default, to be used with `font-size: 62.5%;`
/// @include media('>2.0rem') {}
///
/// /* Generates: */
/// @media (min-width: 2.1rem) {}
///
$unit-intervals: (
'px': 1,
'em': 0.01,
'rem': 0.1,
'': 0
) !default;
///
/// Defines whether support for media queries is available, useful for creating separate stylesheets
/// for browsers that don't support media queries.
///
/// @example scss - Disables support for media queries
/// $im-media-support: false;
/// @include media('>=tablet') {
/// .foo {
/// color: tomato;
/// }
/// }
///
/// /* Generates: */
/// .foo {
/// color: tomato;
/// }
///
$im-media-support: true !default;
///
/// Selects which breakpoint to emulate when support for media queries is disabled. Media queries that start at or
/// intercept the breakpoint will be displayed, any others will be ignored.
///
/// @example scss - This media query will show because it intercepts the static breakpoint
/// $im-media-support: false;
/// $im-no-media-breakpoint: 'desktop';
/// @include media('>=tablet') {
/// .foo {
/// color: tomato;
/// }
/// }
///
/// /* Generates: */
/// .foo {
/// color: tomato;
/// }
///
/// @example scss - This media query will NOT show because it does not intercept the desktop breakpoint
/// $im-media-support: false;
/// $im-no-media-breakpoint: 'tablet';
/// @include media('>=desktop') {
/// .foo {
/// color: tomato;
/// }
/// }
///
/// /* No output */
///
$im-no-media-breakpoint: 'desktop' !default;
///
/// Selects which media expressions are allowed in an expression for it to be used when media queries
/// are not supported.
///
/// @example scss - This media query will show because it intercepts the static breakpoint and contains only accepted media expressions
/// $im-media-support: false;
/// $im-no-media-breakpoint: 'desktop';
/// $im-no-media-expressions: ('screen');
/// @include media('>=tablet', 'screen') {
/// .foo {
/// color: tomato;
/// }
/// }
///
/// /* Generates: */
/// .foo {
/// color: tomato;
/// }
///
/// @example scss - This media query will NOT show because it intercepts the static breakpoint but contains a media expression that is not accepted
/// $im-media-support: false;
/// $im-no-media-breakpoint: 'desktop';
/// $im-no-media-expressions: ('screen');
/// @include media('>=tablet', 'retina2x') {
/// .foo {
/// color: tomato;
/// }
/// }
///
/// /* No output */
///
$im-no-media-expressions: ('screen', 'portrait', 'landscape') !default;
////
/// include-media public exposed API
/// @author Eduardo Boucas
/// @access public
////
///
/// Generates a media query based on a list of conditions
///
/// @param {Arglist} $conditions - Media query conditions
///
/// @example scss - With a single set breakpoint
/// @include media('>phone') { }
///
/// @example scss - With two set breakpoints
/// @include media('>phone', '<=tablet') { }
///
/// @example scss - With custom values
/// @include media('>=358px', '<850px') { }
///
/// @example scss - With set breakpoints with custom values
/// @include media('>desktop', '<=1350px') { }
///
/// @example scss - With a static expression
/// @include media('retina2x') { }
///
/// @example scss - Mixing everything
/// @include media('>=350px', '<tablet', 'retina3x') { }
///
@mixin media($conditions...) {
$is-list-mode: (length($conditions) == 1 and type-of(nth($conditions, 1)) == 'list');
@if ($im-media-support and length($conditions) == 0) or
(not $im-media-support and im-intercepts-static-breakpoint($conditions...) and not $is-list-mode) {
@content;
} @else if ($im-media-support and $is-list-mode) {
// List mode with AND / OR conditions
@media #{unquote(parse-media-query(resolve-feature-queries(nth($conditions, 1))))} {
@content;
}
} @else if ($im-media-support and length($conditions) > 0) {
// Legacy mode
@media #{unquote(parse-media-query(parse-expression(nth($conditions, 1))))} {
// Recursive call
@include media(slice($conditions, 2)...) {
@content;
}
}
}
}
////
/// Parsing engine
/// @author Hugo Giraudel
/// @access private
////
///
/// Get operator of an expression
///
/// @param {String} $expression - Expression to extract operator from
///
/// @return {String} - Any of `>=`, `>`, `<=`, `<`, `≥`, `≤`
///
@function get-expression-operator($expression) {
@each $operator in ('>=', '>', '<=', '<', '≥', '≤') {
@if str-index($expression, $operator) {
@return $operator;
}
}
// It is not possible to include a mixin inside a function, so we have to
// rely on the `im-log(..)` function rather than the `log(..)` mixin. Because
// functions cannot be called anywhere in Sass, we need to hack the call in
// a dummy variable, such as `$_`. If anybody ever raise a scoping issue with
// Sass 3.3, change this line in `@if im-log(..) {}` instead.
$_: im-log('No operator found in `#{$expression}`.');
}
///
/// Get dimension of an expression, based on a found operator
///
/// @param {String} $expression - Expression to extract dimension from
/// @param {String} $operator - Operator from `$expression`
///
/// @return {String} - `width` or `height` (or potentially anything else)
///
@function get-expression-dimension($expression, $operator) {
$operator-index: str-index($expression, $operator);
$parsed-dimension: str-slice($expression, 0, $operator-index - 1);
$dimension: 'width';
@if str-length($parsed-dimension) > 0 {
$dimension: $parsed-dimension;
}
@return $dimension;
}
///
/// Get dimension prefix based on an operator
///
/// @param {String} $operator - Operator
///
/// @return {String} - `min` or `max`
///
@function get-expression-prefix($operator) {
@return if(index(('<', '<=', '≤'), $operator), 'max', 'min');
}
///
/// Get value of an expression, based on a found operator
///
/// @param {String} $expression - Expression to extract value from
/// @param {String} $operator - Operator from `$expression`
///
/// @return {Number} - A numeric value
///
@function get-expression-value($expression, $operator) {
$operator-index: str-index($expression, $operator);
$value: str-slice($expression, $operator-index + str-length($operator));
@if map-has-key($breakpoints, $value) {
$value: map-get($breakpoints, $value);
} @else {
$value: to-number($value);
}
$interval: map-get($unit-intervals, unit($value));
@if not $interval {
// It is not possible to include a mixin inside a function, so we have to
// rely on the `im-log(..)` function rather than the `log(..)` mixin. Because
// functions cannot be called anywhere in Sass, we need to hack the call in
// a dummy variable, such as `$_`. If anybody ever raise a scoping issue with
// Sass 3.3, change this line in `@if im-log(..) {}` instead.
$_: im-log('Unknown unit `#{unit($value)}`.');
}
@if $operator == '>' {
$value: $value + $interval;
} @else if $operator == '<' {
$value: $value - $interval;
}
@return $value;
}
///
/// Parse an expression to return a valid media-query expression
///
/// @param {String} $expression - Expression to parse
///
/// @return {String} - Valid media query
///
@function parse-expression($expression) {
// If it is part of $media-expressions, it has no operator
// then there is no need to go any further, just return the value
@if map-has-key($media-expressions, $expression) {
@return map-get($media-expressions, $expression);
}
$operator: get-expression-operator($expression);
$dimension: get-expression-dimension($expression, $operator);
$prefix: get-expression-prefix($operator);
$value: get-expression-value($expression, $operator);
@return '(#{$prefix}-#{$dimension}: #{$value})';
}
///
/// Get a shallow list of resolved feature queries
///
/// @param {String} $queries - A nested list structure
/// @param {Boolean} $do-parse - Try to parse expressions
///
/// @return {List} - A one item deep nested list like `(a b, c d e)`
///
@function resolve-feature-queries($queries, $do-parse: true) {
$separator: list-separator($queries);
$result: if($separator == 'space', ((), ), ());
@each $query in $queries {
$chainable-queries: ();
@if (type-of($query) == 'list') {
// List item is a list itself
$chainable-queries: resolve-feature-queries($query);
} @else {
// List item is a string
$parsed-query: if($do-parse, parse-expression($query), $query);
@if (type-of($parsed-query) == 'list') {
// Parsed expression is a list
$chainable-queries: resolve-feature-queries($parsed-query, false);
} @else {
// Parsed expression is a string
$chainable-queries: ($parsed-query);
}
}
$result: append-feature-query($result, $chainable-queries, $separator);
}
@return $result;
}
///
/// Combine two query lists as a logical AND / OR operation
///
/// @param {List} $base-queries - The host list
/// @param {List} $append-queries - The list that is being appended
/// @param {String} $separator - Either `space` or `comma`
///
/// @return {List} - A one item deep nested list like `(a b, c d e)`
///
@function append-feature-query($base-queries, $append-queries, $separator) {
$result: if($separator == 'space', (), $base-queries);
@each $append-query in $append-queries {
@if ($separator == 'space') {
// Logical AND
@each $base-query in $base-queries {
$updated-query: join($base-query, $append-query, $separator);
$result: append($result, $updated-query, 'comma');
}
} @else {
// Logical OR
$result: append($result, $append-query, 'comma');
}
}
@return $result;
}
///
/// Parse a list of resolved expressions to return a valid media-query
///
/// @param {List} $queries - A one item deep nested list like `(a b, c d e)`
///
/// @return {String} - A valid media-query string
///
@function parse-media-query($queries) {
$result: null;
$flat-queries: ();
$separator: list-separator($queries);
$conjunction: if($separator == 'space', ' and ', ', ');
@if (type-of($queries) == 'string') {
@return $queries;
}
@each $query in $queries {
@if (type-of($query) == 'list') {
$flat-queries: append($flat-queries, parse-media-query($query));
} @else {
$flat-queries: append($flat-queries, $query);
}
}
@for $i from 1 through length($flat-queries) {
$e: nth($flat-queries, $i);
$result: unquote('#{$result}#{$e}#{if($i != length($flat-queries), $conjunction, '')}');
}
@return $result;
}
////
/// Cross-engine logging engine
/// @author Hugo Giraudel
/// @access private
////
///
/// Log a message either with `@error` if supported
/// else with `@warn`, using `feature-exists('at-error')`
/// to detect support.
///
/// @param {String} $message - Message to log
///
@function im-log($message) {
@if feature-exists('at-error') {
@error $message;
} @else {
@warn $message;
$_: noop();
}
@return $message;
}
///
/// Wrapper mixin for the log function so it can be used with a more friendly
/// API than `@if im-log('..') {}` or `$_: im-log('..')`. Basically, use the function
/// within functions because it is not possible to include a mixin in a function
/// and use the mixin everywhere else because it's much more elegant.
///
/// @param {String} $message - Message to log
///
@mixin log($message) {
@if im-log($message) {}
}
///
/// Function with no `@return` called next to `@warn` in Sass 3.3
/// to trigger a compiling error and stop the process.
///
@function noop() {}
///
/// Determines whether a list of conditions is intercepted by the static breakpoint.
///
/// @param {Arglist} $conditions - Media query conditions
///
/// @return {Boolean} - Returns true if the conditions are intercepted by the static breakpoint
///
@function im-intercepts-static-breakpoint($conditions...) {
$no-media-breakpoint-value: map-get($breakpoints, $im-no-media-breakpoint);
@if not $no-media-breakpoint-value {
@if im-log('`#{$im-no-media-breakpoint}` is not a valid breakpoint.') {}
}
@each $condition in $conditions {
@if not map-has-key($media-expressions, $condition) {
$operator: get-expression-operator($condition);
$prefix: get-expression-prefix($operator);
$value: get-expression-value($condition, $operator);
@if ($prefix == 'max' and $value <= $no-media-breakpoint-value) or
($prefix == 'min' and $value > $no-media-breakpoint-value) {
@return false;
}
} @else if not index($im-no-media-expressions, $condition) {
@return false;
}
}
@return true;
}
///
/// Slice `$list` between `$start` and `$end` indexes
///
/// @access private
///
/// @param {List} $list - List to slice
/// @param {Number} $start [1] - Start index
/// @param {Number} $end [length($list)] - End index
///
/// @return {List} Sliced list
///
@function slice($list, $start: 1, $end: length($list)) {
@if length($list) < 1 or $start > $end {
@return ();
}
$result: ();
@for $i from $start through $end {
$result: append($result, nth($list, $i));
}
@return $result;
}
////
/// String to number converter
/// @author Hugo Giraudel
/// @access private
////
///
/// Casts a string into a number
///
/// @param {String | Number} $value - Value to be parsed
///
/// @return {Number}
///
@function to-number($value) {
@if type-of($value) == 'number' {
@return $value;
} @else if type-of($value) != 'string' {
$_: im-log('Value for `to-number` should be a number or a string.');
}
$first-character: str-slice($value, 1, 1);
$result: 0;
$digits: 0;
$minus: ($first-character == '-');
$numbers: ('0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9);
// Remove +/- sign if present at first character
@if ($first-character == '+' or $first-character == '-') {
$value: str-slice($value, 2);
}
@for $i from 1 through str-length($value) {
$character: str-slice($value, $i, $i);
@if not (index(map-keys($numbers), $character) or $character == '.') {
@return to-length(if($minus, -$result, $result), str-slice($value, $i))
}
@if $character == '.' {
$digits: 1;
} @else if $digits == 0 {
$result: $result * 10 + map-get($numbers, $character);
} @else {
$digits: $digits * 10;
$result: $result + map-get($numbers, $character) / $digits;
}
}
@return if($minus, -$result, $result);
}
///
/// Add `$unit` to `$value`
///
/// @param {Number} $value - Value to add unit to
/// @param {String} $unit - String representation of the unit
///
/// @return {Number} - `$value` expressed in `$unit`
///
@function to-length($value, $unit) {
$units: ('px': 1px, 'cm': 1cm, 'mm': 1mm, '%': 1%, 'ch': 1ch, 'pc': 1pc, 'in': 1in, 'em': 1em, 'rem': 1rem, 'pt': 1pt, 'ex': 1ex, 'vw': 1vw, 'vh': 1vh, 'vmin': 1vmin, 'vmax': 1vmax);
@if not index(map-keys($units), $unit) {
$_: im-log('Invalid unit `#{$unit}`.');
}
@return $value * map-get($units, $unit);
}
///
/// This mixin aims at redefining the configuration just for the scope of
/// the call. It is helpful when having a component needing an extended
/// configuration such as custom breakpoints (referred to as tweakpoints)
/// for instance.
///
/// @author Hugo Giraudel
///
/// @param {Map} $tweakpoints [()] - Map of tweakpoints to be merged with `$breakpoints`
/// @param {Map} $tweak-media-expressions [()] - Map of tweaked media expressions to be merged with `$media-expression`
///
/// @example scss - Extend the global breakpoints with a tweakpoint
/// @include media-context(('custom': 678px)) {
/// .foo {
/// @include media('>phone', '<=custom') {
/// // ...
/// }
/// }
/// }
///
/// @example scss - Extend the global media expressions with a custom one
/// @include media-context($tweak-media-expressions: ('all': 'all')) {
/// .foo {
/// @include media('all', '>phone') {
/// // ...
/// }
/// }
/// }
///
/// @example scss - Extend both configuration maps
/// @include media-context(('custom': 678px), ('all': 'all')) {
/// .foo {
/// @include media('all', '>phone', '<=custom') {
/// // ...
/// }
/// }
/// }
///
@mixin media-context($tweakpoints: (), $tweak-media-expressions: ()) {
// Save global configuration
$global-breakpoints: $breakpoints;
$global-media-expressions: $media-expressions;
// Update global configuration
$breakpoints: map-merge($breakpoints, $tweakpoints) !global;
$media-expressions: map-merge($media-expressions, $tweak-media-expressions) !global;
@content;
// Restore global configuration
$breakpoints: $global-breakpoints !global;
$media-expressions: $global-media-expressions !global;
}
/// Test `$function` with `$spec` test suite
///
/// @author Hugo Giraudel
///
/// @param {String} $function - Name of function to test
/// @param {Map} $spec - Test suite to run `$function` against
///
/// @return {Map}
/// * `function`: `$function`
/// * `length`: the length of `$tests`
/// * `pass`: number of passing tests out of `length`
/// * `fail`: number of failing tests out of `length`
/// * `tests`: list of maps containing:
/// * `input`: test input (key from `$tests`)
/// * `expected`: expected result from `input`
/// * `actual`: actual result from `input`
/// * `pass`: whether the test passed or not
/// * `fail`: whether the test failed or not
///
/// @example scss - Testing of a `double` function
/// @function double($value) { @return $value * 2 }
///
/// $test: test('double', (
/// 1: 2,
/// 2: 4,
/// 3: 6,
/// 4: 8,
/// ));
///
/// @example scss - Result of `double` tests
/// (
/// 'function': 'double',
/// 'length': 4,
/// 'pass': 4,
/// 'fail': 0,
/// 'tests': ( ... ),
/// )
///
/// @example scss - `tests` value from result of `double` tests
/// (
/// (
/// 'input': 1,
/// 'expected': 2,
/// 'actual': 2,
/// 'pass': true,
/// 'fail': false,
/// ),
/// // ...
/// )
@function test($function, $spec) {
$passing-tests: 0;
$tests: ();
@each $arguments, $expected-result in $spec {
$actual-result: call($function, $arguments...);
$passed: $actual-result == $expected-result;
$tests: append($tests, (
'input': $arguments,
'expected': $expected-result,
'actual': $actual-result,
'pass': $passed,
'fail': not $passed,
), 'comma');
@if $passed {
$passing-tests: $passing-tests + 1;
}
}
@return (
'function': $function,
'length': length($tests),
'tests': $tests,
'pass': $passing-tests,
'fail': length($tests) - $passing-tests,
);
}
/// Mixin decorating the result of `test(..)` function to throw it as an error
///
/// @author Hugo Giraudel
///
/// @param {Map} $data - Return of `test(..)` function
///
/// @example scss - Printing the result of `double` function test
/// @include run(test('double', $tests-double));
///
/// @example scss - Result of `double` function test
/// Started tests for function `double`
/// ----------
/// Test 1 out of 4... ✔
/// Test 2 out of 4... ✔
/// Test 3 out of 4... ✔
/// Test 4 out of 4... ✔
/// ----------
/// Finished: 0 test(s) failing out of 4
@mixin run($data) {
$output: '';
$length: map-get($data, 'length');
$tests: map-get($data, 'tests');
@each $test in $tests {
$output: $output
+ 'Test #{index($tests, $test)} out of #{$length}... '
+ if(map-get($test, 'pass'), '✔', '✘\a Expected : `#{map-get($test, "expected")}`\a Actual : `#{map-get($test, "actual")}`') + '\a ';
}
$message: 'Started tests for function `#{map-get($data, "function")}`\a '
+ '----------\a '
+ $output + '\a '
+ '----------\a '
+ 'Finished: #{map-get($data, "fail")} test(s) failing out of #{$length}';
@if map-get($data, "fail") > 0 {
@error $message;
} @else {
@warn $message;
}
}
@import 'parse-expression';
@import 'to-number';
@import '../dist/include-media';
@import 'helpers/SassyTester';
$tests-parse-expression: map-merge($media-expressions, (
'>phone': '(min-width: 321px)',
'<phone': '(max-width: 319px)',
'>=phone': '(min-width: 320px)',
'≥phone': '(min-width: 320px)',
'<=phone': '(max-width: 320px)',
'≤phone': '(max-width: 320px)',
'>tablet': '(min-width: 769px)',
'<tablet': '(max-width: 767px)',
'>=tablet': '(min-width: 768px)',
'≥tablet': '(min-width: 768px)',
'<=tablet': '(max-width: 768px)',
'≤tablet': '(max-width: 768px)',
'>desktop': '(min-width: 1025px)',
'<desktop': '(max-width: 1023px)',
'>=desktop': '(min-width: 1024px)',
'≥desktop': '(min-width: 1024px)',
'<=desktop': '(max-width: 1024px)',
'≤desktop': '(max-width: 1024px)'
));
@include run(test('parse-expression', $tests-parse-expression));
@import '../dist/include-media';
@import 'helpers/SassyTester';
$tests-parse-media-query: (
(('a' 'b'),): 'a and b',
(('a', 'b'),): 'a, b',
(('a' 'b', 'c' 'd'),): 'a and b, c and d',
(('a', 'b', 'c' 'd'),): 'a, b, c and d',
);
@include run(test('parse-media-query', $tests-parse-media-query));
@import '../dist/include-media';
@import 'helpers/SassyTester';
$tests-resolve-feature-queries: (
(('screen' '>desktop'), ): (
('screen' '(min-width: 1025px)'),
),
(('screen', '>desktop'), ): (
('screen'), ('(min-width: 1025px)'),
),
(('screen' ('>phone', '<desktop')), ): (
('screen' '(min-width: 321px)'), ('screen' '(max-width: 1023px)'),
),
(('retina2x' '>phone'), ): (
('(-webkit-min-device-pixel-ratio: 2)' '(min-width: 321px)'),
('(min-resolution: 192dpi)' '(min-width: 321px)'),
('(min-resolution: 2dppx)' '(min-width: 321px)'),
),
(('screen' ('height>300px' 'landscape')), ): (
('screen' '(min-height: 301px)' '(orientation: landscape)'),
),
((('screen', 'portrait') ('height>300px', 'landscape')), ): (
('screen' '(min-height: 301px)'),
('(orientation: portrait)' '(min-height: 301px)'),
('screen' '(orientation: landscape)'),
('(orientation: portrait)' '(orientation: landscape)'),
),
);
@include run(test('resolve-feature-queries', $tests-resolve-feature-queries));
@import '../dist/include-media';
@import 'helpers/SassyTester';
$tests-to-number: ();
@for $i from 1 through 10 {
$tests-to-number: map-merge($tests-to-number, (
'#{$i}': $i,
'#{-$i}': -$i,
'#{$i / 10}': ($i / 10),
'#{$i / 100}': ($i / 100),
'#{$i}px': $i * 1px,
'#{$i}em': $i * 1em,
'#{$i}rem': $i * 1rem,
'#{-$i}px': -$i * 1px,
));
}
@include run(test('to-number', $tests-to-number));
#navbar {
width: 80%;
height: 23px;
}
#navbar ul {
list-style-type: none;
}
#navbar li {
float: left;
a {
font-weight: bold;
}
}
div { width: foo(42px); height: bar(42px); }
@import "vars";
@import "struct";
.myvars {
content: quote($import_counter);
}
.common-struct {
content: "common-struct";
}
$import_counter: $import_counter + 1;
.common-vars {
content: "common-vars";
}
@import "_common";
@import "a1";
.a2 {
content: "a2";
}
@import "b1";
.b2 {
content: "b2";
}
$import_counter: 0;
@import "a";
@import "common";
@import "b";
#the-last {
content: "LAST";
}
#navbar {
width: 80%;
height: 23px;
}
#navbar ul {
list-style-type: none;
}
#navbar li {
float: left;
a {
font-weight: bold;
}
}
@import "file-not-processed-by-loader", "file-processed-by-loader";
$variable-defined-by-file-not-processed-by-loader: 'red';
body {
color: $variable-defined-by-file-not-processed-by-loader;
}
@function colorBlue() {
@return #0000fe;
}
@import 'vars';
@import 'colorBlue';
body {
background: $color;
color: colorBlue();
}
#navbar {
width: 80%;
height: 23px;
}
#navbar ul {
list-style-type: none;
}
#navbar li {
float: left;
a {
font-weight: bold;
}
}
#navbar {
width: 80%;
height: 23px;
}
#navbar ul {
list-style-type: none;
}
#navbar li {
float: left;
a {
font-weight: bold;
}
}
#navbar {
width: 80%;
height: 23px;
}
#navbar ul {
list-style-type: none;
}
#navbar li {
float: left;
a {
font-weight: bold;
}
}
#navbar {
width: 80%;
height: 23px;
}
#navbar ul {
list-style-type: none;
}
#navbar li {
float: left;
a {
font-weight: bold;
}
}
body {
background-color: $green;
}
#navbar {
width: 80%;
height: 23px;
}
#navbar ul {
list-style-type: none;
}
#navbar li {
float: left;
a {
font-weight: bold;
}
}
@import 'colors';
body {
background: $color;
}
#navbar {
width: 80%;
height: 23px;
}
#navbar ul {
list-style-type: none;
}
#navbar li {
float: left;
a {
font-weight: bold;
}
}
#navbar {
width: 80%;
height: 23px;
}
#navbar ul {
list-style-type: none;
}
#navbar li {
float: left;
a {
font-weight: bold;
}
}
#navbar {
width: 80%;
height: 23px;
}
#navbar ul {
list-style-type: none;
}
#navbar li {
float: left;
a {
font-weight: bold;
}
}
#navbar {
width: 80%;
height: 23px;
}
#navbar ul {
list-style-type: none;
}
#navbar li {
float: left;
a {
font-weight: bold;
}
}
@import "partials/one";
.one {
color: red;
}
@import "partials/three";
.one {
color: darkred;
}
@import "partials/three";
.two {
color: darkblue;
}
@import "partials/three";
.three {
color: green;
}
@import "./utils/formatters";
@import "./utils/list-to-js";
@import "./utils/map-to-js";
//---
// [$propName] - optional param (otherwise $value is taken as first)
// if provided- function returns {'propName': sassToJs($value)}
//
// $value [map, list] - Converts provided value to JSON string
// $value [bool, nulls, number, string, color] - Converts provided value to a proper format for JSON
//
// Data types in Sass: http://sass-lang.com/documentation/file.SASS_REFERENCE.html#data_types
//
// Examples of usage:
//
// .breakpoints-data{
// font-family: sassToJs($mediaBreakPointsMap);
// }
//
// .zoom-control:before{
// content: sassToJs("maxZoomStep", $zoomStep);
// }
//
//---
@function sassToJs($propName: null, $value: null) {
@if ($value == null) {
$value: $propName; // emulates sassToJs($value)
} @else {
@return '{#{sassToJs($propName)}:#{sassToJs($value)}}'; // returns {"propName": sassToJs($value)}
}
//--- VARS ---
$valueType: type-of($value);
$jsonPropValue: '';
//--- CHECKING TYPE ---
// BOOL
@if ($valueType == 'bool') {
$jsonPropValue: $value;
// NULLS
} @else if ($valueType == 'null') {
$jsonPropValue: 'null';
// NUMBER
} @else if ($valueType == 'number') {
// '100px' has also type "number" but after multiplying to "0" gives "0px"
@if (unitless($value)) {
// e.g. 100
$jsonPropValue: $value;
} @else {
// e.g. 100px
$jsonPropValue: sassToJs_formatAsString($value);
}
// MAP
} @else if ($valueType == 'map') {
$jsonPropValue: sassToJs_Map($value);
// LIST
} @else if ($valueType == 'list') {
$jsonPropValue: sassToJs_List($value);
// STRING, COLOR
} @else {
$jsonPropValue: sassToJs_formatAsString($value);
}
//--- RETURN ---
@return $jsonPropValue;
}
//---
// Formats provided argument as a string
// Might be used for strings, colors and Sass numbers like "100px"
//---
@function sassToJs_formatAsString($value) {
@return '"#{$value}"';
}
//----------
// Converts Sass list to JSON string
//----------
@function sassToJs_List($list) {
//--- VARS ---
$list-map-length: length($list);
$iteration: 0;
$json: '['; // open square bracket
// add each property from Sass list
@each $item in $list {
// ITEM VALUE
$json: $json + sassToJs($item);
// Add comma if not the last item
$iteration: $iteration + 1;
@if ($iteration < $list-map-length) {
$json: $json + ',';
}
}
$json: $json + ']'; // close square bracket
//--- RETURN ---
@return $json;
}
//----------
// Converts Sass map to JSON string
//----------
@function sassToJs_Map($list-map) {
//--- VARS ---
$list-map-length: length($list-map);
$iteration: 0;
$json: '{'; // open curly brackets
// add each property from Sass map
@each $propName, $propValue in $list-map {
// PROP NAME
$json: $json + sassToJs_formatAsString($propName) + ':';
// PROP VALUE
$json: $json + sassToJs($propValue);
// Add comma if not the last item
$iteration: $iteration + 1;
@if ($iteration < $list-map-length) {
$json: $json + ',';
}
}
$json: $json + '}'; // close curly brackets
//--- RETURN ---
@return $json;
}