From d07f664450ddaaebb44127a4bd057763d13d3f82 Mon Sep 17 00:00:00 2001 From: Feuerfuchs Date: Sun, 1 Nov 2020 20:55:14 +0100 Subject: Init --- src/_bem.scss | 62 ++++ src/_contexts.scss | 315 +++++++++++++++++++++ src/_easing.scss | 483 ++++++++++++++++++++++++++++++++ src/_functions.scss | 328 ++++++++++++++++++++++ src/_gradients.scss | 600 +++++++++++++++++++++++++++++++++++++++ src/_harmony.scss | 94 +++++++ src/_math.scss | 62 ++++ src/_props.scss | 281 +++++++++++++++++++ src/_responsive.scss | 406 +++++++++++++++++++++++++++ src/_vars.scss | 16 ++ src/bem-shortcodes.scss | 349 +++++++++++++++++++++++ src/bem/_block.scss | 392 ++++++++++++++++++++++++++ src/bem/_debug.scss | 16 ++ src/bem/_element.scss | 622 +++++++++++++++++++++++++++++++++++++++++ src/bem/_functions.scss | 26 ++ src/bem/_modifier.scss | 246 ++++++++++++++++ src/bem/_multi.scss | 131 +++++++++ src/bem/_state.scss | 146 ++++++++++ src/bem/_suffix.scss | 118 ++++++++ src/bem/_theme.scss | 61 ++++ src/bem/_validators.scss | 176 ++++++++++++ src/bem/_vars.scss | 108 +++++++ src/harmony-shortcodes.scss | 35 +++ src/main.scss | 10 + src/prep.scss | 2 + src/responsive-shortcodes.scss | 14 + 26 files changed, 5099 insertions(+) create mode 100644 src/_bem.scss create mode 100644 src/_contexts.scss create mode 100644 src/_easing.scss create mode 100644 src/_functions.scss create mode 100644 src/_gradients.scss create mode 100644 src/_harmony.scss create mode 100644 src/_math.scss create mode 100644 src/_props.scss create mode 100644 src/_responsive.scss create mode 100644 src/_vars.scss create mode 100644 src/bem-shortcodes.scss create mode 100644 src/bem/_block.scss create mode 100644 src/bem/_debug.scss create mode 100644 src/bem/_element.scss create mode 100644 src/bem/_functions.scss create mode 100644 src/bem/_modifier.scss create mode 100644 src/bem/_multi.scss create mode 100644 src/bem/_state.scss create mode 100644 src/bem/_suffix.scss create mode 100644 src/bem/_theme.scss create mode 100644 src/bem/_validators.scss create mode 100644 src/bem/_vars.scss create mode 100644 src/harmony-shortcodes.scss create mode 100644 src/main.scss create mode 100644 src/prep.scss create mode 100644 src/responsive-shortcodes.scss (limited to 'src') diff --git a/src/_bem.scss b/src/_bem.scss new file mode 100644 index 0000000..b6032ea --- /dev/null +++ b/src/_bem.scss @@ -0,0 +1,62 @@ +//// +/// BEM. +/// +/// BEM is a methodology for structuring websites and is mostly known for it's CSS naming convention. +/// BEMIT is in extension of this methodology and allows you to give blocks a more fine-grained purpose +/// than BEM alone would let you do. +/// +/// Sass does support BEM quite well thanks to the ampersand (&) and the @at-root directive. However, +/// there is no way to make sure users adhere to the BEM or BEMIT methodology. +/// That's where the mixins in this file come into play: They automatically generate the right selectors +/// and perform checks regarding the nesting order, nesting depth, and so on. +/// +/// There are comments in the mixins explaining what selector is generated. The EBNF grammar is as follows: +/// +/// (* Shorthands for block, element, modifier, suffix *) +/// entity_shorthand = "b" "e" "m" "s" "t" ; +/// +/// (* One or multiple BEMIT entities that were generated with an earlier mixin invocation *) +/// existing_entities = "{" entity_shorthand { "," entity_shorthand } "}" ; +/// +/// (* A BEM entity that doesn't depend on a parent entity *) +/// generated_independent_entity = "block" ; +/// +/// (* A BEM entity that is attached to a parent entity *) +/// generated_attached_entity = existing_entities ( "__element" | "--modifier" | "@suffix" ) ; +/// +/// (* A selector created by the user, such as "&:hover", "> a", and so on *) +/// manual_selector_part = "[manual selector]" ; +/// +/// (* A part of the selector that may or may not be in the generated result *) +/// optional_selector_part = "(" ( existing_entities | manual_selector_part ) ")" ; +/// +/// (* One part of the selector *) +/// selector_part = existing_entities | manual_selector_part | optional_selector_part | generated_independent_entity | generated_attached_entity ; +/// +/// (* How the left and right selector are related, i.e. space means right is a descendant of left, and dot means right specializes left *) +/// selector_link = " " | "." ; +/// +/// (* The whole selector *) +/// selector = selector_part { ( selector_link ) selector_part } ; +/// +/// @link https://en.bem.info/ Information about BEM +/// @link https://csswizardry.com/2015/08/bemit-taking-the-bem-naming-convention-a-step-further/ Information about BEMIT +/// +/// @group BEM +/// +/// @access public +//// + +@import 'bem/vars'; +@import 'bem/functions'; +@import 'bem/validators'; +@import 'bem/block'; +@import 'bem/element'; +@import 'bem/modifier'; +@import 'bem/suffix'; +@import 'bem/state'; +@import 'bem/theme'; +@import 'bem/multi'; +@import 'bem/debug'; + +@include iro-context-stack-create($iro-bem-context-id); diff --git a/src/_contexts.scss b/src/_contexts.scss new file mode 100644 index 0000000..556fde3 --- /dev/null +++ b/src/_contexts.scss @@ -0,0 +1,315 @@ +//// +/// Context handling. +/// +/// Contexts allow you to pass data between mixins and let you enforce a certain nesting order. +/// It's an essential part for the BEM-related mixins. +/// +/// If you want to create a new context, the easiest pattern is to create a new mixin and wrap +/// the @content between a pair of iro-context-push and iro-context-pop. +/// From within the @content, you can access the context's data with iro-context-get. +/// To make the compilation fail if a certain nesting order is violated, use +/// iro-context-assert-stack-must-contain and iro-context-assert-stack-must-not-contain. +/// +/// @group Contexts +/// +/// @access public +//// + +/// +/// Map of all context stacks. +/// +/// @type map +/// +/// @access private +/// +$iro-context-stacks: (); + +/// +/// Create a new context stack. +/// +/// @param {string} $stack-id - ID of context stack +/// +/// @throw If context stack already exists +/// +@mixin iro-context-stack-create($stack-id) { + $noop: iro-context-stack-create($stack-id); +} + +/// +/// Create a new context stack. +/// +/// @param {string} $stack-id - ID of context stack +/// +@function iro-context-stack-create($stack-id) { + @if map-has-key($iro-context-stacks, $stack-id) { + @error 'Context stack "#{inspect($stack-id)}" does not exist.'; + } + + $iro-context-stacks: map-merge($iro-context-stacks, ($stack-id: ())) !global; + + @return null; +} + +/// +/// Clear a context stack. +/// +/// @param {string} $stack-id - ID of context stack +/// +@mixin iro-context-stack-clear($stack-id) { + $noop: iro-context-stack-clear($stack-id); +} + +/// +/// Clear a context stack. +/// +/// @param {string} $stack-id - ID of context stack +/// +@function iro-context-stack-clear($stack-id) { + @if not map-has-key($iro-context-stacks, $stack-id) { + @error 'Context stack "#{inspect($stack-id)}" does not exist.'; + } + + $context-stack: (); + $iro-context-stacks: map-merge($iro-context-stacks, ($stack-id: $context-stack)) !global; + + @return null; +} + +/// +/// Delete a context stack. +/// +/// @param {string} $stack-id - ID of context stack +/// +@mixin iro-context-stack-delete($stack-id) { + $noop: iro-context-stack-delete($stack-id); +} + +/// +/// Delete a context stack. +/// +/// @param {string} $stack-id - ID of context stack +/// +@function iro-context-stack-delete($stack-id) { + @if not map-has-key($iro-context-stacks, $stack-id) { + @error 'Context stack "#{inspect($stack-id)}" does not exist.'; + } + + $iro-context-stacks: map-remove($iro-context-stacks, $stack-id) !global; + + @return null; +} + +/// +/// Push a new context to a context stack. +/// +/// @param {string} $stack-id - ID of context stack to use +/// @param {string} $id - ID of new context +/// @param {any} $data [()] - Data that belongs to the context +/// +@mixin iro-context-push($stack-id, $id, $data: ()) { + $noop: iro-context-push($stack-id, $id, $data); +} + +/// +/// Push a new context to a context stack. +/// +/// @param {string} $stack-id - ID of context stack to use +/// @param {string} $id - ID of new context +/// @param {any} $data [()] - Data that belongs to the context +/// +/// @return {list} A list with two items: 1 = context id, 2 = context data +/// +@function iro-context-push($stack-id, $id, $data: ()) { + @if not map-has-key($iro-context-stacks, $stack-id) { + @error 'Context stack "#{inspect($stack-id)}" does not exist.'; + } + + $context: $id $data; + $context-stack: map-get($iro-context-stacks, $stack-id); + $context-stack: append($context-stack, $context); + $iro-context-stacks: map-merge($iro-context-stacks, ($stack-id: $context-stack)) !global; + + @return $context; +} + +/// +/// Pop the latest context from a context stack. +/// +/// @param {string} $stack-id - ID of context stack to use +/// +/// @throw If context stack doesn't exist +/// +@mixin iro-context-pop($stack-id) { + $noop: iro-context-pop($stack-id); +} + +/// +/// Pop the latest context from a context stack. +/// +/// @param {string} $stack-id - ID of context stack to use +/// +/// @return {list} A list with two items: 1 = context id, 2 = context data +/// +@function iro-context-pop($stack-id) { + @if not map-has-key($iro-context-stacks, $stack-id) { + @error 'Context stack "#{inspect($stack-id)}" does not exist.'; + } + + $context-stack: map-get($iro-context-stacks, $stack-id); + + @if length($context-stack) == 0 { + @error 'Context stack "#{inspect($stack-id)}" is already empty.'; + } + + $popped-context: nth($context-stack, -1); + + @if length($context-stack) == 1 { + $context-stack: (); + } @else { + $context-stack: iro-list-slice($context-stack, 1, length($context-stack) - 1); + } + + $iro-context-stacks: map-merge($iro-context-stacks, ($stack-id: $context-stack)) !global; + + @return $popped-context; +} + +/// +/// Assert that a context stack must contain one of the given context IDs. +/// +/// @param {string} $stack-id - ID of context stack to use +/// @param {list} $context-ids - Context IDs +/// @param {bool} $check-head-only [false] - If false, all items will be checked. If true, only the head will be checked. +/// +/// @throw If assertion fails +/// +@mixin iro-context-assert-stack-must-contain($stack-id, $context-ids, $check-head-only: false) { + @if not iro-context-stack-contains($stack-id, $context-ids, $check-head-only) { + @error 'Must be called inside of contexts "#{inspect($context-ids)}".'; + } +} + +/// +/// Assert that a context stack must not contain any of the given context IDs. +/// +/// @param {string} $stack-id - ID of context stack to use +/// @param {list} $context-ids - Context IDs +/// @param {bool} $check-head-only [false] - If false, all items will be checked. If true, only the head will be checked. +/// +/// @throw If assertion fails +/// +@mixin iro-context-assert-stack-must-not-contain($stack-id, $context-ids, $check-head-only: false) { + @if iro-context-stack-contains($stack-id, $context-ids, $check-head-only) { + @error 'Must not be called inside of contexts "#{inspect($context-ids)}".'; + } +} + +/// +/// Check if a context stack contains one of the given context IDs. +/// +/// @param {string} $stack-id - ID of context stack to use +/// @param {list} $context-ids - Context IDs +/// @param {bool} $check-head-only [false] - If false, all items will be checked. If true, only the head will be checked. +/// +/// @return {bool} `true` if the context stack contains one of the context IDs, otherwise `false` +/// +@function iro-context-stack-contains($stack-id, $context-ids, $check-head-only: false) { + @if not map-has-key($iro-context-stacks, $stack-id) { + @error 'Context stack "#{inspect($stack-id)}" does not exist.'; + } + + $context-stack: map-get($iro-context-stacks, $stack-id); + + @if length($context-stack) == 0 { + @return false; + } + + $end-idx: if($check-head-only, length($context-stack), 1); + + @for $i from length($context-stack) through $end-idx { + $context: nth($context-stack, $i); + + @each $chk-context in $context-ids { + @if nth($context, 1) == $chk-context { + @return true; + } + } + } + + @return false; +} + +/// +/// Assert that a context stack must contain a number of contexts smaller than $max-count. +/// +/// @param {string} $stack-id - ID of context stack to use +/// @param {number} $max-count - Maximum number ofg contexts in context stack +/// +/// @throw If assertion fails +/// +@mixin iro-context-assert-stack-count($stack-id, $max-count) { + @if iro-context-stack-count($stack-id) > $max-count { + @error 'Maximum context count "#{inspect($max-count)}" exceeded.'; + } +} + +/// +/// Get the number of contexts from a context stack. +/// +/// @param {string} $stack-id - ID of context stack to use +/// +/// @return {number} The number of contexts +/// +@function iro-context-stack-count($stack-id) { + @if not map-has-key($iro-context-stacks, $stack-id) { + @error 'Context stack "#{inspect($stack-id)}" does not exist.'; + } + + $context-stack: map-get($iro-context-stacks, $stack-id); + + @return length($context-stack); +} + +/// +/// Get a specific context from the stack. +/// +/// @param {string} $stack-id - ID of context stack to use +/// @param {number | string | list} $type-or-level - If this is a number (!= 0), the nth context from the head will be returned. If it is a string, the first context with a matching ID will be returned. If it is a list, the first context that matches one of the IDs in the list will be returned. +/// +/// @return {list} Null if no match was found, otherwise a list with two items: 1. context ID, 2. context data. +/// +@function iro-context-get($stack-id, $type-or-level: null) { + @if not map-has-key($iro-context-stacks, $stack-id) { + @error 'Context stack "#{inspect($stack-id)}" does not exist.'; + } + + $context-stack: map-get($iro-context-stacks, $stack-id); + + @if length($context-stack) == 0 { + @return null; + } + + @if type-of($type-or-level) == number { + $context: nth($context-stack, -$type-or-level); + + @return $context; + } @else { + @for $i from -1 through -(length($context-stack)) { + $context: nth($context-stack, $i); + + @if type-of($type-or-level) == list { + @for $j from 1 through length($type-or-level) { + $ctx: nth($type-or-level, $j); + + @if nth($context, 1) == $ctx { + @return $context; + } + } + } @else if nth($context, 1) == $type-or-level { + @return $context; + } + } + } + + @return null; +} diff --git a/src/_easing.scss b/src/_easing.scss new file mode 100644 index 0000000..c41635b --- /dev/null +++ b/src/_easing.scss @@ -0,0 +1,483 @@ +//// +/// Easing. +/// +/// A collection of easing functions which are commonly used for animations. +/// This code is based on https://github.com/gre/bezier-easing. +/// +/// @group Easing +/// +/// @access public +//// + +/// +/// @access private +/// +$iro-cubic-bezier-sample-pool: (); + +/// +/// Sample pool size for cubic bezier calculations. +/// +$iro-cubic-bezier-sample-pool-size: 10 !default; + +/// +/// Minimum slope required to use the Newton-Raphson method for cubic bezier calculations. +/// +$iro-cubic-bezier-newton-min-slope: 0.001 !default; + +/// +/// Number of iterations of the Newton-Raphson method. +/// +$iro-cubic-bezier-newton-iters: 4 !default; + +/// +/// Precision of the subdivision method for cubic bezier calculations. +/// +$iro-cubic-bezier-subdiv-precision: 0.0000001 !default; + +/// +/// Maximum iterations of the subdivision method for cubic bezier calculations. +/// +$iro-cubic-bezier-subdiv-max-iters: 10 !default; + +/// +/// A cubic bezier function identical to the CSS cubic-bezier function. +/// +/// @param {number} $x1 - X of first point +/// @param {number} $y1 - Y of first point +/// @param {number} $x2 - X of second point +/// @param {number} $y2 - Y of second point +/// @param {number} $x - Progress between 0 and 1 inclusive +/// +/// @return {number} +/// +@function iro-cubic-bezier($x1, $y1, $x2, $y2, $x) { + // + // Cover simple cases + // + + @if ($x1 == $y1) and ($x2 == $y2) { + @return $x; + } + @if $x == 0 { + @return 0; + } + @if $x == 1 { + @return 1; + } + + // + // Generate samples + // + + $sample-pool-key: $x1 + '_' + $x2; + + @if not map-has-key($iro-cubic-bezier-sample-pool, $sample-pool-key) { + $samples: (); + + @for $i from 0 through $iro-cubic-bezier-sample-pool-size { + $samples: append($samples, iro-cubic-bezier-func($x1, $x2, $i / $iro-cubic-bezier-sample-pool-size)); + } + + $iro-cubic-bezier-sample-pool: map-merge($iro-cubic-bezier-sample-pool, ($sample-pool-key: $samples)) !global; + } + + // + // Calculate cubic bezier + // + + @return iro-cubic-bezier-func($y1, $y2, iro-cubic-bezier-t-for-x($x1, $x2, $x)); +} + +/// +/// @access private +/// +@function iro-cubic-bezier-func-a($p1, $p2) { + @return 1 - 3 * $p2 + 3 * $p1; +} + +/// +/// @access private +/// +@function iro-cubic-bezier-func-b($p1, $p2) { + @return 3 * $p2 - 6 * $p1; +} + +/// +/// @access private +/// +@function iro-cubic-bezier-func-c($p1) { + @return 3 * $p1; +} + +/// +/// One-dimensional cubic bezier function. +/// +/// @access private +/// +@function iro-cubic-bezier-func($p1, $p2, $t) { + @return ((iro-cubic-bezier-func-a($p1, $p2) * $t + iro-cubic-bezier-func-b($p1, $p2)) * $t + iro-cubic-bezier-func-c($p1)) * $t; +} + +/// +/// Derivative of the one-dimensional cubic bezier function. +/// +/// @access private +/// +@function iro-cubic-bezier-func-slope($p1, $p2, $t) { + @return 3 * iro-cubic-bezier-func-a($p1, $p2) * $t * $t + 2 * iro-cubic-bezier-func-b($p1, $p2) * $t + iro-cubic-bezier-func-c($p1); +} + +/// +/// Newton-Raphson method to calculate the t parameter for a given x parameter. +/// +/// @access private +/// +@function iro-cubic-bezier-newton-raphson($x1, $x2, $x, $t) { + @for $i from 1 through $iro-cubic-bezier-newton-iters { + $cur-slope: iro-cubic-bezier-func-slope($x1, $x2, $t); + + @if $cur-slope == 0 { + @return $t; + } + + $cur-x: iro-cubic-bezier-func($x1, $x2, $t) - $x; + $t: $t - $cur-x / $cur-slope; + } + + @return $t; +} + +/// +/// Subdivision method to calculate the t parameter for a given x parameter. +/// +/// @access private +/// +@function iro-cubic-bezier-binary-subdivide($x1, $x2, $x, $a, $b) { + $cur-x: 0; + $cur-t: 0; + $i: 0; + + @while $i < $iro-cubic-bezier-subdiv-max-iters { + $cur-t: $a + ($b - $a) / 2; + $cur-x: iro-cubic-bezier-func($x1, $x2, $cur-t) - $x; + + @if $cur-x > 0 { + $b: $cur-t; + } @else { + $a: $cur-t; + } + + @if abs($cur-x) < $iro-cubic-bezier-subdiv-precision { + @return $cur-t; + } + } + + @return $cur-t; +} + +/// +/// Calculate the t parameter for a given x parameter. +/// +/// @access private +/// +@function iro-cubic-bezier-t-for-x($x1, $x2, $x) { + $sample-pool-key: $x1 + '_' + $x2; + $samples: map-get($iro-cubic-bezier-sample-pool, $sample-pool-key); + + $intv-start: 0; + $cur-sample: 1; + $last-sample: $iro-cubic-bezier-sample-pool-size; + + @while ($cur-sample != $last-sample) and (nth($samples, $cur-sample) <= $x) { + $intv-start: $intv-start + (1 / $iro-cubic-bezier-sample-pool-size); + $cur-sample: $cur-sample + 1; + } + $cur-sample: $cur-sample - 1; + + $dist: ($x - nth($samples, $cur-sample)) / (nth($samples, $cur-sample + 1) - nth($samples, $cur-sample)); + $guess-t: $intv-start + $dist / $iro-cubic-bezier-sample-pool-size; + + $init-slope: iro-cubic-bezier-func-slope($x1, $x2, $guess-t); + @if $init-slope >= $iro-cubic-bezier-newton-min-slope { + @return iro-cubic-bezier-newton-raphson($x1, $x2, $x, $guess-t); + } @else if $init-slope == 0 { + @return $guess-t; + } @else { + @return iro-cubic-bezier-binary-subdivide($x1, $x2, $x, $intv-start, $intv-start + 1 / $iro-cubic-bezier-sample-pool-size); + } +} + +/// +/// Sinusoidal easing function (in direction). +/// +/// @param {number} $x - Progress between 0 and 1 inclusive +/// +/// @return {number} +/// +@function iro-ease($x) { + @return iro-cubic-bezier(0.25, 0.1, 0.25, 1, $x); +} + +/// +/// Sinusoidal easing function (in direction). +/// +/// @param {number} $x - Progress between 0 and 1 inclusive +/// +/// @return {number} +/// +@function iro-ease-in($x) { + @return iro-cubic-bezier(0.42, 0, 1, 1, $x); +} + +/// +/// Sinusoidal easing function (out direction). +/// +/// @param {number} $x - Progress between 0 and 1 inclusive +/// +/// @return {number} +/// +@function iro-ease-out($x) { + @return iro-cubic-bezier(0, 0, 0.58, 1, $x); +} + +/// +/// Sinusoidal easing function (in-out direction). +/// +/// @param {number} $x - Progress between 0 and 1 inclusive +/// +/// @return {number} +/// +@function iro-ease-in-out($x) { + @return iro-cubic-bezier(0.42, 0, 0.58, 1, $x); +} + +/// +/// Sinusoidal easing function (in direction). +/// +/// @param {number} $x - Progress between 0 and 1 inclusive +/// +/// @return {number} +/// +@function iro-ease-in-sine($x) { + @return iro-cubic-bezier(0.47, 0, 0.745, 0.715, $x); +} + +/// +/// Sinusoidal easing function (out direction). +/// +/// @param {number} $x - Progress between 0 and 1 inclusive +/// +/// @return {number} +/// +@function iro-ease-out-sine($x) { + @return iro-cubic-bezier(0.39, 0.575, 0.565, 1, $x); +} + +/// +/// Sinusoidal easing function (in-out direction). +/// +/// @param {number} $x - Progress between 0 and 1 inclusive +/// +/// @return {number} +/// +@function iro-ease-in-out-sine($x) { + @return iro-cubic-bezier(0.445, 0.05, 0.55, 0.95, $x); +} + +/// +/// Quadratic easing function (in direction). +/// +/// @param {number} $x - Progress between 0 and 1 inclusive +/// +/// @return {number} +/// +@function iro-ease-in-quad($x) { + @return iro-cubic-bezier(0.55, 0.085, 0.68, 0.53, $x); +} + +/// +/// Quadratic easing function (out direction). +/// +/// @param {number} $x - Progress between 0 and 1 inclusive +/// +/// @return {number} +/// +@function iro-ease-out-quad($x) { + @return iro-cubic-bezier(0.25, 0.46, 0.45, 0.94, $x); +} + +/// +/// Quadratic easing function (in-out direction). +/// +/// @param {number} $x - Progress between 0 and 1 inclusive +/// +/// @return {number} +/// +@function iro-ease-in-out-quad($x) { + @return iro-cubic-bezier(0.455, 0.03, 0.515, 0.955, $x); +} + +/// +/// Cubic easing function (in direction). +/// +/// @param {number} $x - Progress between 0 and 1 inclusive +/// +/// @return {number} +/// +@function iro-ease-in-cubic($x) { + @return iro-cubic-bezier(0.55, 0.055, 0.675, 0.19, $x); +} + +/// +/// Cubic easing function (out direction). +/// +/// @param {number} $x - Progress between 0 and 1 inclusive +/// +/// @return {number} +/// +@function iro-ease-out-cubic($x) { + @return iro-cubic-bezier(0.215, 0.61, 0.355, 1, $x); +} + +/// +/// Cubic easing function (in-out direction). +/// +/// @param {number} $x - Progress between 0 and 1 inclusive +/// +/// @return {number} +/// +@function iro-ease-in-out-cubic($x) { + @return iro-cubic-bezier(0.645, 0.045, 0.355, 1, $x); +} + +/// +/// Quart easing function (in direction). +/// +/// @param {number} $x - Progress between 0 and 1 inclusive +/// +/// @return {number} +/// +@function iro-ease-in-quart($x) { + @return iro-cubic-bezier(0.895, 0.03, 0.685, 0.22, $x); +} + +/// +/// Quart easing function (out direction). +/// +/// @param {number} $x - Progress between 0 and 1 inclusive +/// +/// @return {number} +/// +@function iro-ease-out-quart($x) { + @return iro-cubic-bezier(0.165, 0.84, 0.44, 1, $x); +} + +/// +/// Quart easing function (in-out direction). +/// +/// @param {number} $x - Progress between 0 and 1 inclusive +/// +/// @return {number} +/// +@function iro-ease-in-out-quart($x) { + @return iro-cubic-bezier(0.77, 0, 0.175, 1, $x); +} + +/// +/// Quint easing function (in direction). +/// +/// @param {number} $x - Progress between 0 and 1 inclusive +/// +/// @return {number} +/// +@function iro-ease-in-quint($x) { + @return iro-cubic-bezier(0.755, 0.05, 0.855, 0.06, $x); +} + +/// +/// Quint easing function (out direction). +/// +/// @param {number} $x - Progress between 0 and 1 inclusive +/// +/// @return {number} +/// +@function iro-ease-out-quint($x) { + @return iro-cubic-bezier(0.23, 1, 0.32, 1, $x); +} + +/// +/// Quint easing function (in-out direction). +/// +/// @param {number} $x - Progress between 0 and 1 inclusive +/// +/// @return {number} +/// +@function iro-ease-in-out-quint($x) { + @return iro-cubic-bezier(0.86, 0, 0.07, 1, $x); +} + +/// +/// Exponential easing function (in direction). +/// +/// @param {number} $x - Progress between 0 and 1 inclusive +/// +/// @return {number} +/// +@function iro-ease-in-expo($x) { + @return iro-cubic-bezier(0.95, 0.05, 0.795, 0.035, $x); +} + +/// +/// Exponential easing function (out direction). +/// +/// @param {number} $x - Progress between 0 and 1 inclusive +/// +/// @return {number} +/// +@function iro-ease-out-expo($x) { + @return iro-cubic-bezier(0.19, 1, 0.22, 1, $x); +} + +/// +/// Exponential easing function (in-out direction). +/// +/// @param {number} $x - Progress between 0 and 1 inclusive +/// +/// @return {number} +/// +@function iro-ease-in-out-expo($x) { + @return iro-cubic-bezier(1, 0, 0, 1, $x); +} + +/// +/// Circular easing function (in direction). +/// +/// @param {number} $x - Progress between 0 and 1 inclusive +/// +/// @return {number} +/// +@function iro-ease-in-circ($x) { + @return iro-cubic-bezier(0.6, 0.04, 0.98, 0.335, $x); +} + +/// +/// Circular easing function (out direction). +/// +/// @param {number} $x - Progress between 0 and 1 inclusive +/// +/// @return {number} +/// +@function iro-ease-out-circ($x) { + @return iro-cubic-bezier(0.075, 0.82, 0.165, 1, $x); +} + +/// +/// Circular easing function (in-out direction). +/// +/// @param {number} $x - Progress between 0 and 1 inclusive +/// +/// @return {number} +/// +@function iro-ease-in-out-circ($x) { + @return iro-cubic-bezier(0.785, 0.135, 0.15, 0.86, $x); +} diff --git a/src/_functions.scss b/src/_functions.scss new file mode 100644 index 0000000..2f34dc4 --- /dev/null +++ b/src/_functions.scss @@ -0,0 +1,328 @@ +//// +/// Various functions. +/// +/// This file contains various and mostly unrelated functions. Some of which +/// are used in this framework, while others are just there and may be used. +/// +/// @group Functions +/// +/// @access public +//// + +/// +/// Replace a substring with a new string. +/// +/// @param {string} $string - String to search +/// @param {string} $search - Substring that gets replaced +/// @param {string} $replace - String that replaces $search +/// +/// @return {string} A string with all instances of $search replaced with $replace +/// +@function iro-str-replace($string, $search, $replace) { + $index: str-index($string, $search); + + @if $index { + @return str-slice($string, 1, $index - 1) + $replace + iro-str-replace(str-slice($string, $index + str-length($search)), $search, $replace); + } + + @return $string; +} + +/// +/// Concatenate all items from $list. +/// +/// @param {list} $list +/// @param {number} $glue - Delimiter +/// +/// @return {string} +/// +@function iro-str-implode($list, $glue: '') { + $result: ''; + + @each $item in $list { + $result: $result + if(length($item) > 1, iro-str-implode($item, $glue), $item); + + @if $item != nth($list, length($list)) { + $result: $result + $glue; + } + } + + @return $result; +} + +/// +/// Extract a subset from the given list. +/// +/// @param {list} $list +/// @param {number} $start [1] - Indices before this value will be discarded +/// @param {number} $end [length($list)] - Indices starting after this value will be discarded +/// +/// @return {list} A slice of the list +/// +@function iro-list-slice($list, $start: 1, $end: length($list)) { + $result: (); + + @for $i from $start through $end { + @if $i != 0 { + $result: append($result, nth($list, $i), list-separator($list)); + } + } + + @return $result; +} + +/// +/// Add a new item to the beginning of a list. +/// +/// @param {list} $list +/// @param {number} $value +/// +/// @return {list} A list with $value at the beginning, followed by the other items +/// +@function iro-list-prepend($list, $value) { + $result: append((), $value, list-separator($list)); + + @if length($list) > 0 { + @for $i from 1 through length($list) { + $result: append($result, nth($list, $i), list-separator($list)); + } + } + + @return $result; +} + +/// +/// Sort numeric items in a list. +/// +/// The implementation is based on the algorithm on the German Wikipedia article +/// about quicksort: https://de.wikipedia.org/wiki/Quicksort#Pseudocode +/// +/// @param {list} $l +/// +/// @return {list} Sorted list +/// +@function iro-quicksort($l, $left: 1, $right: length($l)) { + @if $left < $right { + $pvr: iro-quicksort-partition($l, $left, $right); + $pivot: nth($pvr, 1); + $l: nth($pvr, 2); + $l: iro-quicksort($l, $left, $pivot); + $l: iro-quicksort($l, $pivot + 1, $right); + } + + @return $l; +} + +/// +/// @access private +/// +@function iro-quicksort-partition($l, $left, $right) { + $start: true; + $i: $left; + $j: $right - 1; + $pivot: nth($l, $right); + + @while ($i < $j) or $start { + @while (nth($l, $i) < $pivot) and ($i < $right - 1) { + $i: $i + 1; + } + + @while (nth($l, $j)>= $pivot) and ($j > $left) { + $j: $j - 1; + } + + @if $i < $j { + $i-val: nth($l, $i); + $l: set-nth($l, $i, nth($l, $j)); + $l: set-nth($l, $j, $i-val); + } + + $start: false; + } + + @if nth($l, $i) > $pivot { + $i-val: nth($l, $i); + $l: set-nth($l, $i, nth($l, $right)); + $l: set-nth($l, $right, $i-val); + } + + @return $i $l; +} + +/// +/// Try to get the value for the given key from the given map. If it doesn't contain that key, +/// return the provided default value instead. +/// +/// @param {map} $map +/// @param {string} $key +/// @param {any} $default +/// +/// @return {any} Either the value assigned to $key or $default +/// +@function iro-map-get-default($map, $key, $default) { + @return if(map-has-key($map, $key), map-get($map, $key), $default); +} + +/// +/// Get the value for a map within a map (or deeper). +/// +/// @param {map} $map +/// @param {string | list} $key +/// @param {any} $default [null] +/// +/// @return {any} Either the value assigned to $key or $default +/// +@function iro-map-get-deep($map, $key, $default: null) { + $value: null; + + @if type-of($key) == list { + $value: $map; + + @each $k in $key { + $value: map-get($value, $k); + + @if $value == null { + @return $default; + } + } + } @else { + $value: map-get($map, $key); + + @if $value == null { + @return $default; + } + } + + @return $value; +} + +/// +/// Merge two maps recursively. +/// +/// @param {map} $map1 +/// @param {map} $map2 +/// +/// @return {map} The result of a recursive merge of $map1 and $map2 +/// +@function iro-map-merge-recursive($map1, $map2) { + @if type-of($map1) != map or type-of($map2) != map { + @error 'Two maps expected.'; + } + + $result: $map1; + + @each $key, $value in $map2 { + @if type-of(map-get($result, $key)) == map and type-of($value) == map { + $result: map-merge($result, ($key: iro-map-merge-recursive(map-get($result, $key), $value))); + } @else { + $result: map-merge($result, ($key: $value)); + } + } + + @return $result; +} + +/// +/// Get the contents of a map as a user-friendly string representation. +/// +/// @param {map} $map +/// +/// @return {string} +/// +@function iro-map-print($map) { + $output: ''; + + @each $key, $value in $map { + $value-str: ''; + + @if type-of($value) == map { + $value-str: '[ ' + iro-map-print($value) + ' ]'; + } @else if type-of($value) == list { + $value-str: '[ ' + iro-str-implode($value, ', ') + ' ]'; + } @else if type-of($value) == string { + $value-str: '\'' + $value + '\''; + } @else { + $value-str: $value; + } + + @if $output == '' { + $output: $key + ': ' + $value-str; + } @else { + $output: $output + ', ' + $key + ': ' + $value-str; + } + } + + @return $output; +} + +/// +/// Check if the given selector ends with one of the provided suffixes. +/// +/// @param {selector} $selector +/// @param {selector} $suffixes +/// +/// @return {bool} `true` if the selector matches at least one suffix, otherwise `false`. +/// +@function iro-selector-suffix-match($selector, $suffixes) { + $match: true; + + @each $sel in $selector { + @if $match { + $sel-match: false; + + @each $suffix in $suffixes { + @if not $sel-match { + $suf-match: true; + + @for $i from 1 through length($suffix) { + @if $suf-match and (nth($sel, -$i) != nth($suffix, -$i)) { + $suf-match: false; + } + } + + @if $suf-match { + $sel-match: true; + } + } + } + + @if not $sel-match { + $match: false; + } + } + } + + @return $match; +} + +/// +/// Remove the unit from any variable. +/// +/// @param {any} $n +/// +/// @return {number} Unit-less variable +/// +@function iro-strip-unit($n) { + @return $n / ($n * 0 + 1); +} + +/// +/// Convert a pixel value to a rem value. +/// +/// @param {number} $size - Pixel value to convert +/// @param {number} $base [$iro-root-size] - Reference base font size used for conversion +/// +/// @return {number} Pixel value converted to rem +/// +@function iro-px-to-rem($size, $base: $iro-root-size) { + @return $size / $base * 1rem; +} + +/// +/// A mixin with the sole purpose of letting you use temporary variables without polluting the global namespace. +/// +/// @content +/// +@mixin iro-execute { + @content; +} diff --git a/src/_gradients.scss b/src/_gradients.scss new file mode 100644 index 0000000..7c52d63 --- /dev/null +++ b/src/_gradients.scss @@ -0,0 +1,600 @@ +//// +/// Smoother background gradients. +/// +/// The default background gradients produced by any browser have a quite harsh transition between +/// colors. This is especially apparent if you, for example, use a strong fade-out gradient to make +/// text in front of a background more readable. +/// +/// The function in this file generates smoother gradients by using easing functions of the user's +/// choice. +/// It's essentially a more flexible alternative to the PostCSS plugin "PostCSS Easing Gradients": +/// https://github.com/larsenwork/postcss-easing-gradients +/// +/// @group Background gradients +/// +/// @access public +//// + +/// +/// Number of intermediate color stops generated to achieve easing. +/// A higher value results in better quality, but also much more generated code. +/// +/// @type number +/// +$iro-easing-gradient-steps: 10 !default; + +/// +/// Generate a new easing background gradient. +/// This function is intended to be as similar as possible to the newly proposed syntax for +/// linear-gradient and radial-gradient which includes easing hints. +/// +/// @param {string} $type - Either 'linear' or 'radial', which means the gradient will be either a linear-gradient or a radial-gradient. +/// @param {string} $dir - The direction of the gradient. Depending on $type, this value must be a valid direction for linear-gradient or radial-gradient. +/// @param {color | list} $stop - A color stop as used for linear-gradient or radial-gradient. +/// @param {arglist} $stops - More color stops as used for linear-gradient or radial-gradient. Between two color stops, you may also define an easing hint such as `ease-in-out`, `cubic-bezier 0.42 0 0.58 1`, `steps 3 jump-end`, and so on. +/// +/// @return {string} A linear-gradient or radial-gradient with an alternative transitioning behavior. +/// +/// @throw If $type is invalid +/// +/// @link https://github.com/w3c/csswg-drafts/issues/1332 The new CSSWG proposal +/// +/// @example scss - A smoother linear gradient +/// .background { +/// background-image: iro-easing-gradient( +/// linear, +/// to top, +/// #000, +/// in-out-sine, +/// transparent +/// ); +/// } +/// +/// // Generates: +/// +/// .background { +/// background-image: linear-gradient( +/// to top, +/// black 0%, +/// rgba(0, 0, 0, 0.975528) 10%, +/// rgba(0, 0, 0, 0.904508) 20%, +/// rgba(0, 0, 0, 0.793893) 30%, +/// rgba(0, 0, 0, 0.654508) 40%, +/// rgba(0, 0, 0, 0.5) 50%, +/// rgba(0, 0, 0, 0.345492) 60%, +/// rgba(0, 0, 0, 0.206107) 70%, +/// rgba(0, 0, 0, 0.0954915) 80%, +/// rgba(0, 0, 0, 0.0244717) 90%, +/// rgba(0, 0, 0, 3.78257e-11) 100% +/// ); +/// } +/// +/// @example scss - A smoother radial gradient +/// .background { +/// background-image: iro-easing-gradient( +/// radial, +/// 50em 16em at 0 0, +/// #000, +/// in-out-sine, +/// transparent +/// ); +/// } +/// +/// // Generates: +/// +/// .background { +/// background-image: radial-gradient( +/// 50em 16em at 0 0, +/// black 0%, +/// rgba(0, 0, 0, 0.975528) 10%, +/// rgba(0, 0, 0, 0.904508) 20%, +/// rgba(0, 0, 0, 0.793893) 30%, +/// rgba(0, 0, 0, 0.654508) 40%, +/// rgba(0, 0, 0, 0.5) 50%, +/// rgba(0, 0, 0, 0.345492) 60%, +/// rgba(0, 0, 0, 0.206107) 70%, +/// rgba(0, 0, 0, 0.0954915) 80%, +/// rgba(0, 0, 0, 0.0244717) 90%, +/// rgba(0, 0, 0, 3.78257e-11) 100% +/// ); +/// } +/// +/// @example scss - A smoother linear gradient with complex color positions +/// .background { +/// background-image: iro-easing-gradient( +/// linear, +/// to top, +/// #000 20%, +/// in-out-sine, +/// transparent calc(20% + 25em) +/// ); +/// } +/// +/// // Generates: +/// +/// .background { +/// background-image: linear-gradient( +/// to top, +/// black 20%, +/// rgba(0, 0, 0, 0.975528) calc(20% + (20% + 25em - 20%) * 0.1), +/// rgba(0, 0, 0, 0.904508) calc(20% + (20% + 25em - 20%) * 0.2), +/// rgba(0, 0, 0, 0.793893) calc(20% + (20% + 25em - 20%) * 0.3), +/// rgba(0, 0, 0, 0.654508) calc(20% + (20% + 25em - 20%) * 0.4), +/// rgba(0, 0, 0, 0.5) calc(20% + (20% + 25em - 20%) * 0.5), +/// rgba(0, 0, 0, 0.345492) calc(20% + (20% + 25em - 20%) * 0.6), +/// rgba(0, 0, 0, 0.206107) calc(20% + (20% + 25em - 20%) * 0.7), +/// rgba(0, 0, 0, 0.0954915) calc(20% + (20% + 25em - 20%) * 0.8), +/// rgba(0, 0, 0, 0.0244717) calc(20% + (20% + 25em - 20%) * 0.9), +/// transparent calc(20% + 25em)) +/// ); +/// } +/// +@function iro-easing-gradient($type, $dir, $stop, $stops...) { + $pos-template: null; + $stops: iro-list-prepend($stops, $stop); + + $last-positioned-stop: 1; + $generated-stops: (); + + // + // Generate gradient + // + + @for $i from 1 through length($stops) { + $stop: nth($stops, $i); + + @if $i == 1 { + @if not iro-easing-gradient-is-color-stop($stop) { + @error 'The first color stop argument must be a color stop.'; + } + + @if type-of($stop) == color { + // + // The first color stop is unpositioned. The default position for the first + // color stop is 0, which is explicitly added for easier calculations. + // + + $stop: $stop 0; + $stops: set-nth($stops, $i, $stop); + } + + $generated-stops: append($generated-stops, iro-str-implode($stop, ' ')); + } @else if iro-easing-gradient-is-positioned-color-stop($stop) or ($i == length($stops)) { + @if not iro-easing-gradient-is-color-stop($stop) { + @error 'The last color stop argument must be a color stop.'; + } + + // + // Either the current stops list item is a positioned color stop, or the end of + // the stops list has been reached. + // + + @if (type-of($stop) == color) and ($i == length($stops)) { + // + // The current stop is an unpositioned color stop, which means this is the end + // of the stops list. The default position for the last color stop is 100%, which + // is explicitly added for easier calculations. + // + + $stop: $stop 100%; + $stops: set-nth($stops, $i, $stop); + } + + // + // Now the current color stop is guaranteed to be a positioned color stop. + // + + @if $i > $last-positioned-stop + 1 { + // + // There is at least one stops list item (unpositioned color stop or easing function) + // between the last positioned color stop and the current stops list item. Interpolate + // the positions of all stops list items that are color stops. + // + + $interpolated-stops: iro-easing-gradient-interpolate-stop-positions( + nth($stops, $last-positioned-stop), + iro-list-slice($stops, $last-positioned-stop + 1, $i - 1), + $stop + ); + + $new-stops: join( + iro-list-slice($stops, 1, $last-positioned-stop), + $interpolated-stops + ); + $new-stops: join( + $new-stops, + iro-list-slice($stops, $i) + ); + $stops: $new-stops; + } + + // + // Now all color stops between this one and the last positioned one have + // interpolated positions. + // Next task is to perform an easing transition between all color stops that + // have an easing function specified. The rest can be left alone since the + // browser will automatically apply a linear transition between them. + // + + $j: $last-positioned-stop + 1; + @while $j <= $i { + $easing: null; + $prev-stop: nth($stops, $j - 1); + $next-stop: nth($stops, $j); + + @if not iro-easing-gradient-is-color-stop($next-stop) { + $j: $j + 1; + + $easing: $next-stop; + $next-stop: nth($stops, $j); + + @if not iro-easing-gradient-is-color-stop($next-stop) { + @error 'There can be at most one interpolation hint between to color stops.'; + } + } + + @if $easing != null { + @if type-of($easing) == number { + @error 'Midpoint shifts are not supported.'; + } + + $easing-func: null; + $easing-args: (); + + @if type-of($easing) == list { + $easing-args: iro-list-slice($easing, 2); + $easing: nth($easing, 1); + } + + $generated-stops: join( + $generated-stops, + iro-easing-gradient-ease-stops($prev-stop, $next-stop, $easing, $easing-args) + ); + } @else { + $generated-stops: append($generated-stops, iro-str-implode($next-stop, ' ')); + } + + $j: $j + 1; + } + + $last-positioned-stop: $i; + } + } + + @if $type == 'linear' { + @return linear-gradient($dir, unquote(iro-str-implode($generated-stops, ', '))); + } @else if $type == 'radial' { + @return radial-gradient($dir, unquote(iro-str-implode($generated-stops, ', '))); + } @else { + @error 'Invalid gradient type: #{inspect($type)}.'; + } +} + +/// +/// Alias for iro-easing-gradient('linear',...) +/// +/// @see {function} iro-easing-gradient +/// +@function iro-easing-linear-gradient($dir, $stop, $stops...) { + @return iro-easing-gradient('linear', $dir, $stop, $stops...); +} + +/// +/// Alias for iro-easing-gradient('radial',...) +/// +/// @see {function} iro-easing-gradient +/// +@function iro-easing-radial-gradient($dir, $stop, $stops...) { + @return iro-easing-gradient('radial', $dir, $stop, $stops...); +} + +/// +/// Generate a smooth transition from one color stop to another using the provided easing function. +/// +/// @access private +/// +@function iro-easing-gradient-ease-stops($prev-stop, $next-stop, $easing, $easing-args: ()) { + @if $easing == 'steps' { + $steps: null; + $jump: null; + + @if length($easing-args) > 1 { + $steps: nth($easing-args, 1); + $jump: nth($easing-args, 2); + } @else { + $steps: nth($easing-args, 1); + $jump: jump-end; + } + + @return iro-easing-gradient-steps-stops($prev-stop, $next-stop, $steps, $jump); + } @else { + $easing-func: null; + @if function-exists('iro-' + $easing) { + $easing-func: get-function('iro-' + $easing); + } @else { + $easing-func: get-function($easing); + } + + @return iro-easing-gradient-bezier-stops($prev-stop, $next-stop, $easing-func, $easing-args); + } +} + +/// +/// Generate a smooth transition from one color stop to another using the provided cubic-bezier function. +/// +/// @access private +/// +@function iro-easing-gradient-bezier-stops($prev-stop, $next-stop, $easing-func, $easing-args: ()) { + $prev-stop-color: nth($prev-stop, 1); + $prev-stop-pos: nth($prev-stop, 2); + $next-stop-color: nth($next-stop, 1); + $next-stop-pos: nth($next-stop, 2); + + $stops: (); + + @if ((type-of($prev-stop-pos) == number) and (type-of($next-stop-pos) == number) and (unit($prev-stop-pos) == unit($next-stop-pos))) or ($prev-stop-pos == 0) or ($next-stop-pos == 0) { + // + // The transition color stop positions can be statically calculated. + // + + $distance: $next-stop-pos - $prev-stop-pos; + + @for $i from 1 through $iro-easing-gradient-steps { + $perc: $i / $iro-easing-gradient-steps; + + $color: null; + $pos: $prev-stop-pos + $perc * $distance; + @if $perc == 1 { + $color: $next-stop-color; + } @else { + $color: mix($next-stop-color, $prev-stop-color, call($easing-func, append($easing-args, $perc)...) * 100%); + } + + $stops: append($stops, $color + ' ' + $pos); + } + } @else { + // + // The transition color stop positions have to be dynamically calculated with the calc() function. + // + + @if type-of($prev-stop-pos) != number { + // must be calc() + @if (type-of($prev-stop-pos) != string) or (str-index($prev-stop-pos, 'calc(') != 1) { + @error 'Invalid color stop position: #{inspect($prev-stop-pos)}'; + } + + $prev-stop-pos: str-slice($prev-stop-pos, 6, str-length($prev-stop-pos) - 1); + } + + @if type-of($next-stop-pos) != number { + // must be calc() + @if (type-of($next-stop-pos) != string) or (str-index($next-stop-pos, 'calc(') != 1) { + @error 'Invalid color stop position: #{inspect($next-stop-pos)}'; + } + + $next-stop-pos: str-slice($next-stop-pos, 6, str-length($next-stop-pos) - 1); + } + + @for $i from 1 through $iro-easing-gradient-steps { + $perc: $i / $iro-easing-gradient-steps; + + $color: null; + $pos: null; + @if $perc == 1 { + $color: $next-stop-color; + $pos: calc(#{$next-stop-pos}); + } @else { + $color: mix($next-stop-color, $prev-stop-color, call($easing-func, append($easing-args, $perc)...) * 100%); + $pos: calc(#{$prev-stop-pos} + (#{$next-stop-pos} - #{$prev-stop-pos}) * #{$perc}); + } + + $stops: append($stops, $color + ' ' + $pos); + } + } + + @return $stops; +} + +/// +/// Generate a step transition from one color stop to another. +/// +/// @access private +/// +@function iro-easing-gradient-steps-stops($prev-stop, $next-stop, $steps, $jump: jump-end) { + $prev-stop-color: nth($prev-stop, 1); + $prev-stop-pos: nth($prev-stop, 2); + $next-stop-color: nth($next-stop, 1); + $next-stop-pos: nth($next-stop, 2); + + $stops: (); + + @if ((type-of($prev-stop-pos) == number) and (type-of($next-stop-pos) == number) and (unit($prev-stop-pos) == unit($next-stop-pos))) or ($prev-stop-pos == 0) or ($next-stop-pos == 0) { + // + // The transition color stop positions can be statically calculated. + // + + $distance: $next-stop-pos - $prev-stop-pos; + + @for $i from 1 through $steps { + $x1: ($i - 1) / $steps; + $x2: $i / $steps; + $y: null; + + @if $jump == jump-start { + $y: $i / $steps; + } @else if $jump == jump-end { + $y: ($i - 1) / $steps; + } @else if $jump == jump-both { + $y: $i / ($steps + 1); + } @else if $jump == jump-none { + $y: ($i - 1) / ($steps - 1); + } @else { + @error 'Invalid $jump: #{inspect($jump)}'; + } + + $color: null; + $pos1: if($x1 == 0, $prev-stop-pos, $prev-stop-pos + $x1 * $distance); + $pos2: if($x2 == 1, $next-stop-pos, $prev-stop-pos + $x2 * $distance); + + @if $y == 0 { + $color: $prev-stop-color; + } @else if $y == 1 { + $color: $next-stop-color; + } @else { + $color: mix($next-stop-color, $prev-stop-color, $y * 100%); + } + + $stops: append($stops, $color + ' ' + $pos1); + $stops: append($stops, $color + ' ' + $pos2); + } + } @else { + // + // The transition color stop positions have to be dynamically calculated with the calc() function. + // + + @if type-of($prev-stop-pos) != number { + // must be calc() + @if (type-of($prev-stop-pos) != string) or (str-index($prev-stop-pos, 'calc(') != 1) { + @error 'Invalid color stop position: #{inspect($prev-stop-pos)}'; + } + + $prev-stop-pos: str-slice($prev-stop-pos, 6, str-length($prev-stop-pos) - 1); + } + + @if type-of($next-stop-pos) != number { + // must be calc() + @if (type-of($next-stop-pos) != string) or (str-index($next-stop-pos, 'calc(') != 1) { + @error 'Invalid color stop position: #{inspect($next-stop-pos)}'; + } + + $next-stop-pos: str-slice($next-stop-pos, 6, str-length($next-stop-pos) - 1); + } + + @for $i from 1 through $steps { + $x1: ($i - 1) / $steps; + $x2: $i / $steps; + $y: null; + + @if $jump == jump-start { + $y: $i / $steps; + } @else if $jump == jump-end { + $y: ($i - 1) / $steps; + } @else if $jump == jump-both { + $y: $i / ($steps + 1); + } @else if $jump == jump-none { + $y: ($i - 1) / ($steps - 1); + } @else { + @error 'Invalid $jump: #{inspect($jump)}'; + } + + $color: null; + $pos1: if($x1 == 0, $prev-stop-pos, calc(#{$prev-stop-pos} + (#{$next-stop-pos} - #{$prev-stop-pos}) * #{$x1})); + $pos2: if($x2 == 1, $next-stop-pos, calc(#{$prev-stop-pos} + (#{$next-stop-pos} - #{$prev-stop-pos}) * #{$x2})); + + @if $y == 0 { + $color: $prev-stop-color; + } @else if $y == 1 { + $color: $next-stop-color; + } @else { + $color: mix($next-stop-color, $prev-stop-color, $y * 100%); + } + + $stops: append($stops, $color + ' ' + $pos1); + $stops: append($stops, $color + ' ' + $pos2); + } + } + + @return $stops; +} + +/// +/// Interpolate the positions of multiple color stops between two color stops whose positions are set. +/// +/// @access private +/// +@function iro-easing-gradient-interpolate-stop-positions($prev-stop, $stops, $next-stop) { + $prev-stop-pos: nth($prev-stop, 2); + $next-stop-pos: nth($next-stop, 2); + + $stops-num: 0; + @for $i from 1 through length($stops) { + $stop: nth($stops, $i); + @if iro-easing-gradient-is-color-stop($stop) { + $stops-num: $stops-num + 1; + } + } + + $i: 1; + $cur-stop-num: 1; + + @if ((type-of($prev-stop-pos) == number) and (type-of($next-stop-pos) == number) and (unit($prev-stop-pos) == unit($next-stop-pos))) or ($prev-stop-pos == 0) or ($next-stop-pos == 0) { + // + // The color stop positions can be statically calculated. + // + + $distance: $next-stop-pos - $prev-stop-pos; + + @for $i from 1 through length($stops) { + $stop: nth($stops, $i); + @if iro-easing-gradient-is-color-stop($stop) { + $pos: $prev-stop-pos + $distance / ($stops-num + 1) * $cur-stop-num; + $stops: set-nth($stops, $i, $stop $pos); + + $cur-stop-num: $cur-stop-num + 1; + } + } + } @else { + // + // The color stop positions have to be dynamically calculated with the calc() function. + // + + @if type-of($prev-stop-pos) != number { + // must be calc() + @if (type-of($prev-stop-pos) != string) or (str-index($prev-stop-pos, 'calc(') != 1) { + @error 'Invalid color stop position: #{inspect($prev-stop-pos)}'; + } + + $prev-stop-pos: str-slice($prev-stop-pos, 6, str-length($prev-stop-pos) - 1); + } + + @if type-of($next-stop-pos) != number { + // must be calc() + @if (type-of($next-stop-pos) != string) or (str-index($next-stop-pos, 'calc(') != 1) { + @error 'Invalid color stop position: #{inspect($next-stop-pos)}'; + } + + $next-stop-pos: str-slice($next-stop-pos, 6, str-length($next-stop-pos) - 1); + } + + @for $i from 1 through length($stops) { + $stop: nth($stops, $i); + @if iro-easing-gradient-is-color-stop($stop) { + $perc: $cur-stop-num / ($stops-num + 1); + $pos: calc(#{$prev-stop-pos} + (#{$next-stop-pos} - #{$prev-stop-pos}) * #{$perc}); + $stops: set-nth($stops, $i, $stop $pos); + + $cur-stop-num: $cur-stop-num + 1; + } + } + } + + @return $stops; +} + +/// +/// Check if the input is a valid color stop. +/// +/// @access private +/// +@function iro-easing-gradient-is-color-stop($input) { + @return (type-of($input) == color) or iro-easing-gradient-is-positioned-color-stop($input); +} + +/// +/// Check if the input is a valid positioned color stop. +/// +/// @access private +/// +@function iro-easing-gradient-is-positioned-color-stop($input) { + @return (type-of($input) == list) and (type-of(nth($input, 1)) == color); +} diff --git a/src/_harmony.scss b/src/_harmony.scss new file mode 100644 index 0000000..c3c8633 --- /dev/null +++ b/src/_harmony.scss @@ -0,0 +1,94 @@ +//// +/// Harmony. +/// +/// Contains functions to make a design appear more harmonic. +/// +/// @group Harmony +/// +/// @access public +//// + +/// +/// Adjust a value to a modular scale. +/// +/// For a more sophisticated solution, check out [modularscale-sass](https://github.com/modularscale/modularscale-sass). +/// +/// @link http://alistapart.com/article/more-meaningful-typography An article about modular scales by Tim Brown +/// +/// @param {number} $times - Number of iterations. If positive, $base will be multiplied with $ratio. If negative, $base will be divided by $ratio. +/// @param {number | list} $base - Single base value or, for a multi-stranded modular scale, a list of base values +/// @param {number} $ratio - Ratio +/// +/// @return {number} +/// +@function iro-harmony-modular-scale($times, $base, $ratio) { + @if type-of($base) == number { + @return $base * iro-math-pow($ratio, $times); + } + + $main-base: nth($base, 1); + $norm-bases: (); + + @each $b in iro-list-slice($base, 2) { + @if $b > $main-base { + @while $b > $main-base { + $b: $b / $ratio; + } + $b: $b * $ratio; + } @else if $b < $main-base { + @while $b < $main-base { + $b: $b * $ratio; + } + } + + $norm-bases: append($norm-bases, $b); + } + + $all-bases: append($norm-bases, $main-base); + $all-bases: iro-quicksort($all-bases); + + $base-index: $times % length($all-bases) + 1; + $exp: floor($times / length($all-bases)); + + @return nth($all-bases, $base-index) * iro-math-pow($ratio, $exp); +} + +/// +/// Combine responsive properties with modular scales to achieve responsive modular scales. +/// +/// @param {string | list} $props - Property or list of properties to set +/// @param {number} $times - Number of iterations. See iro-harmony-modular-scale for more information. +/// @param {number} $responsive-map - A map with keys = viewports and values = modular scales +/// @param {bool} $fluid [true] - If enabled, property values will smoothly transition from one viewport to the next +/// +/// @see {function} iro-harmony-modular-scale +/// +/// @example scss - Responsive font sizes between 2 viewports based on modular scales +/// $ms: ( +/// 320px: (1rem 2rem, 1.1), +/// 640px: (1rem 2rem, 1.2) +/// ); +/// +/// h1 { +/// @include iro-responsive-modular-scale(font-size, 3, $ms); +/// } +/// +/// h2 { +/// @include iro-responsive-modular-scale(font-size, 2, $ms); +/// } +/// +/// h3 { +/// @include iro-responsive-modular-scale(font-size, 1, $ms); +/// } +/// +@mixin iro-responsive-modular-scale($props, $times, $responsive-map, $fluid: true) { + $new-map: (); + + @each $key, $value in $responsive-map { + $new-map: map-merge($new-map, ( + $key: iro-harmony-modular-scale($times, $value...) + )); + } + + @include iro-responsive-property($props, $new-map, $fluid); +} diff --git a/src/_math.scss b/src/_math.scss new file mode 100644 index 0000000..9b71bf6 --- /dev/null +++ b/src/_math.scss @@ -0,0 +1,62 @@ +//// +/// Basic mathematical functions. +/// +/// @group Math functions +/// +/// @access public +//// + +/// +/// Perform exponentiation. Only integer exponents are supported. +/// +/// @param {number} $base +/// @param {number} $exp +/// +/// @return {number} +/// +/// @example scss - Exponentiation with a positive exponent +/// $result: iro-math-pow(3, 2); // The value of $result is 3^2 = 9 +/// +/// @example scss - Exponentiation with a negative exponent +/// $result: iro-math-pow(2, -3); // The value of $result is 1/(2^3) = 1/8 +/// +@function iro-math-pow($base, $exp) { + $value: 1; + + @if $exp > 0 { + @for $i from 1 through $exp { + $value: $value * $base; + } + } @else if $exp < 0 { + @for $i from 1 through -$exp { + $value: $value / $base; + } + } + + @return $value; +} + +/// +/// Clamp a number between a minimum and maximum value. +/// +/// @param {number} $value - Value to clamp +/// @param {number} $min - Minimum value +/// @param {number} $max - Maximum value +/// +/// @return {number} +/// +/// @example scss +/// $result: iro-math-clamp(20, 0, 10); // The value of $result is 10 +/// +/// @example scss +/// $result: iro-math-clamp(50, 20, 100); // The value of $result is 50 +/// +@function iro-math-clamp($value, $min, $max) { + @if $value < $min { + @return $min; + } + @if $value > $max { + @return $max; + } + @return $value; +} diff --git a/src/_props.scss b/src/_props.scss new file mode 100644 index 0000000..7377c88 --- /dev/null +++ b/src/_props.scss @@ -0,0 +1,281 @@ +//// +/// Property trees. +/// +/// Property trees allow you to organize properties in a tree structure (internally nested maps). +/// The intended use is to store all your properties at the beginning and for the rest of the +/// stylesheet you just get them. +/// +/// @group Property trees +/// +/// @access public +//// + +/// +/// The maximum depth of resolved iro-prop-ref() references. +/// +/// @type number +/// +$iro-props-native-assing-max-depth: 2 !default; + +/// +/// Indicate if property names must start with two dashes (--). +/// This is required if property trees are also used for native CSS custom properties. +/// +/// @type bool +/// +$iro-props-enforce-double-dashes: true !default; + +/// +/// Default tree name to use if no name is specified. +/// +/// @type string +/// +$iro-props-default-tree: 'default' !default; + +/// +/// List of all created property trees. +/// +/// @type list +/// +/// @access private +/// +$iro-props-trees: (); + +/// +/// Save a property tree. If a tree with the sane name already exists, the trees +/// will be merged. +/// +/// @param {map} $map - Map containing properties +/// @param {string} $tree [$iro-props-default-tree] - ID the map is saved as +/// @param {bool} $merge [false] - If a tree named $tree already exists and this value is set to true, they will be merged. Otherwise an error will be emitted. +/// +@mixin iro-props-save($map, $tree: $iro-props-default-tree, $merge: false) { + $noop: iro-props-save($map, $tree, $merge); +} + +/// +/// Save a property tree. +/// +/// @param {map} $map - Map containing properties +/// @param {string} $tree [$iro-props-default-tree] - ID the map is saved as +/// @param {bool} $merge [false] - If a tree named $tree already exists and this value is set to true, they will be merged. Otherwise an error will be emitted. +/// +@function iro-props-save($map, $tree: $iro-props-default-tree, $merge: false) { + $prop-map: null; + + @if $iro-props-enforce-double-dashes { + @if not iro-props-validate($map) { + @error 'Property tree keys must start with two dashes (--). If you don\'t use property trees for native CSS custom properties, set $iro-props-enforce-double-dashes to false.'; + } + } + + @if map-has-key($iro-props-trees, $tree) { + @if $merge { + $map: iro-map-merge-recursive(map-get($iro-props-trees, $tree), $map); + } @else { + @error 'Property tree #{inspect($tree)} does already exist.'; + } + } + + $iro-props-trees: map-merge($iro-props-trees, ($tree: $map)) !global; + + @return null; +} + +/// +/// Delete a property tree. +/// +/// @param {string} $tree [$iro-props-default-tree] - ID of the tree to be deleted +/// +@mixin iro-props-delete($tree: $iro-props-default-tree) { + $noop: iro-props-delete($tree); +} + +/// +/// Unset a property tree. +/// +/// @param {string} $tree [$iro-props-default-tree] - ID of the tree to be deleted +/// +/// @throw If the property tree does not exist +/// +@function iro-props-delete($tree: $iro-props-default-tree) { + @if not map-has-key($iro-props-trees, $tree) { + @error 'Property tree "#{inspect($tree)}" does not exist.'; + } + + $iro-props-trees: map-remove($iro-props-trees, $tree) !global; + + @return null; +} + +/// +/// Access a whole property or a subsection (i.e. value) of it. +/// +/// @param {string | list} $key [null] - Key of the property to read. If this is a list of keys, the map will be traversed in that order. +/// @param {string} $tree [$iro-props-default-tree] - ID of the property tree to use +/// @param {any} $default [null] - Default value to return of no match was found. If null, this function will throw an error instead. +/// +/// @return {any} Value assigned to property or $default +/// +/// @throw If there was no match for $key and $default is null +/// +@function iro-props-get($key: (), $tree: $iro-props-default-tree, $default: null) { + @if not map-has-key($iro-props-trees, $tree) { + @error 'Unknown tree "#{$tree}".'; + } + + $result: map-get($iro-props-trees, $tree); + + @if type-of($key) == list { + $stop: false; + + @each $k in $key { + @if map-has-key($result, $k) and not $stop { + $result: map-get($result, $k); + + @if type-of($result) == list and nth($result, 1) == 'iro-prop-ref' { + @if length($result) == 2 { + $result: iro-props-get($tree: nth($result, 2)); + } @else { + $result: iro-props-get(nth($result, 3), nth($result, 2)); + } + } + } @else { + $stop: true; + } + } + + @if $stop { + $result: null; + } + } @else { + $result: map-get($result, $key); + + @if type-of($result) == list and nth($result, 1) == 'iro-prop-ref' { + @if length($result) == 2 { + $result: iro-props-get($tree: nth($result, 2)); + } @else { + $result: iro-props-get(nth($result, 3), nth($result, 2)); + } + } + } + + @if $result == null { + @if $default == null { + @error '"#{$key}" is null.'; + } @else { + @return $default; + } + } + + @return $result; +} + +/// +/// Generate a var() function call to get native CSS custom property. +/// +/// @param {string | list} $key - Key of the property to read. If this is a list of keys, the map will be traversed in that order. +/// @param {string | null} $tree [null] - Optional tree to check if the property actually exists. +/// @param {any} $default [null] - Default value to return of no match was found. +/// +/// @return {string} var() +/// +@function iro-props-get-native($key, $tree: null, $default: null) { + @if $tree != null { + $noop: iro-props-get($key, $tree, $default); + } + + $native-var: ''; + + @if type-of($key) == list { + @each $subkey in $key { + $native-var: $native-var + $subkey; + } + } @else { + $native-var: $key; + } + + @if $default == null { + @return var(#{$native-var}); + } @else { + @return var(#{$native-var}, #{$default}); + } +} + +/// +/// Generate assignments for native CSS custom properties with the values from the specified tree. +/// +/// @param {string} $tree [$iro-props-default-tree] - ID of the property tree to use +/// @param {string} $root [()] - Sub-tree to use for assignment +/// +@mixin iro-props-assign-native($tree: $iro-props-default-tree, $root: (), $skip: ()) { + $map: iro-props-get($root, $tree); + $map: map-remove($map, $skip...); + + @include iro-props-assign-native-internal($map); +} + +/// +/// @access private +/// +@mixin iro-props-assign-native-internal($map, $prefix: '', $ref-depth: $iro-props-native-assing-max-depth) { + @each $key, $value in $map { + $rd: $ref-depth; + @if type-of($value) == list and nth($value, 1) == 'iro-prop-ref' { + @if $ref-depth != 0 { + $rd: $rd - 1; + @if length($value) == 2 { + $value: iro-props-get($tree: nth($value, 2)); + } @else { + $value: iro-props-get(nth($value, 3), nth($value, 2)); + } + } @else { + $value: null; + } + } + @if type-of($value) != map { + #{$prefix + $key}: #{$value}; + } @else { + @include iro-props-assign-native-internal($value, $prefix + $key, $rd); + } + } +} + +/// +/// Validate property names. +/// +/// @access private +/// +@function iro-props-validate($map) { + @each $key, $value in $map { + @if str-index($key, '--') != 1 { + @return false; + } + + @if type-of($value) == map { + @if not iro-props-validate($value) { + @return false; + } + } + } + + @return true; +} + +/// +/// Generate a reference to another tree. Dereferencing is lazy, so you may specify a tree that hasn't been created yet. +/// +/// @param {string} $tree [$iro-props-default-tree] - ID of the property tree to use +/// @param {string | list} $key - Key of the property to read. If this is a list of keys, the map will be traversed in that order. +/// +/// @return {list} A special list that let's Ignis know that this is a lazy value. +/// +/// @throw If there was no match for $key and $default is null +/// +@function iro-props-ref($tree: $iro-props-default-tree, $key: null) { + @if $key == null { + @return ('iro-prop-ref' $tree); + } @else { + @return ('iro-prop-ref' $tree $key); + } +} diff --git a/src/_responsive.scss b/src/_responsive.scss new file mode 100644 index 0000000..6f2a416 --- /dev/null +++ b/src/_responsive.scss @@ -0,0 +1,406 @@ +//// +/// responsive properties. +/// +/// The mixins and functions in this file allow you to scale any px- or rem-based value depending on +/// the available viewport width. One popular use case is the dynamic scaling of fonts. +/// +/// The code in this file is based on an article by Niklas Postulart: +/// http://niklaspostulart.de/2015/10/sass-responsive-type-mixin +/// +/// The following adjustments were made: +/// - Support any property passed by the user, not just font-size +/// - Allow multiple target viewports / values +/// - Provide a variant of the mixin which integrates include-media for media queries +/// +/// @group Responsive +/// +/// @access public +//// + +/// +/// If true, named viewports will be supported if a compatible $breakpoints map exists. +/// This is the case for [include-media](https://include-media.com/), for example. +/// +/// @type bool +/// +$iro-responsive-support-named-viewports: true !default; + +/// +/// Context ID used for responsive environment-related mixins. +/// +/// @type string +/// +$iro-responsive-context-id: 'responsive' !default; + +/// +/// Scale a property uniformly between a specific set of target viewports / values. +/// +/// @param {string | list} $props - Property or list of properties to set +/// @param {number} $responsive-map - A map with keys = viewports and values = target value +/// @param {bool} $fluid [true] - If enabled, property values will smoothly transition from one viewport to the next +/// @param {bool} $vertical [false] - If enabled, property viewport height will be used instead of viewport width +/// +/// @example scss - Responsive font-size between 2 viewports +/// .something { +/// @include iro-responsive-property(font-size, ( 320px: 20px, 720px: 30px )); +/// } +/// +/// // Generates: +/// +/// @media (min-width: 320px) and (max-width: 720px) { +/// .something { +/// font-size: calc(20px + 10 * ((100vw - 20px) / 400)); +/// } +/// } +/// +/// @media (max-width: 320px) { +/// .something { +/// font-size: 20px; +/// } +/// } +/// +/// @media (min-width: 720px) { +/// .something { +/// font-size: 30px; +/// } +/// } +/// +/// @example scss - Responsive font-size between 3 viewports +/// .something { +/// @include iro-responsive-property(font-size, ( 320px: 20px, 720px: 30px, 1280px: 40px )); +/// } +/// +/// // Generates: +/// +/// @media (min-width: 320px) and (max-width: 720px) { +/// .something { +/// font-size: calc(20px + 10 * ((100vw - 20px) / 400)); +/// } +/// } +/// +/// @media (min-width: 720px) and (max-width: 1280px) { +/// .something { +/// font-size: calc(30px + 10 * ((100vw - 30px) / 400)); +/// } +/// } +/// +/// @media (max-width: 320px) { +/// .something { +/// font-size: 20px; +/// } +/// } +/// +/// @media (min-width: 720px) { +/// .something { +/// font-size: 30px; +/// } +/// } +/// +@mixin iro-responsive-property($props, $responsive-map, $fluid: true, $vertical: false) { + @include iro-responsive-env(map-keys($responsive-map), $fluid, $vertical) { + @if type-of($props) == list { + @each $prop in $props { + #{$prop}: iro-responsive-set(map-values($responsive-map)); + } + } @else { + #{$props}: iro-responsive-set(map-values($responsive-map)); + } + } +} + +/// +/// Create a new responsive environment by specifying a set of viewports. +/// Inside a responsive environment, use the iro-responsive-set function to make a property scale automatically. +/// +/// @param {list} $viewports - Viewports sorted in ascending order +/// @param {bool} $fluid [true] - If enabled, property values will smoothly transition from one viewport to the next +/// @param {bool} $vertical [false] - If enabled, property viewport height will be used instead of viewport width +/// +/// @content +/// +/// @see {function} iro-responsive-set +/// +/// @example scss - Responsive font-size between 2 viewports +/// .something { +/// @include iro-responsive-env((320px, 720px)) { +/// font-size: iro-responsive-set(20px, 30px); +/// } +/// } +/// +/// // Generates: +/// +/// @media (min-width: 320px) and (max-width: 720px) { +/// .something { +/// font-size: calc(20px + 10 * ((100vw - 20px) / 400)); +/// } +/// } +/// +/// @media (max-width: 320px) { +/// .something { +/// font-size: 20px; +/// } +/// } +/// +/// @media (min-width: 720px) { +/// .something { +/// font-size: 30px; +/// } +/// } +/// +@mixin iro-responsive-env($viewports, $fluid: true, $vertical: false) { + @if length($viewports) <= 1 { + @error '$viewports must contain at least two viewports.'; + } + + $new-viewports: (); + + @each $viewport in $viewports { + @if $iro-responsive-support-named-viewports and global-variable-exists(breakpoints) { + @if map-has-key($breakpoints, $viewport) { + $viewport: map-get($breakpoints, $viewport); + } + } + + @if (type-of($viewport) != number) or unitless($viewport) { + @error '$viewports contains invalid viewports.'; + } + + $new-viewports: append($new-viewports, $viewport); + } + + $viewports: iro-quicksort($new-viewports); + + @if $new-viewports != $viewports { + @error '$viewports was not sorted in ascending order.'; + } + + @if $fluid { + $first-vp: nth($viewports, 1); + $last-vp: nth($viewports, length($viewports)); + + @include iro-context-push($iro-responsive-context-id, 'env', ( + 'viewports': $viewports, + 'mode': set, + 'index': 1, + 'fluid': $fluid, + 'vertical': $vertical, + )); + + @content; + + @include iro-context-pop($iro-responsive-context-id); + + @for $i from 1 to length($viewports) { + $prev-vp: nth($viewports, $i); + $next-vp: nth($viewports, $i + 1); + + @if not $vertical { + @media (min-width: $prev-vp) and (max-width: $next-vp) { + @include iro-context-push($iro-responsive-context-id, 'env', ( + 'viewports': $viewports, + 'mode': transition, + 'index': $i, + 'fluid': $fluid, + 'vertical': $vertical, + )); + + @content; + + @include iro-context-pop($iro-responsive-context-id); + } + } @else { + @media (min-height: $prev-vp) and (max-height: $next-vp) { + @include iro-context-push($iro-responsive-context-id, 'env', ( + 'viewports': $viewports, + 'mode': transition, + 'index': $i, + 'fluid': $fluid, + 'vertical': $vertical, + )); + + @content; + + @include iro-context-pop($iro-responsive-context-id); + } + } + } + + @if not $vertical { + @media (min-width: $last-vp) { + @include iro-context-push($iro-responsive-context-id, 'env', ( + 'viewports': $viewports, + 'mode': set, + 'index': length($viewports), + 'fluid': $fluid, + 'vertical': $vertical, + )); + + @content; + + @include iro-context-pop($iro-responsive-context-id); + } + } @else { + @media (min-height: $last-vp) { + @include iro-context-push($iro-responsive-context-id, 'env', ( + 'viewports': $viewports, + 'mode': set, + 'index': length($viewports), + 'fluid': $fluid, + 'vertical': $vertical, + )); + + @content; + + @include iro-context-pop($iro-responsive-context-id); + } + } + } @else { + @include iro-context-push($iro-responsive-context-id, 'env', ( + 'viewports': $viewports, + 'mode': set, + 'index': 1, + 'fluid': $fluid, + 'vertical': $vertical, + )); + + @content; + + @include iro-context-pop($iro-responsive-context-id); + + @for $i from 2 through length($viewports) { + $vp: nth($viewports, $i); + + @if not $vertical { + @media (min-width: $vp) { + @include iro-context-push($iro-responsive-context-id, 'env', ( + 'viewports': $viewports, + 'mode': set, + 'index': $i + )); + + @content; + + @include iro-context-pop($iro-responsive-context-id); + } + } @else { + @media (min-height: $vp) { + @include iro-context-push($iro-responsive-context-id, 'env', ( + 'viewports': $viewports, + 'mode': set, + 'index': $i + )); + + @content; + + @include iro-context-pop($iro-responsive-context-id); + } + } + } + } +} + +/// +/// Make a property scale automatically inside a responsive environment. +/// +/// @param {list} $values - Value for each viewport in the responsive environment. The first value will be used for the first viewport, the second value for the second viewport, and so on. +/// +/// @return {number|string} +/// +@function iro-responsive-set($values, $without-calc: false) { + $noop: iro-context-assert-stack-must-contain($iro-responsive-context-id, 'env'); + + $data: nth(iro-context-get($iro-responsive-context-id, 'env'), 2); + $viewports: map-get($data, 'viewports'); + $mode: map-get($data, 'mode'); + $fluid: map-get($data, 'fluid'); + $vertical: map-get($data, 'vertical'); + + @if length($values) != length($viewports) { + @error '$values must contain the same number of items as the responsive environment\'s $viewports.'; + } + + @if $mode == set { + @return nth($values, map-get($data, 'index')); + } @else { + $index: map-get($data, 'index'); + $prev-vp: nth($viewports, $index); + $next-vp: nth($viewports, $index + 1); + $prev-value: nth($values, $index); + $next-value: nth($values, $index + 1); + + @return iro-responsive-fluid-calc($prev-value, $next-value, $prev-vp, $next-vp, $vertical, $without-calc); + } +} + +/// +/// Generate the calc() function that uniformly scales a value from $min-value to $max-value depending +/// on the viewport width. +/// +/// @param {number} $min-value - Minimum value +/// @param {number} $max-value - Maximum value +/// @param {number} $min-viewport - Minimum viewport size +/// @param {number} $max-viewport - Maximum viewport size +/// @param {bool} $vertical [false] - If enabled, property viewport height will be used instead of viewport width +/// +/// @access private +/// +@function iro-responsive-fluid-calc($min-value, $max-value, $min-viewport, $max-viewport, $vertical: false, $without-calc: false) { + $value-unit: unit($min-value); + $max-value-unit: unit($max-value); + $viewport-unit: unit($min-viewport); + $max-viewport-unit: unit($max-viewport); + + @if $min-value == 0 { + $value-unit: $max-value-unit; + } + @if $max-value == 0 { + $max-value-unit: $value-unit; + } + @if $min-viewport == 0 { + $viewport-unit: $max-viewport-unit; + } + @if $max-viewport == 0 { + $max-viewport-unit: $viewport-unit; + } + + @if ($value-unit != $max-value-unit) or ($viewport-unit != $max-viewport-unit) { + @error 'Units of $min-value and $max-value, $min-viewport and $max-viewport must match.'; + } + + @if ($value-unit == rem) and ($viewport-unit == px) { + $min-viewport: iro-px-to-rem($min-viewport); + $max-viewport: iro-px-to-rem($max-viewport); + $viewport-unit: rem; + } @else if ($value-unit == px) and ($viewport-unit == rem) { + $min-value: iro-px-to-rem($min-value); + $max-value: iro-px-to-rem($max-value); + $value-unit: rem; + } + + @if $value-unit != $viewport-unit { + @error 'This combination of units is not supported.'; + } + + $value-diff: iro-strip-unit($max-value - $min-value); + $viewport-diff: iro-strip-unit($max-viewport - $min-viewport); + + $calc: ''; + + @if $min-value != 0 { + $calc: '#{$min-value} + '; + } + + @if not $vertical { + $calc: unquote('#{$calc}#{$value-diff} * ((100vw - #{$min-viewport}) / #{$viewport-diff})'); + } @else { + $calc: unquote('#{$calc}#{$value-diff} * ((100vh - #{$min-viewport}) / #{$viewport-diff})'); + } + + @if $without-calc { + @return $calc; + } @else { + @return calc(#{$calc}); + } +} + +@include iro-context-stack-create($iro-responsive-context-id); diff --git a/src/_vars.scss b/src/_vars.scss new file mode 100644 index 0000000..ce6efda --- /dev/null +++ b/src/_vars.scss @@ -0,0 +1,16 @@ +//// +/// Variables. +/// +/// Global variables that are used throughout the framework. +/// +/// @group Global variables +/// +/// @access public +//// + +/// +/// Reference root font size in px that is used for px -> rem conversions. +/// +/// @type number +/// +$iro-root-size: 16px !default; diff --git a/src/bem-shortcodes.scss b/src/bem-shortcodes.scss new file mode 100644 index 0000000..11abeed --- /dev/null +++ b/src/bem-shortcodes.scss @@ -0,0 +1,349 @@ +//// +/// Shorter version of the bem-related mixins. Useful to reduce clutter. +/// +/// @group BEM shortcodes +/// +/// @access public +//// + +/// +/// @alias iro-bem-block +/// +@mixin block($name, $type: null) { + @include iro-bem-block($name, $type: null) { + @content; + } +} + +/// +/// @alias block +/// +@mixin b($name, $type: null) { + @include block($name, $type: null) { + @content; + } +} + +/// +/// @alias iro-bem-object +/// +@mixin object($name) { + @include iro-bem-object($name) { + @content; + } +} + +/// +/// @alias object +/// +@mixin ob($name) { + @include object($name) { + @content; + } +} + +/// +/// @alias iro-bem-component +/// +@mixin component($name) { + @include iro-bem-component($name) { + @content; + } +} + +/// +/// @alias component +/// +@mixin cb($name) { + @include component($name) { + @content; + } +} + +/// +/// @alias iro-bem-layout +/// +@mixin layout($name) { + @include iro-bem-layout($name) { + @content; + } +} + +/// +/// @alias layout +/// +@mixin lb($name) { + @include layout($name) { + @content; + } +} + +/// +/// @alias iro-bem-utility +/// +@mixin utility($name) { + @include iro-bem-utility($name) { + @content; + } +} + +/// +/// @alias utility +/// +@mixin ub($name) { + @include utility($name) { + @content; + } +} + +/// +/// @alias iro-bem-scope +/// +@mixin scope($name) { + @include iro-bem-scope($name) { + @content; + } +} + +/// +/// @alias scope +/// +@mixin sb($name) { + @include scope($name) { + @content; + } +} + +/// +/// @alias iro-bem-theme +/// +@mixin theme($name) { + @include iro-bem-theme($name) { + @content; + } +} + +/// +/// @alias theme +/// +@mixin tb($name) { + @include theme($name) { + @content; + } +} + +/// +/// @alias iro-bem-js +/// +@mixin js($name) { + @include iro-bem-js($name) { + @content; + } +} + +/// +/// @alias iro-bem-qa +/// +@mixin qa($name) { + @include iro-bem-qa($name) { + @content; + } +} + +/// +/// @alias iro-bem-hack +/// +@mixin hack($name) { + @include iro-bem-hack($name) { + @content; + } +} + +/// +/// @alias iro-bem-composed-of +/// +@mixin composed-of($block, $blocks...) { + @include iro-bem-composed-of($block, $blocks...) { + @content; + } +} + +/// +/// @alias composed-of +/// +@mixin co($block, $blocks...) { + @include composed-of($block, $blocks...) { + @content; + } +} + +/// +/// @alias iro-bem-element +/// +@mixin element($name, $names...) { + @include iro-bem-element($name, $names...) { + @content; + } +} + +/// +/// @alias element +/// +@mixin e($name, $names...) { + @include element($name, $names...) { + @content; + } +} + +/// +/// @alias iro-bem-related-element +/// +@mixin related-element($sign, $name, $names...) { + @include iro-bem-related-element($sign, $name, $names...) { + @content; + } +} + +/// +/// @alias related-element +/// +@mixin re($sign, $name, $names...) { + @include related-element($sign, $name, $names...) { + @content; + } +} + +/// +/// @alias iro-bem-sibling-element +/// +@mixin sibling-element($name, $names...) { + @include iro-bem-sibling-element($name, $names...) { + @content; + } +} + +/// +/// @alias sibling-element +/// +@mixin se($name, $names...) { + @include sibling-element($name, $names...) { + @content; + } +} + +/// +/// @alias iro-bem-next-element +/// +@mixin next-element($name, $names...) { + @include iro-bem-next-element($name, $names...) { + @content; + } +} + +/// +/// @alias next-element +/// +@mixin ne($name, $names...) { + @include next-element($name, $names...) { + @content; + } +} + +/// +/// @alias iro-bem-next-twin-element +/// +@mixin next-twin-element { + @include iro-bem-next-twin-element { + @content; + } +} + +/// +/// @alias next-twin-element +/// +@mixin te { + @include next-twin-element { + @content; + } +} + +/// +/// @alias iro-bem-modifier +/// +@mixin modifier($name, $names...) { + @include iro-bem-modifier($name, $names...) { + @content; + } +} + +/// +/// @alias modifier +/// +@mixin m($name, $names...) { + @include modifier($name, $names...) { + @content; + } +} + +/// +/// @alias iro-bem-suffix +/// +@mixin suffix($name) { + @include iro-bem-suffix($name) { + @content; + } +} + +/// +/// @alias suffix +/// +@mixin s($name) { + @include suffix($name) { + @content; + } +} + +/// +/// @alias iro-bem-is +/// +@mixin is($state, $states...) { + @include iro-bem-is($state, $states...) { + @content; + } +} + +/// +/// @alias iro-bem-has +/// +@mixin has($state, $states...) { + @include iro-bem-has($state, $states...) { + @content; + } +} + +/// +/// @alias iro-bem-at-theme +/// +@mixin at-theme($name, $names...) { + @include iro-bem-at-theme($name, $names...) { + @content; + } +} + +/// +/// @alias theme +/// +@mixin at($name, $names...) { + @include at-theme($name, $names...) { + @content; + } +} + +/// +/// @alias iro-bem-multi +/// +@mixin multi($first, $others...) { + @include iro-bem-multi($first, $others...) { + @content; + } +} diff --git a/src/bem/_block.scss b/src/bem/_block.scss new file mode 100644 index 0000000..d065891 --- /dev/null +++ b/src/bem/_block.scss @@ -0,0 +1,392 @@ +//// +/// @group BEM +/// +/// @access public +//// + +/// +/// Generate a new block. +/// +/// This mixin simply creates a new block with the name {namespace}_{name}, +/// where {namespace} is the prefix assigned to $type and {name} is the +/// block's name. +/// +/// @param {string} $name - Block name +/// @param {string} $type [null] - BEMIT namespace of the block +/// +/// @content +/// +/// @throw If $type is invalid +/// @throw If the block is preceded by another block, element, modifier or suffix +/// +/// @example scss - Creating a new block +/// @include iro-bem-block('something', 'component') { +/// /* some definitions */ +/// } +/// +/// // Generates: +/// +/// .c-something { +/// /* some definitions */ +/// } +/// +@mixin iro-bem-block($name, $type: null) { + $result: iro-bem-block($name, $type); + $selector: nth($result, 1); + $context: nth($result, 2); + + @include iro-bem-validate( + 'block', + (name: $name, type: $type), + $selector, + $context + ); + + @if $type != null { + $iro-bem-blocks: append($iro-bem-blocks, $name + '_' + $type) !global; + } @else { + $iro-bem-blocks: append($iro-bem-blocks, $name) !global; + } + + @include iro-context-push($iro-bem-context-id, $context...); + @at-root #{$selector} { + @content; + } + @include iro-context-pop($iro-bem-context-id); +} + +/// +/// Generate a new block. Check the respective mixin documentation for more information. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-block +/// +@function iro-bem-block($name, $type: null) { + // + // Possible outcomes: + // - ({b,e,m,s}) block + // + + $noop: iro-context-assert-stack-count($iro-bem-context-id, $iro-bem-max-depth); + + $selector: null; + $base-selector: null; + + @if $type != null { + $namespace: map-get($iro-bem-namespaces, $type); + + @if not $namespace { + @error '"#{$type}" is not a valid type.'; + } + + $base-selector: selector-parse('.' + $namespace + '-' + $name); + + @if $type != 'theme' or & { + $selector: $base-selector; + } @else if not & { + $selector: iro-bem-theme-selector($name); + } + } @else { + $base-selector: selector-parse('.' + $name); + $selector: $base-selector; + } + + @if & { + $selector: selector-nest(&, $selector); + } + + $context: 'block', ( + 'name': $name, + 'type': $type, + 'selector': $selector, + 'base-selector': $base-selector + ); + + @return $selector $context; +} + +/// +/// Generate a new object block. It's a shorthand for iro-bem-block($name, 'object'). +/// +/// @param {string} $name - Object block name +/// +/// @content +/// +@mixin iro-bem-object($name) { + @include iro-bem-block($name, 'object') { + @content; + } +} + +/// +/// Generate a new object block. Check the respective mixin documentation for more information. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-object +/// +@function iro-bem-object($name) { + @return iro-bem-block($name, 'object'); +} + +/// +/// Generate a new component block. It's a shorthand for iro-bem-block($name, 'component'). +/// +/// @param {string} $name - Component block name +/// +/// @content +/// +@mixin iro-bem-component($name) { + @include iro-bem-block($name, 'component') { + @content; + } +} + +/// +/// Generate a new component block. Check the respective mixin documentation for more information. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-component +/// +@function iro-bem-component($name) { + @return iro-bem-block($name, 'component'); +} + +/// +/// Generate a new layout block. It's a shorthand for iro-bem-block($name, 'layout'). +/// +/// @param {string} $name - Layout block name +/// +/// @content +/// +@mixin iro-bem-layout($name) { + @include iro-bem-block($name, 'layout') { + @content; + } +} + +/// +/// Generate a new layout block. Check the respective mixin documentation for more information. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-layout +/// +@function iro-bem-layout($name) { + @return iro-bem-block($name, 'layout'); +} + +/// +/// Generate a new utility block. It's a shorthand for iro-bem-block($name, 'utility'). +/// +/// @param {string} $name - Utility block name +/// +/// @content +/// +@mixin iro-bem-utility($name) { + @include iro-bem-block($name, 'utility') { + @content; + } +} + +/// +/// Generate a new utility block. Check the respective mixin documentation for more information. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-utility +/// +@function iro-bem-utility($name) { + @return iro-bem-block($name, 'utility'); +} + +/// +/// Generate a new scope block. It's a shorthand for iro-bem-block($name, 'scope'). +/// +/// @param {string} $name - Scope block name +/// +/// @content +/// +@mixin iro-bem-scope($name) { + @include iro-bem-block($name, 'scope') { + @content; + } +} + +/// +/// Generate a new scope block. Check the respective mixin documentation for more information. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-scope +/// +@function iro-bem-scope($name) { + @return iro-bem-block($name, 'scope'); +} + +/// +/// Generate a new theme block. It's a shorthand for iro-bem-block($name, 'theme'). +/// +/// @param {string} $name - Theme block name +/// +/// @content +/// +@mixin iro-bem-theme($name) { + @include iro-bem-block($name, 'theme') { + @content; + } +} + +/// +/// Generate a new theme block. Check the respective mixin documentation for more information. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-theme +/// +@function iro-bem-theme($name) { + @return iro-bem-block($name, 'theme'); +} + +/// +/// Generate a new JS block. It's a shorthand for iro-bem-block($name, 'js'). +/// +/// @param {string} $name - JS block name +/// +/// @content +/// +@mixin iro-bem-js($name) { + @include iro-bem-block($name, 'js') { + @content; + } +} + +/// +/// Generate a new JS block. Check the respective mixin documentation for more information. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-js +/// +@function iro-bem-js($name) { + @return iro-bem-block($name, 'js'); +} + +/// +/// Generate a new QA block. It's a shorthand for iro-bem-block($name, 'qa'). +/// +/// @param {string} $name - QA block name +/// +/// @content +/// +@mixin iro-bem-qa($name) { + @include iro-bem-block($name, 'qa') { + @content; + } +} + +/// +/// Generate a new QA block. Check the respective mixin documentation for more information. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-qa +/// +@function iro-bem-qa($name) { + @return iro-bem-block($name, 'qa'); +} + +/// +/// Generate a new hack block. It's a shorthand for iro-bem-block($name, 'hack'). +/// +/// @param {string} $name - Hack block name +/// +/// @content +/// +@mixin iro-bem-hack($name) { + @include iro-bem-block($name, 'hack') { + @content; + } +} + +/// +/// Generate a new hack block. Check the respective mixin documentation for more information. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-hack +/// +@function iro-bem-hack($name) { + @return iro-bem-block($name, 'hack'); +} + +/// +/// Assert that a block or element is composed of another block. In BEM, such a relationship is referred to +/// as a "mix": https://en.bem.info/methodology/key-concepts/#mix +/// +/// Compilation will fail if the foreign block doesn't exist. This way, you can ensure that blocks are +/// defined in the right order so that composed blocks/elements will actually override the foreign +/// declarations without having to artificially increase the specificity. +/// +/// @param {string | list} $block - Either first block name, or list with two items: 1. block name, 2. block type +/// @param {string | list} $blocks - Either other block names, or list with two items: 1. block name, 2. block type +/// +/// @throw If a block type is invalid +/// @throw If a block doesn't exist +/// +/// @example scss - Successful assertion +/// @include iro-bem-component('someBlock') { +/// /* some definitions */ +/// } +/// +/// @include iro-bem-component('anotherBlock') { +/// /* some definitions */ +/// +/// @include iro-bem-element('elem') { +/// @include iro-bem-composed-of('someBlock' 'component'); +/// +/// /* some definitions */ +/// } +/// } +/// +/// // Intended use:
...
+/// +/// @example scss - Failing assertion +/// @include iro-bem-component('anotherBlock') { +/// /* some definitions */ +/// +/// @include iro-bem-element('elem') { +/// @include iro-bem-composed-of('someBlock' 'component'); +/// +/// /* some definitions */ +/// } +/// } +/// +/// @include iro-bem-component('someBlock') { +/// /* some definitions */ +/// } +/// +/// // Compilation will fail because c-someBlock is defined after c-anotherBlock__elem +/// +@mixin iro-bem-composed-of($block, $blocks...) { + @each $block in iro-list-prepend($blocks, $block) { + @if type-of($block) == string { + @if not index($iro-bem-blocks, $block) { + @error 'Block "#{$block}" does not exist.'; + } + } @else { + $name: nth($block, 1); + $type: nth($block, 2); + + @if not map-get($iro-bem-namespaces, $type) { + @error '"#{$type}" is not a valid type.'; + } + + @if not index($iro-bem-blocks, $name + '_' + $type) { + @error 'Block "#{$name}" does not exist.'; + } + } + } +} diff --git a/src/bem/_debug.scss b/src/bem/_debug.scss new file mode 100644 index 0000000..e69083c --- /dev/null +++ b/src/bem/_debug.scss @@ -0,0 +1,16 @@ +//// +/// @group BEM +/// +/// @access public +//// + +@if $iro-bem-debug { + @each $type, $color in $iro-bem-debug-colors { + $namespace: map-get($iro-bem-namespaces, $type); + + [class^='#{$namespace}-'], + [class*=' #{$namespace}-'] { + outline: 5px solid $color; + } + } +} diff --git a/src/bem/_element.scss b/src/bem/_element.scss new file mode 100644 index 0000000..b3d2fee --- /dev/null +++ b/src/bem/_element.scss @@ -0,0 +1,622 @@ +//// +/// @group BEM +/// +/// @access public +//// + +/// +/// Generate a new BEM element. +/// +/// The element will be generated according to the BEM naming convention. +/// If the parent selector doesn't match the block selector, the element will be +/// nested inside the parent selector. This means, you may nest elements inside +/// other elements, modifiers or any kind of selector such as &:hover. +/// +/// @param {string} $name - First element name +/// @param {string} $names - More element names +/// +/// @content +/// +/// @throw If the element is not preceded by a block, element, modifier or suffix. +/// +/// @example scss - Element for a block +/// @include iro-bem-component('block') { +/// /* some block definitions */ +/// +/// @include iro-bem-element('elem') { +/// /* some element definitions */ +/// } +/// } +/// +/// // Generates: +/// +/// .c-block { +/// /* some block definitions */ +/// } +/// +/// .c-block__elem { +/// /* some element definitions */ +/// } +/// +/// @example scss - Element that is affected by the user hovering the block +/// @include iro-bem-component('block') { +/// /* some block definitions */ +/// +/// @include iro-bem-element('elem') { +/// background-color: #eee; +/// } +/// +/// &:hover { +/// @include iro-bem-element('elem') { +/// background-color: #000; +/// } +/// } +/// } +/// +/// // Generates: +/// +/// .c-block { +/// /* some block definitions */ +/// } +/// +/// .c-block__elem { +/// background-color: #eee; +/// } +/// +/// .c-block:hover .c-block__elem { +/// background-color: #000; +/// } +/// +/// @example scss - Multiple elements +/// @include iro-bem-component('block') { +/// /* some block definitions */ +/// +/// @include iro-bem-element('elem1', 'elem2') { +/// /* some element definitions */ +/// } +/// } +/// +/// // Generates: +/// +/// .c-block { +/// /* some block definitions */ +/// } +/// +/// .c-block__elem1, .c-block__elem2 { +/// /* some element definitions */ +/// } +/// +@mixin iro-bem-element($name, $names...) { + $result: iro-bem-element($name, $names...); + $selector: nth($result, 1); + $context: nth($result, 2); + + @include iro-bem-validate( + 'element', + (name: $name, names: $names), + $selector, + $context + ); + + @include iro-context-push($iro-bem-context-id, $context...); + @at-root #{$selector} { + @content; + } + @include iro-context-pop($iro-bem-context-id); +} + +/// +/// Generate a new BEM element. Check the respective mixin documentation for more information. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-element +/// +@function iro-bem-element($name, $names...) { + $noop: iro-context-assert-stack-count($iro-bem-context-id, $iro-bem-max-depth); + $noop: iro-context-assert-stack-must-contain($iro-bem-context-id, 'block'); + + $parent-context: iro-context-get($iro-bem-context-id, 'block' 'element'); + + $selector: (); + $parts-data: (); + + @if nth($parent-context, 1) == 'element' { + @if $iro-bem-element-nesting-policy == 'disallow' { + @error 'Element nesting is forbidden.'; + } + + @if $iro-bem-element-nesting-policy == 'append' { + $element-selector: map-get(nth($parent-context, 2), 'selector'); + + @if not iro-selector-suffix-match(&, $element-selector) { + @error 'A nested element must be an immediate children of the parent element.'; + } + + // + // Possible outcomes: + // - {e}__element + // - [manual selector] {e}__element + // + + @each $name in join($name, $names) { + $sel: selector-append(&, $iro-bem-element-separator + $name); + $selector: join($selector, $sel, comma); + $parts-data: append($parts-data, ( + 'name': $name, + 'selector': $sel + )); + } + } + + $parent-context: iro-context-get($iro-bem-context-id, 'block'); + } + + @if length($selector) == 0 { + $parent-selector: map-get(nth($parent-context, 2), 'selector'); + + @if iro-selector-suffix-match(&, $parent-selector) { + // + // Possible outcomes: + // - {b}__element + // - [manual selector] {b}__element + // + + @each $name in join($name, $names) { + $sel: selector-append(&, $iro-bem-element-separator + $name); + $selector: join($selector, $sel, comma); + $parts-data: append($parts-data, ( + 'name': $name, + 'selector': $sel + )); + } + } @else { + // + // Possible outcomes: + // - {b} [manual selector] {b}__element + // - {e,m,s} ([manual selector]) {b}__element + // + + @if nth($parent-context, 1) != 'block' { + $parent-context: iro-context-get($iro-bem-context-id, 'block'); + } + + $block-base-selector: map-get(nth($parent-context, 2), 'base-selector'); + + @each $name in join($name, $names) { + $sel: selector-nest(&, selector-append($block-base-selector, $iro-bem-element-separator + $name)); + $selector: join($selector, $sel, comma); + $parts-data: append($parts-data, ( + 'name': $name, + 'selector': $sel + )); + } + } + } + + $context: 'element', ( + 'parts': $parts-data, + 'selector': $selector + ); + + @return $selector $context; +} + +/// +/// Generate a BEM element that is related to the current element. +/// +/// The generated element selector is appended to the current element selector. The $sign +/// determines the relationship. +/// +/// @param {string} $sign - Relationshop sign, either '+' or '~' +/// @param {string} $name - First element name +/// @param {string} $names - More element names +/// +/// @content +/// +/// @throw If the element is not preceded by an element. +/// +/// @example scss - A sibling element to a single element +/// @include iro-bem-component('block') { +/// @include iro-bem-element('elem') { +/// /* some element definitions */ +/// +/// @include iro-bem-related-element('~', 'sibling') { +/// /* some sibling element definitions */ +/// } +/// } +/// } +/// +/// // Generates: +/// +/// .c-block__elem { +/// /* some element definitions */ +/// } +/// +/// .c-block__elem ~ .c-block__sibling { +/// /* some sibling element definitions */ +/// } +/// +/// @example scss - A successor element to a single element +/// @include iro-bem-component('block') { +/// @include iro-bem-element('elem') { +/// /* some element definitions */ +/// +/// @include iro-bem-related-element('+', 'successor') { +/// /* some successor element definitions */ +/// } +/// } +/// } +/// +/// // Generates: +/// +/// .c-block__elem { +/// /* some element definitions */ +/// } +/// +/// .c-block__elem + .c-block__successor { +/// /* some successor element definitions */ +/// } +/// +/// @example scss - A successor element to multiple elements +/// @include iro-bem-component('block') { +/// @include iro-bem-element('elem1', 'elem2') { +/// /* some element definitions */ +/// +/// @include iro-bem-related-element('+', 'successor') { +/// /* some successor element definitions */ +/// } +/// } +/// } +/// +/// // Generates: +/// +/// .c-block__elem1, .c-block__elem2 { +/// /* some element definitions */ +/// } +/// +/// .c-block__elem1 + .c-block__successor, .c-block__elem2 + .c-block__successor { +/// /* some successor element definitions */ +/// } +/// +@mixin iro-bem-related-element($sign, $name, $names...) { + $result: iro-bem-related-element($sign, $name, $names...); + $selector: nth($result, 1); + $context: nth($result, 2); + + @include iro-bem-validate( + 'related-element', + (sign: $sign, name: $name, names: $names), + $selector, + $context + ); + + @include iro-context-push($iro-bem-context-id, $context...); + @at-root #{$selector} { + @content; + } + @include iro-context-pop($iro-bem-context-id); +} + +/// +/// Generate a new BEM element that is related to the current element. +/// Check the respective mixin documentation for more information. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-related-element +/// +@function iro-bem-related-element($sign, $name, $names...) { + // + // Generating this selector is simple: Take the latest block context, use it + // to generate the element part, and insert it at the end of the current selector. + // Possible outcomes: + // - {e} ({m,s}) ([manual selector]) + {e} + // - {e} ({m,s}) ([manual selector]) ~ {e} + // + + $noop: iro-context-assert-stack-count($iro-bem-context-id, $iro-bem-max-depth); + $noop: iro-context-assert-stack-must-contain($iro-bem-context-id, 'element'); + + @if $sign != '+' and $sign != '~' { + @error 'Invalid relationship sign #{inspect($sign)}.'; + } + + $block-context: iro-context-get($iro-bem-context-id, 'block'); + $block-base-selector: map-get(nth($block-context, 2), 'base-selector'); + + $selector: (); + $parts-data: (); + + @each $name in join($name, $names) { + $sel: selector-nest(&, $sign, selector-append($block-base-selector, $iro-bem-element-separator + $name)); + $selector: join($selector, $sel, comma); + $parts-data: append($parts-data, ( + 'name': $name, + 'selector': $sel + )); + } + + $context: 'element', ( + 'parts': $parts-data, + 'selector': $selector + ); + + @return $selector $context; +} + +/// +/// Generate a BEM element that is a sibling of the current element. +/// +/// It's a shorthand for iro-bem-related-element('~', $name). +/// +/// @param {string} $name - First element name +/// @param {list} $names - List of more element names +/// +/// @content +/// +@mixin iro-bem-sibling-element($name, $names...) { + @include iro-bem-related-element('~', $name, $names...) { + @content; + } +} + +/// +/// Generate a new BEM element that is a sibling of the current element. +/// Check the respective mixin documentation for more information. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-sibling-element +/// +@function iro-bem-sibling-element($name, $names...) { + @return iro-bem-related-element('~', $name, $names...); +} + +/// +/// Generate a BEM element that is the successor of the current element. +/// +/// It's a shorthand for iro-bem-related-element('+', $name). +/// +/// @param {string} $name - First element name +/// @param {string} $names - More element names +/// +/// @content +/// +@mixin iro-bem-next-element($name, $names...) { + @include iro-bem-related-element('+', $name, $names...) { + @content; + } +} + +/// +/// Generate a new BEM element that is the successor of the current element. +/// Check the respective mixin documentation for more information. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-next-element +/// +@function iro-bem-next-element($name, $names...) { + @return iro-bem-related-element('+', $name, $names...); +} + +/// +/// Generate the current BEM element as a successor of itself. +/// +/// If this is applied to a single element, it behaves exactly the same as +/// iro-bem-related-element('+', name); +/// However, if it is applied to multiple elements, each twin element only will influence +/// their other twin, which is not replicable with iro-bem-related-element. +/// +/// @content +/// +/// @example scss - Two twin elements +/// @include iro-bem-component('block') { +/// @include iro-bem-element('elem') { +/// /* some element definitions */ +/// +/// @include iro-bem-next-twin-element { +/// /* some twin element definitions */ +/// } +/// } +/// } +/// +/// // Generates: +/// +/// .c-block__elem { +/// /* some element definitions */ +/// } +/// +/// .c-block__elem + .c-block__elem { +/// /* some twin element definitions */ +/// } +/// +/// @example scss - Multiple twin elements +/// @include iro-bem-component('block') { +/// @include iro-bem-element('elem1', 'elem2') { +/// /* some element definitions */ +/// +/// @include iro-bem-next-twin-element { +/// /* some twin element definitions */ +/// } +/// } +/// } +/// +/// // Generates: +/// +/// .c-block__elem1, .c-block__elem2 { +/// /* some element definitions */ +/// } +/// +/// .c-block__elem1 + .c-block__elem1, .c-block__elem2 + .c-block__elem2 { +/// /* some twin element definitions */ +/// } +/// +@mixin iro-bem-related-twin-element($sign) { + $result: iro-bem-related-twin-element($sign); + $selector: nth($result, 1); + $context: nth($result, 2); + + @include iro-bem-validate( + 'next-twin-element', + (), + $selector, + $context + ); + + @include iro-context-push($iro-bem-context-id, $context...); + @at-root #{$selector} { + @content; + } + @include iro-context-pop($iro-bem-context-id); +} + +/// +/// Generate the current BEM element as a successor of itself. +/// Check the respective mixin documentation for more information. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-next-twin-element +/// +@function iro-bem-related-twin-element($sign) { + $noop: iro-context-assert-stack-count($iro-bem-context-id, $iro-bem-max-depth); + $noop: iro-context-assert-stack-must-contain($iro-bem-context-id, 'element'); + + $element-context: iro-context-get($iro-bem-context-id, 'element'); + $element-selector: map-get(nth($element-context, 2), 'selector'); + + $block-context: iro-context-get($iro-bem-context-id, 'block'); + $block-base-selector: map-get(nth($block-context, 2), 'base-selector'); + + $selector: (); + $parts-data: (); + + // + // To determine the twin for each element, iterate the sub-selectors from the current selector + // and check if it contains the currently inspected element. This has to be done with string + // comparison since none of Sass selector functions is of use here. + // Finally, the current twin will be appended to the extracted sub-selector as a successor + // element. + // + @each $part-data in map-get(nth($element-context, 2), 'parts') { + $part-selector: map-get($part-data, 'selector'); + $part-name: map-get($part-data, 'name'); + + $sel: (); + @if iro-selector-suffix-match(&, $element-selector) { + // + // This mixin is included in the selector the last element mixin created. + // Possible outcomes: + // - {e} + {e} + // - [manual selector] {e} + {e} + // + + @each $s in & { + @each $ps in $part-selector { + @if nth($s, -1) == nth($ps, -1) { + $sel-ent: selector-nest($s, $sign, selector-append($block-base-selector, $iro-bem-element-separator + $part-name)); + $sel: join($sel, $sel-ent, comma); + } + } + } + } @else { + // + // This mixin is NOT included in the selector the last element mixin created. + // Possible outcomes: + // - {e} {m,s} + {e} + // - {e} [manual selector] + {e} + // - {e} {m,s} [manual selector] + {e} + // + + @each $s in & { + @each $ps in $part-selector { + @if str-index(inspect($s), inspect($ps)) { + $char-index: str-length(inspect($ps)) + 1; + $match: index(' ' ':' ',', str-slice(inspect($s), $char-index, $char-index)) != null; + + @if not $match { + @each $separator in $iro-bem-element-separator $iro-bem-modifier-separator $iro-bem-suffix-separator { + @if str-slice(inspect($s), $char-index, $char-index + str-length($separator) - 1) == $separator { + $match: true; + } + } + } + + @if $match { + $sel-ent: selector-nest($s, '+', selector-append($block-base-selector, $iro-bem-element-separator + $part-name)); + $sel: join($sel, $sel-ent, comma); + } + } + } + } + } + @if length($sel) != length($part-selector) { + @error 'Could not generate twin element selector.'; + } + + $selector: join($selector, $sel, comma); + $parts-data: append($parts-data, ( + 'name': $part-name, + 'selector': $sel + )); + } + + $context: 'element', ( + 'parts': $parts-data, + 'selector': $selector + ); + + @return $selector $context; +} + +/// +/// Generate the current BEM element as a sibling of itself. +/// +/// It's a shorthand for iro-bem-related-twin-element('~'). +/// +/// @content +/// +@mixin iro-bem-sibling-twin-element { + @include iro-bem-related-twin-element('~') { + @content; + } +} + +/// +/// Generate the current BEM element as a sibling of itself. +/// Check the respective mixin documentation for more information. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-sibling-twin-element +/// +@function iro-bem-sibling-twin-element() { + @return iro-bem-related-twin-element('~'); +} + +/// +/// Generate the current BEM element as a next sibling of itself. +/// +/// It's a shorthand for iro-bem-related-twin-element('+', $name). +/// +/// @content +/// +@mixin iro-bem-next-twin-element { + @include iro-bem-related-twin-element('+') { + @content; + } +} + +/// +/// Generate the current BEM element as a next sibling of itself. +/// Check the respective mixin documentation for more information. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-next-twin-element +/// +@function iro-bem-next-twin-element() { + @return iro-bem-related-twin-element('+'); +} diff --git a/src/bem/_functions.scss b/src/bem/_functions.scss new file mode 100644 index 0000000..4bb95c4 --- /dev/null +++ b/src/bem/_functions.scss @@ -0,0 +1,26 @@ +//// +/// @group BEM +/// +/// @access public +//// + +/// +/// @access private +/// +@function iro-bem-theme-selector($name, $names...) { + $namespace: map-get($iro-bem-namespaces, 'theme'); + $selector: null; + + @each $name in join($name, $names) { + $sel: '.' + $namespace + '-' + $name; + + @if $selector == null { + $selector: join(selector-parse($sel), selector-parse('[class*=\' t-\'] ' + $sel), comma); + $selector: join($selector, selector-parse('[class^=\'t-\'] ' + $sel), comma); + } @else { + $selector: selector-nest($selector, $sel); + } + } + + @return $selector; +} diff --git a/src/bem/_modifier.scss b/src/bem/_modifier.scss new file mode 100644 index 0000000..e1f9507 --- /dev/null +++ b/src/bem/_modifier.scss @@ -0,0 +1,246 @@ +//// +/// @group BEM +/// +/// @access public +//// + +/// +/// Generate a new BEM modifier. +/// +/// If the parent context is block or element, the modifier will modify said block or element according +/// to the BEM naming convention. +/// +/// If the parent context is a modifier or suffix, then the modifier will depend on said modifier or suffix. +/// Depending on $extend, the meaning of this dependency (and the resulting selector) varies: +/// If it's false (default), you signalize that the modifier also exists by itself, but it changes its +/// behavior when the parent modifier or suffix is set. +/// If it's true, you signalize that the modifier extends the parent modifier or suffix and can only be +/// used in conjunction with it. +/// +/// @param {string | list} $name - First element name or list with two items: 1. first element name, 2. bool indicating if the modifier is extending +/// @param {string | list} $names - More element names or lists with two items: 1. element name, 2. bool indicating if the modifier is extending +/// +/// @content +/// +/// @throw If the element is not preceded by a block, element, modifier or suffix. +/// +/// @example scss - Modifier that modifies a block or element +/// @include iro-bem-component('block') { +/// @include iro-bem-modifier('mod') { +/// background-color: #eee; +/// } +/// +/// @include iro-bem-element('elem') { +/// @include iro-bem-modifier('mod') { +/// background-color: #222; +/// } +/// } +/// } +/// +/// // Generates: +/// +/// .c-block--mod { +/// background-color: #eee; +/// } +/// +/// .c-block__elem--mod { +/// background-color: #222; +/// } +/// +/// @example scss - Modifier nested in another modifier, not extending +/// @include iro-bem-component('block') { +/// @include iro-bem-modifier('mod') { +/// background-color: #eee; +/// } +/// +/// @include iro-bem-modifier('dark') { +/// /* some definitions */ +/// +/// @include iro-bem-modifier('mod') { +/// background-color: #222; +/// } +/// } +/// } +/// +/// // Generates: +/// +/// .c-block--mod { +/// background-color: #eee; +/// } +/// +/// .c-block--dark { +/// /* some definitions */ +/// } +/// +/// .c-block--dark.c-block--mod { +/// background-color: #222; +/// } +/// +/// @example scss - Modifier nested in another modifier, extending +/// @include iro-bem-component('block') { +/// @include iro-bem-modifier('mod') { +/// background-color: #eee; +/// } +/// +/// @include iro-bem-modifier('dark') { +/// /* some definitions */ +/// +/// @include iro-bem-modifier('mod' true) { +/// background-color: #222; +/// } +/// } +/// } +/// +/// // Generates: +/// +/// .c-block--mod { +/// background-color: #eee; +/// } +/// +/// .c-block--dark { +/// /* some definitions */ +/// } +/// +/// .c-block--dark--mod { +/// background-color: #222; +/// } +/// +@mixin iro-bem-modifier($name, $names...) { + $result: iro-bem-modifier($name, $names...); + $selector: nth($result, 1); + $context: nth($result, 2); + + @include iro-bem-validate( + 'modifier', + (name: $name, names: $names), + $selector, + $context + ); + + @include iro-context-push($iro-bem-context-id, $context...); + @at-root #{$selector} { + @content; + } + @include iro-context-pop($iro-bem-context-id); +} + +/// +/// Generate a new BEM modifier. Check the respective mixin documentation for more information. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-modifier +/// +@function iro-bem-modifier($name, $names...) { + $noop: iro-context-assert-stack-count($iro-bem-context-id, $iro-bem-max-depth); + $noop: iro-context-assert-stack-must-contain($iro-bem-context-id, 'block'); + + $parent-context: iro-context-get($iro-bem-context-id, 'block' 'element' 'modifier' 'suffix'); + $parent-selector: map-get(nth($parent-context, 2), 'selector'); + $selector: (); + $parts-data: (); + + @if not iro-selector-suffix-match(&, $parent-selector) { + // + // The current selector doesn't match the parent selector. + // The user manually added a selector between parent context and this modifier call. + // This case is forbidden because any outcome semantically wouldn't make sense: + // - {b,e,m,s} [manual selector] {b,e,m,s}--modifier + // - {b,e,m,s}--modifier [manual selector] + // The first case would make the modifier behave like an element. + // The second case is unintuitive, the code would be more clear by nesting the manual + // selector in the modifier instead. + // + + @error 'A modifier must be an immediate child of the parent context'; + } + + @each $name in iro-list-prepend($names, $name) { + $extend: false; + @if type-of($name) == list { + $extend: nth($name, 2); + $name: nth($name, 1); + } + + @if index('block' 'element', nth($parent-context, 1)) or $extend == true { + // + // Either the parent context is block or element, or a modifier or suffix + // is to be extended. The modifier part can simply be appended. + // Possible outcomes: + // - {b,e,m,s}--modifier + // + + $sel: selector-append(&, $iro-bem-modifier-separator + $name); + $selector: join($selector, $sel, comma); + $parts-data: append($parts-data, ( + 'name': $name, + 'selector': $sel + )); + } @else { + // + // Parent context is modifier, suffix or state and $extend is false. + // + + $be-context: iro-context-get($iro-bem-context-id, 'block' 'element'); + + @if nth($be-context, 1) == 'element' { + // + // Latest context is element. Since element contexts can consist of multiple single + // elements, inspect all elements and append its selector with the suffix "--$name". + // This has to be done with string comparison since none of Sass selector functions + // is of use here. + // Possible outcomes: + // - {m,s}.{e}--modifier + // + + $nsel: (); + + @each $elem-part-data in map-get(nth($be-context, 2), 'parts') { + $elem-part-selector: map-get($elem-part-data, 'selector'); + + $sel: (); + @each $s in & { + @each $ps in $elem-part-selector { + @if str-index(inspect($s), inspect($ps) + $iro-bem-modifier-separator) or str-index(inspect($s), inspect($ps) + $iro-bem-suffix-separator) { + $sel: join($sel, selector-unify($s, selector-append($ps, $iro-bem-modifier-separator + $name)), comma); + } + } + } + @if length($sel) == 0 { + @error 'Could not generate modifier selector.'; + } + + $nsel: join($nsel, $sel, comma); + } + + $selector: join($selector, $nsel, comma); + $parts-data: append($parts-data, ( + 'name': $name, + 'selector': $nsel + )); + } @else { + // + // Latest context is block. Just append the modifier part. + // Possible outcomes: + // - {m,s}.{b}--modifier + // + + $block-base-selector: map-get(nth($be-context, 2), 'base-selector'); + + $sel: selector-append(&, $block-base-selector, $iro-bem-modifier-separator + $name); + $selector: join($selector, $sel, comma); + $parts-data: append($parts-data, ( + 'name': $name, + 'selector': $sel + )); + } + } + } + + $context: 'modifier', ( + 'parts': $parts-data, + 'selector': $selector + ); + + @return $selector $context; +} diff --git a/src/bem/_multi.scss b/src/bem/_multi.scss new file mode 100644 index 0000000..9e47ce4 --- /dev/null +++ b/src/bem/_multi.scss @@ -0,0 +1,131 @@ +//// +/// @group BEM +/// +/// @access public +//// + +/// +/// Generate multiple entities (BEM or not) at once. +/// +/// NOTE: This mixin does not generate perfectly optimized selectors in order to keep track of contexts. +/// +/// @param {string | list} $first - First selector. Either a string for a manual selector, or a list with the first items standing for a BEM selector function (optionally suffixed by a colon) and other items being passed as arguments to said function. +/// @param {string | list} $others - Other selectors. Either a string for a manual selector, or a list with the first items standing for a BEM selector function (optionally suffixed by a colon) and other items being passed as arguments to said function. +/// +/// @content +/// +/// @example scss - Creating multiple elements, a modifier and an anchor +/// @include iro-bem-object('buttonstrip') { +/// display: none; +/// +/// @include iro-bem-multi('modifier' 'mod', 'element' 'button' 'separator', '> a') { +/// display: block; +/// } +/// } +/// +/// // Generates: +/// +/// .o-buttonstrip { +/// display: none; +/// } +/// +/// .o-buttonstrip--mod { +/// display: block; +/// } +/// +/// .o-buttonstrip__button, { +/// .o-buttonstrip__separator { +/// display: block; +/// } +/// +/// .o-buttonstrip > a { +/// display: block; +/// } +/// +/// @example scss - Creating multiple elements, a modifier and an anchor - optional colons included +/// @include iro-bem-object('buttonstrip') { +/// display: none; +/// +/// @include iro-bem-multi('modifier:' 'mod', 'element:' 'button' 'separator', '> a') { +/// display: block; +/// } +/// } +/// +/// // Generates: +/// +/// .o-buttonstrip { +/// display: none; +/// } +/// +/// .o-buttonstrip--mod { +/// display: block; +/// } +/// +/// .o-buttonstrip__button, { +/// .o-buttonstrip__separator { +/// display: block; +/// } +/// +/// .o-buttonstrip > a { +/// display: block; +/// } +/// +@mixin iro-bem-multi($first, $others...) { + @include iro-context-assert-stack-count($iro-bem-context-id, $iro-bem-max-depth); + + @each $entity in iro-list-prepend($others, $first) { + $is-manual-selector: false; + + @if type-of($entity) == string and not function-exists('iro-bem-' + $entity) { + $is-manual-selector: true; + } + + @if $is-manual-selector { + $sel: if(&, selector-nest(&, $entity), selector-parse($entity)); + + @at-root #{$sel} { + @content; + } + } @else { + $entity-func-id: null; + + @if type-of($entity) == list { + $entity-func-id: nth($entity, 1); + $entity: iro-list-slice($entity, 2); + } @else { + $entity-func-id: $entity; + $entity: (); + } + + @if str-slice($entity-func-id, str-length($entity-func-id)) == ':' { + $entity-func-id: str-slice($entity-func-id, 1, str-length($entity-func-id) - 1); + } + + $sel-func: null; + + @if function-exists('iro-bem-' + $entity-func-id) { + $sel-func: get-function('iro-bem-' + $entity-func-id); + } @else if function-exists($entity-func-id) { + $sel-func: get-function($entity-func-id); + } + + @if $sel-func == null { + @error 'Function "#{inspect($entity-func-id)}" was not found.'; + } + + $entity-result: call($sel-func, $entity...); + $entity-result-selector: nth($entity-result, 1); + $entity-result-context: nth($entity-result, 2); + + @if $entity-result-context != null { + @include iro-context-push($iro-bem-context-id, $entity-result-context...); + } + @at-root #{$entity-result-selector} { + @content; + } + @if $entity-result-context != null { + @include iro-context-pop($iro-bem-context-id); + } + } + } +} diff --git a/src/bem/_state.scss b/src/bem/_state.scss new file mode 100644 index 0000000..4a85bbb --- /dev/null +++ b/src/bem/_state.scss @@ -0,0 +1,146 @@ +//// +/// @group BEM +/// +/// @access public +//// + +/// +/// Create a new state rule. +/// +/// @param {string} $state - First state name +/// @param {list} $states - List of more state names +/// +/// @content +/// +/// @example scss - Using single is-state +/// @include iro-bem-object('menu') { +/// display: none; +/// +/// @include iro-bem-state('is', open') { +/// display: block; +/// } +/// } +/// +/// // Generates: +/// +/// .o-menu { +/// display: none; +/// } +/// +/// .o-menu.is-open { +/// display: block; +/// } +/// +/// @example scss - Using multiple is-states +/// @include iro-bem-object('menu') { +/// display: none; +/// +/// @include iro-bem-state('is', open', 'visible') { +/// display: block; +/// } +/// } +/// +/// // Generates: +/// +/// .o-menu { +/// display: none; +/// } +/// +/// .o-menu.is-open, +/// .o-menu.is-visible { +/// display: block; +/// } +/// +@mixin iro-bem-state($prefix, $state, $states...) { + $result: iro-bem-state($prefix, $state, $states...); + $selector: nth($result, 1); + $context: nth($result, 2); + + @include iro-bem-validate( + 'state', + (prefix: $prefix, state: $state, states: $states), + $selector, + $context + ); + + @include iro-context-push($iro-bem-context-id, $context...); + @at-root #{$selector} { + @content; + } + @include iro-context-pop($iro-bem-context-id); +} + +/// +/// Generate a new state. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-has +/// +@function iro-bem-state($prefix, $state, $states...) { + $selector: (); + $parts-data: (); + + @each $state in join($state, $states) { + $sel: selector-parse('.#{$prefix}-#{$state}'); + @if & { + $sel: selector-append(&, $sel); + } + $selector: join($selector, $sel, comma); + $parts-data: append($parts-data, ( + 'name': $state, + 'selector': $sel + )); + } + + $context: 'state', ( + 'parts': $parts-data, + 'selector': $selector + ); + + @return $selector $context; +} + +/// +/// Create a new has-state modifier. +/// +/// It's a shorthand for iro-bem-state('is', $state, $states...). +/// +@mixin iro-bem-is($state, $states...) { + @include iro-bem-state('is', $state, $states...) { + @content; + } +} + +/// +/// Generate a new is-state modifier. Check the respective mixin documentation for more information. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-is +/// +@function iro-bem-is($state, $states...) { + @return iro-bem-state('is', $state, $states...); +} + +/// +/// Create a new has-state modifier. +/// +/// It's a shorthand for iro-bem-state('has', $state, $states...). +/// +@mixin iro-bem-has($state, $states...) { + @include iro-bem-state('has', $state, $states...) { + @content; + } +} + +/// +/// Generate a new has-state modifier. Check the respective mixin documentation for more information. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-has +/// +@function iro-bem-has($state, $states...) { + @return iro-bem-state('has', $state, $states...); +} diff --git a/src/bem/_suffix.scss b/src/bem/_suffix.scss new file mode 100644 index 0000000..b103c9f --- /dev/null +++ b/src/bem/_suffix.scss @@ -0,0 +1,118 @@ +//// +/// @group BEM +/// +/// @access public +//// + +/// +/// Generate a new suffix. +/// +/// @param {string} $name - Suffix name +/// +/// @content +/// +/// @throw If the element is not preceded by a block or modifier. +/// +/// @example scss - Using a suffix +/// @include iro-bem-utility('hidden') { +/// display: none; +/// +/// @media (max-width: 320px) { +/// @include iro-bem-suffix('phone') { +/// display: none; +/// } +/// } +/// +/// @media (max-width: 768px) { +/// @include iro-bem-suffix('tablet') { +/// display: none; +/// } +/// } +/// } +/// +/// // Generates: +/// +/// .u-hidden { +/// display: none; +/// } +/// +/// @media (max-width: 320px) { +/// .u-hidden@phone { +/// display: none; +/// } +/// } +/// +/// @media (max-width: 768px) { +/// .u-hidden@tablet { +/// display: none; +/// } +/// } +/// +@mixin iro-bem-suffix($name) { + $result: iro-bem-suffix($name); + $selector: nth($result, 1); + $context: nth($result, 2); + + @include iro-bem-validate( + 'suffix', + (name: $name), + $selector, + $context + ); + + @include iro-context-push($iro-bem-context-id, $context...); + @at-root #{$selector} { + @content; + } + @include iro-context-pop($iro-bem-context-id); +} + +/// +/// Generate a new suffix. Check the respective mixin documentation for more information. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-suffix +/// +@function iro-bem-suffix($name) { + // + // Suffixes can be used on block, element and modifier. + // + + $noop: iro-context-assert-stack-count($iro-bem-context-id, $iro-bem-max-depth); + $noop: iro-context-assert-stack-must-contain($iro-bem-context-id, 'block'); + $noop: iro-context-assert-stack-must-not-contain($iro-bem-context-id, 'suffix'); + + $parent-context: iro-context-get($iro-bem-context-id, 'block' 'element' 'modifier'); + $parent-selector: map-get(nth($parent-context, 2), 'selector'); + + @if not iro-selector-suffix-match(&, $parent-selector) { + // + // The current selector doesn't match the parent selector. + // The user manually added a selector between parent context and this suffix call. + // This case is forbidden because any outcome semantically wouldn't make sense: + // - {b,e,m} [manual selector] {b,e,m}@suffix + // - {b,e,m}@suffix [manual selector] + // The first case would make the modifier behave like an element. + // The second case is unintuitive, the code would be more clear by nesting the manual + // selector in the suffix instead. + // + + @error 'A suffix must be an immediate child of the parent context'; + } + + // + // The suffix part can simply be appended. + // Possible outcomes: + // - {b,e,m}@suffix + // + + $selector: selector-append(&, $iro-bem-suffix-separator + $name); + + $context: 'suffix', ( + 'name': $name, + 'selector': $selector + ); + + @return $selector $context; +} diff --git a/src/bem/_theme.scss b/src/bem/_theme.scss new file mode 100644 index 0000000..b4bcc76 --- /dev/null +++ b/src/bem/_theme.scss @@ -0,0 +1,61 @@ +//// +/// @group BEM +/// +/// @access public +//// + +/// +/// Declare new rules for the current block for when this theme is active. +/// +/// @param {string} $name - First theme block name +/// @param {string} $names - More theme block names +/// +/// @content +/// +@mixin iro-bem-at-theme($name, $names...) { + $result: iro-bem-at-theme($name, $names...); + $selector: nth($result, 1); + $context: nth($result, 2); + + @include iro-bem-validate( + 'at-theme', + (name: $name, names: $names), + $selector, + $context + ); + + @include iro-context-push($iro-bem-context-id, $context...); + @at-root #{$selector} { + @content; + } + @include iro-context-pop($iro-bem-context-id); +} + +/// +/// Declare new rules for the current block for when this theme is active. +/// Check the respective mixin documentation for more information. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-at-theme +/// +@function iro-bem-at-theme($name, $names...) { + $noop: iro-context-assert-stack-must-contain($iro-bem-context-id, 'block'); + + $parent-context: iro-context-get($iro-bem-context-id, 'block'); + $parent-selector: map-get(nth($parent-context, 2), 'selector'); + + @if not iro-selector-suffix-match(&, $parent-selector) { + @error 'An at-theme rule must be an immediate child of a block'; + } + + $selector: iro-bem-theme-selector($name, $names...); + $selector: selector-nest($selector, &); + + $context: 'at-theme', ( + 'name': join($name, $names), + 'selector': $selector + ); + + @return $selector $context; +} diff --git a/src/bem/_validators.scss b/src/bem/_validators.scss new file mode 100644 index 0000000..eb09a60 --- /dev/null +++ b/src/bem/_validators.scss @@ -0,0 +1,176 @@ +//// +/// Validators are custom functions that will be called before a BEM entity is created. +/// They check if the current mixin usage is valid or not and thus they are a flexible way to +/// let you implement your own rules. +/// +/// Validator functions receive the following information: +/// - BEM entity type +/// - Arguments passed to the mixin +/// - The generated selector +/// - The generated context, if any +/// +/// Additionally, the context stack used by the BEM system can be examined. +/// +/// @group BEM +/// +/// @access public +//// + +/// +/// A list of validator functions. +/// +/// @type list +/// +/// @access private +/// +$iro-bem-validators: (); + +/// +/// Register one or multiple validator functions. +/// +/// A validator function is a function that accepts 4 arguments: +/// 1. BEM entity type (string) +/// 2. Arguments passed to the mixin (map) +/// 3. The generated selector (selector) +/// 4. The generated context (list, may be null) +/// +/// The function must return a list with two items: +/// 1. `true` if the mixin usage is valid, otherwise `false`, +/// 2. a string with a rejection reason (empty if the usage is valid). +/// +/// @param {string} $func-name - First function name. +/// @param {string} $func-names - Other function names. +/// +@mixin iro-bem-add-validator($func-name, $func-names...) { + $noop: iro-bem-add-validator($func-name, $func-names...); +} + +/// +/// Register one or multiple validator functions. Check the respective mixin documentation for more information. +/// +/// @see {mixin} iro-bem-add-validator +/// +@function iro-bem-add-validator($func-name, $func-names...) { + @each $fn-name in join($func-name, $func-names) { + $fn: get-function($fn-name); + $iro-bem-validators: map-merge($iro-bem-validators, ($fn-name: $fn)) !global; + } + @return null; +} + +/// +/// Unregister one or multiple validator functions. +/// +/// @param {string} $func-name - First function name. +/// @param {string} $func-names - Other function names. +/// +@mixin iro-bem-remove-validator($func-name, $func-names...) { + $noop: iro-bem-remove-validator($func-name, $func-names...); +} + +/// +/// Unregister one or multiple validator functions. Check the respective mixin documentation for more information. +/// +/// @see {mixin} iro-bem-remove-validator +/// +@function iro-bem-remove-validator($func-name, $func-names...) { + $iro-bem-validators: map-remove($iro-bem-validators, $func-name, $func-names...) !global; + @return null; +} + +/// +/// @access private +/// +@mixin iro-bem-validate($type, $args, $selector, $context) { + @each $id, $fn in $iro-bem-validators { + $result: call($fn, $type, $args, $selector, $context); + @if not nth($result, 1) { + @error 'A BEM validator rejected this mixin usage due to the following reason: #{nth($result, 2)}'; + } + } +} + +// +// --------------------------------------------------------------------------------------------------------- +// Built-in validators +// --------------------------------------------------------------------------------------------------------- +// + +/// +/// A validator that makes sure blocks are declared in the right order, determined by the +/// namespace used. +/// +@function iro-bem-validator--enforce-namespace-order($type, $args, $selector, $context) { + @if not global-variable-exists(iro-bem-namespace-order) { + $iro-bem-namespace-order: map-keys($iro-bem-namespaces) !global; + } + @if not global-variable-exists(iro-bem-cur-namespace-index) { + $iro-bem-cur-namespace-index: 1 !global; + } + + @if $type == 'block' { + $block-type: map-get($args, 'type'); + + @if $block-type != null { + $ns-index: index($iro-bem-namespace-order, $block-type); + + @if $ns-index != null { + @if $ns-index < $iro-bem-cur-namespace-index { + @return false 'Namespace "#{$block-type}" comes before current namespace #{nth($iro-bem-namespace-order, $iro-bem-cur-namespace-index)}'; + } + + $iro-bem-cur-namespace-index: $ns-index !global; + } + } + } + + @return true ''; +} + +/// +/// A validator that makes all BEM entities immutable. +/// +@function iro-bem-validator--immutable-entities($type, $args, $selector, $context) { + @if not global-variable-exists(iro-bem-generated-selectors) { + $iro-bem-generated-selectors: () !global; + } + + $block-name: null; + $block-type: null; + $block-id: null; + + @if $type == 'block' { + $block-name: map-get($args, 'name'); + $block-type: map-get($args, 'type'); + } @else { + $block-context: iro-context-get($iro-bem-context-id, 'block'); + $block-name: map-get(nth($block-context, 2), 'name'); + $block-type: map-get(nth($block-context, 2), 'type'); + } + + @if $block-type != null { + $block-id: $block-name + '_' + $block-type; + } @else { + $block-id: $block-name; + } + + @if $type == 'block' { + @if map-has-key($iro-bem-generated-selectors, $block-id) { + @return false 'Entity "#{$type}" with arguments [ #{iro-map-print($args)} ] was already defined.'; + } + + $iro-bem-generated-selectors: map-merge($iro-bem-generated-selectors, ($block-id: ())) !global; + } @else { + $selectors: map-get($iro-bem-generated-selectors, $block-id); + + @if index($selectors, $selector) { + @return false 'Entity "#{$type}" with arguments [ #{iro-map-print($args)} ] was already defined.'; + } + + $selectors: append($selectors, $selector); + + $iro-bem-generated-selectors: map-merge($iro-bem-generated-selectors, ($block-id: $selectors)) !global; + } + + @return true ''; +} diff --git a/src/bem/_vars.scss b/src/bem/_vars.scss new file mode 100644 index 0000000..5942d4f --- /dev/null +++ b/src/bem/_vars.scss @@ -0,0 +1,108 @@ +//// +/// @group BEM +/// +/// @access public +//// + +/// +/// Separating character sequence for elements. +/// +/// @type string +/// +$iro-bem-element-separator: '__' !default; + +/// +/// Separating character sequence for modifiers. +/// +/// @type string +/// +$iro-bem-modifier-separator: '--' !default; + +/// +/// Separating character sequence for BEMIT suffixes. +/// +/// @type string +/// +$iro-bem-suffix-separator: '\\@' !default; + +/// +/// Prefixes for all BEMIT namespaces. +/// +/// @prop {string} utility ['u'] - Utility namespace +/// @prop {string} object ['o'] - Object namespace +/// @prop {string} component ['c'] - Component namespace +/// @prop {string} layout ['l'] - Layout namespace +/// @prop {string} scope ['s'] - Scope namespace +/// @prop {string} theme ['t'] - Theme namespace +/// @prop {string} js ['js'] - JS namespace +/// @prop {string} qa ['qa'] - QA namespace +/// @prop {string} hack ['_'] - Hack namespace +/// +/// @type map +/// +$iro-bem-namespaces: ( + object: 'o', + component: 'c', + layout: 'l', + scope: 's', + theme: 't', + utility: 'u', + js: 'js', + qa: 'qa', + hack: '_' +) !default; + +/// +/// A list of all generated blocks. +/// +/// @type list +/// +/// @access private +/// +$iro-bem-blocks: (); + +/// +/// Maximum nesting depth of BEM mixins. The large default value means there is no effective limit. +/// +/// @type number +/// +$iro-bem-max-depth: 99 !default; + +/// +/// Indicates how nested elements should be handled. +/// +/// 'allow' means elements will be nested, i.e. the result will be {e} {b}__element. +/// 'disallow' means an error will be emitted. +/// 'append' means the element name will be appended to the parent element, i.e. the result will be {e}__element. +/// Any other value is treated as 'allow'. +/// +/// @type string +/// +$iro-bem-element-nesting-policy: 'allow' !default; + +/// +/// Context ID used for all BEM-related mixins. +/// +/// @type string +/// +$iro-bem-context-id: 'bem' !default; + +/// +/// Debug mode. +/// +/// @type bool +/// +$iro-bem-debug: false !default; + +/// +/// Colors assigned to namespaces. +/// +/// @type map +/// +$iro-bem-debug-colors: ( + object: #ffa500, + component: #00f, + layout: #ff0, + utility: #008000, + hack: #f00 +) !default; diff --git a/src/harmony-shortcodes.scss b/src/harmony-shortcodes.scss new file mode 100644 index 0000000..3307a7e --- /dev/null +++ b/src/harmony-shortcodes.scss @@ -0,0 +1,35 @@ +//// +/// Shorter version of the harmony-related functions. Useful to reduce clutter. +/// +/// @group Harmony shortcodes +/// +/// @access public +//// + +/// +/// @alias iro-harmony-modular-scale +/// +@function modular-scale($times, $base, $ratio) { + @include iro-harmony-modular-scale($times, $base, $ratio); +} + +/// +/// @alias modular-scale +/// +@function ms($times, $base, $ratio) { + @include modular-scale($times, $base, $ratio); +} + +/// +/// @alias iro-responsive-modular-scale +/// +@mixin responsive-modular-scale($prop, $times, $responsive-map) { + @include iro-responsive-modular-scale($prop, $times, $responsive-map); +} + +/// +/// @alias responsive-modular-scale +/// +@mixin responsive-ms($prop, $times, $responsive-map) { + @include responsive-modular-scale($prop, $times, $responsive-map); +} diff --git a/src/main.scss b/src/main.scss new file mode 100644 index 0000000..5b1b3d1 --- /dev/null +++ b/src/main.scss @@ -0,0 +1,10 @@ +@import 'functions'; +@import 'math'; +@import 'vars'; +@import 'contexts'; +@import 'bem'; +@import 'props'; +@import 'harmony'; +@import 'responsive'; +@import 'easing'; +@import 'gradients'; diff --git a/src/prep.scss b/src/prep.scss new file mode 100644 index 0000000..616165d --- /dev/null +++ b/src/prep.scss @@ -0,0 +1,2 @@ +@import 'functions'; +@import 'math'; diff --git a/src/responsive-shortcodes.scss b/src/responsive-shortcodes.scss new file mode 100644 index 0000000..e43dfc0 --- /dev/null +++ b/src/responsive-shortcodes.scss @@ -0,0 +1,14 @@ +//// +/// Shorter version of the responsive-related mixins. Useful to reduce clutter. +/// +/// @group Responsive shortcodes +/// +/// @access public +//// + +/// +/// @alias iro-responsive-property +/// +@mixin responsive($props, $responsive-map, $fluid: true, $vertical: false) { + @include iro-responsive-property($props, $responsive-map, $fluid, $vertical); +} -- cgit v1.2.3-70-g09d2