diff options
author | Feuerfuchs <git@feuerfuchs.dev> | 2020-11-01 20:55:14 +0100 |
---|---|---|
committer | Feuerfuchs <git@feuerfuchs.dev> | 2020-11-01 20:55:14 +0100 |
commit | d07f664450ddaaebb44127a4bd057763d13d3f82 (patch) | |
tree | 234cfd673ac527869a8dda4f32afbec48c87b512 /src | |
download | iro-sass-d07f664450ddaaebb44127a4bd057763d13d3f82.tar.gz iro-sass-d07f664450ddaaebb44127a4bd057763d13d3f82.tar.bz2 iro-sass-d07f664450ddaaebb44127a4bd057763d13d3f82.zip |
Init
Diffstat (limited to 'src')
-rw-r--r-- | src/_bem.scss | 62 | ||||
-rw-r--r-- | src/_contexts.scss | 315 | ||||
-rw-r--r-- | src/_easing.scss | 483 | ||||
-rw-r--r-- | src/_functions.scss | 328 | ||||
-rw-r--r-- | src/_gradients.scss | 600 | ||||
-rw-r--r-- | src/_harmony.scss | 94 | ||||
-rw-r--r-- | src/_math.scss | 62 | ||||
-rw-r--r-- | src/_props.scss | 281 | ||||
-rw-r--r-- | src/_responsive.scss | 406 | ||||
-rw-r--r-- | src/_vars.scss | 16 | ||||
-rw-r--r-- | src/bem-shortcodes.scss | 349 | ||||
-rw-r--r-- | src/bem/_block.scss | 392 | ||||
-rw-r--r-- | src/bem/_debug.scss | 16 | ||||
-rw-r--r-- | src/bem/_element.scss | 622 | ||||
-rw-r--r-- | src/bem/_functions.scss | 26 | ||||
-rw-r--r-- | src/bem/_modifier.scss | 246 | ||||
-rw-r--r-- | src/bem/_multi.scss | 131 | ||||
-rw-r--r-- | src/bem/_state.scss | 146 | ||||
-rw-r--r-- | src/bem/_suffix.scss | 118 | ||||
-rw-r--r-- | src/bem/_theme.scss | 61 | ||||
-rw-r--r-- | src/bem/_validators.scss | 176 | ||||
-rw-r--r-- | src/bem/_vars.scss | 108 | ||||
-rw-r--r-- | src/harmony-shortcodes.scss | 35 | ||||
-rw-r--r-- | src/main.scss | 10 | ||||
-rw-r--r-- | src/prep.scss | 2 | ||||
-rw-r--r-- | src/responsive-shortcodes.scss | 14 |
26 files changed, 5099 insertions, 0 deletions
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 @@ | |||
1 | //// | ||
2 | /// BEM. | ||
3 | /// | ||
4 | /// BEM is a methodology for structuring websites and is mostly known for it's CSS naming convention. | ||
5 | /// BEMIT is in extension of this methodology and allows you to give blocks a more fine-grained purpose | ||
6 | /// than BEM alone would let you do. | ||
7 | /// | ||
8 | /// Sass does support BEM quite well thanks to the ampersand (&) and the @at-root directive. However, | ||
9 | /// there is no way to make sure users adhere to the BEM or BEMIT methodology. | ||
10 | /// That's where the mixins in this file come into play: They automatically generate the right selectors | ||
11 | /// and perform checks regarding the nesting order, nesting depth, and so on. | ||
12 | /// | ||
13 | /// There are comments in the mixins explaining what selector is generated. The EBNF grammar is as follows: | ||
14 | /// | ||
15 | /// (* Shorthands for block, element, modifier, suffix *) | ||
16 | /// entity_shorthand = "b" "e" "m" "s" "t" ; | ||
17 | /// | ||
18 | /// (* One or multiple BEMIT entities that were generated with an earlier mixin invocation *) | ||
19 | /// existing_entities = "{" entity_shorthand { "," entity_shorthand } "}" ; | ||
20 | /// | ||
21 | /// (* A BEM entity that doesn't depend on a parent entity *) | ||
22 | /// generated_independent_entity = "block" ; | ||
23 | /// | ||
24 | /// (* A BEM entity that is attached to a parent entity *) | ||
25 | /// generated_attached_entity = existing_entities ( "__element" | "--modifier" | "@suffix" ) ; | ||
26 | /// | ||
27 | /// (* A selector created by the user, such as "&:hover", "> a", and so on *) | ||
28 | /// manual_selector_part = "[manual selector]" ; | ||
29 | /// | ||
30 | /// (* A part of the selector that may or may not be in the generated result *) | ||
31 | /// optional_selector_part = "(" ( existing_entities | manual_selector_part ) ")" ; | ||
32 | /// | ||
33 | /// (* One part of the selector *) | ||
34 | /// selector_part = existing_entities | manual_selector_part | optional_selector_part | generated_independent_entity | generated_attached_entity ; | ||
35 | /// | ||
36 | /// (* How the left and right selector are related, i.e. space means right is a descendant of left, and dot means right specializes left *) | ||
37 | /// selector_link = " " | "." ; | ||
38 | /// | ||
39 | /// (* The whole selector *) | ||
40 | /// selector = selector_part { ( selector_link ) selector_part } ; | ||
41 | /// | ||
42 | /// @link https://en.bem.info/ Information about BEM | ||
43 | /// @link https://csswizardry.com/2015/08/bemit-taking-the-bem-naming-convention-a-step-further/ Information about BEMIT | ||
44 | /// | ||
45 | /// @group BEM | ||
46 | /// | ||
47 | /// @access public | ||
48 | //// | ||
49 | |||
50 | @import 'bem/vars'; | ||
51 | @import 'bem/functions'; | ||
52 | @import 'bem/validators'; | ||
53 | @import 'bem/block'; | ||
54 | @import 'bem/element'; | ||
55 | @import 'bem/modifier'; | ||
56 | @import 'bem/suffix'; | ||
57 | @import 'bem/state'; | ||
58 | @import 'bem/theme'; | ||
59 | @import 'bem/multi'; | ||
60 | @import 'bem/debug'; | ||
61 | |||
62 | @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 @@ | |||
1 | //// | ||
2 | /// Context handling. | ||
3 | /// | ||
4 | /// Contexts allow you to pass data between mixins and let you enforce a certain nesting order. | ||
5 | /// It's an essential part for the BEM-related mixins. | ||
6 | /// | ||
7 | /// If you want to create a new context, the easiest pattern is to create a new mixin and wrap | ||
8 | /// the @content between a pair of iro-context-push and iro-context-pop. | ||
9 | /// From within the @content, you can access the context's data with iro-context-get. | ||
10 | /// To make the compilation fail if a certain nesting order is violated, use | ||
11 | /// iro-context-assert-stack-must-contain and iro-context-assert-stack-must-not-contain. | ||
12 | /// | ||
13 | /// @group Contexts | ||
14 | /// | ||
15 | /// @access public | ||
16 | //// | ||
17 | |||
18 | /// | ||
19 | /// Map of all context stacks. | ||
20 | /// | ||
21 | /// @type map | ||
22 | /// | ||
23 | /// @access private | ||
24 | /// | ||
25 | $iro-context-stacks: (); | ||
26 | |||
27 | /// | ||
28 | /// Create a new context stack. | ||
29 | /// | ||
30 | /// @param {string} $stack-id - ID of context stack | ||
31 | /// | ||
32 | /// @throw If context stack already exists | ||
33 | /// | ||
34 | @mixin iro-context-stack-create($stack-id) { | ||
35 | $noop: iro-context-stack-create($stack-id); | ||
36 | } | ||
37 | |||
38 | /// | ||
39 | /// Create a new context stack. | ||
40 | /// | ||
41 | /// @param {string} $stack-id - ID of context stack | ||
42 | /// | ||
43 | @function iro-context-stack-create($stack-id) { | ||
44 | @if map-has-key($iro-context-stacks, $stack-id) { | ||
45 | @error 'Context stack "#{inspect($stack-id)}" does not exist.'; | ||
46 | } | ||
47 | |||
48 | $iro-context-stacks: map-merge($iro-context-stacks, ($stack-id: ())) !global; | ||
49 | |||
50 | @return null; | ||
51 | } | ||
52 | |||
53 | /// | ||
54 | /// Clear a context stack. | ||
55 | /// | ||
56 | /// @param {string} $stack-id - ID of context stack | ||
57 | /// | ||
58 | @mixin iro-context-stack-clear($stack-id) { | ||
59 | $noop: iro-context-stack-clear($stack-id); | ||
60 | } | ||
61 | |||
62 | /// | ||
63 | /// Clear a context stack. | ||
64 | /// | ||
65 | /// @param {string} $stack-id - ID of context stack | ||
66 | /// | ||
67 | @function iro-context-stack-clear($stack-id) { | ||
68 | @if not map-has-key($iro-context-stacks, $stack-id) { | ||
69 | @error 'Context stack "#{inspect($stack-id)}" does not exist.'; | ||
70 | } | ||
71 | |||
72 | $context-stack: (); | ||
73 | $iro-context-stacks: map-merge($iro-context-stacks, ($stack-id: $context-stack)) !global; | ||
74 | |||
75 | @return null; | ||
76 | } | ||
77 | |||
78 | /// | ||
79 | /// Delete a context stack. | ||
80 | /// | ||
81 | /// @param {string} $stack-id - ID of context stack | ||
82 | /// | ||
83 | @mixin iro-context-stack-delete($stack-id) { | ||
84 | $noop: iro-context-stack-delete($stack-id); | ||
85 | } | ||
86 | |||
87 | /// | ||
88 | /// Delete a context stack. | ||
89 | /// | ||
90 | /// @param {string} $stack-id - ID of context stack | ||
91 | /// | ||
92 | @function iro-context-stack-delete($stack-id) { | ||
93 | @if not map-has-key($iro-context-stacks, $stack-id) { | ||
94 | @error 'Context stack "#{inspect($stack-id)}" does not exist.'; | ||
95 | } | ||
96 | |||
97 | $iro-context-stacks: map-remove($iro-context-stacks, $stack-id) !global; | ||
98 | |||
99 | @return null; | ||
100 | } | ||
101 | |||
102 | /// | ||
103 | /// Push a new context to a context stack. | ||
104 | /// | ||
105 | /// @param {string} $stack-id - ID of context stack to use | ||
106 | /// @param {string} $id - ID of new context | ||
107 | /// @param {any} $data [()] - Data that belongs to the context | ||
108 | /// | ||
109 | @mixin iro-context-push($stack-id, $id, $data: ()) { | ||
110 | $noop: iro-context-push($stack-id, $id, $data); | ||
111 | } | ||
112 | |||
113 | /// | ||
114 | /// Push a new context to a context stack. | ||
115 | /// | ||
116 | /// @param {string} $stack-id - ID of context stack to use | ||
117 | /// @param {string} $id - ID of new context | ||
118 | /// @param {any} $data [()] - Data that belongs to the context | ||
119 | /// | ||
120 | /// @return {list} A list with two items: 1 = context id, 2 = context data | ||
121 | /// | ||
122 | @function iro-context-push($stack-id, $id, $data: ()) { | ||
123 | @if not map-has-key($iro-context-stacks, $stack-id) { | ||
124 | @error 'Context stack "#{inspect($stack-id)}" does not exist.'; | ||
125 | } | ||
126 | |||
127 | $context: $id $data; | ||
128 | $context-stack: map-get($iro-context-stacks, $stack-id); | ||
129 | $context-stack: append($context-stack, $context); | ||
130 | $iro-context-stacks: map-merge($iro-context-stacks, ($stack-id: $context-stack)) !global; | ||
131 | |||
132 | @return $context; | ||
133 | } | ||
134 | |||
135 | /// | ||
136 | /// Pop the latest context from a context stack. | ||
137 | /// | ||
138 | /// @param {string} $stack-id - ID of context stack to use | ||
139 | /// | ||
140 | /// @throw If context stack doesn't exist | ||
141 | /// | ||
142 | @mixin iro-context-pop($stack-id) { | ||
143 | $noop: iro-context-pop($stack-id); | ||
144 | } | ||
145 | |||
146 | /// | ||
147 | /// Pop the latest context from a context stack. | ||
148 | /// | ||
149 | /// @param {string} $stack-id - ID of context stack to use | ||
150 | /// | ||
151 | /// @return {list} A list with two items: 1 = context id, 2 = context data | ||
152 | /// | ||
153 | @function iro-context-pop($stack-id) { | ||
154 | @if not map-has-key($iro-context-stacks, $stack-id) { | ||
155 | @error 'Context stack "#{inspect($stack-id)}" does not exist.'; | ||
156 | } | ||
157 | |||
158 | $context-stack: map-get($iro-context-stacks, $stack-id); | ||
159 | |||
160 | @if length($context-stack) == 0 { | ||
161 | @error 'Context stack "#{inspect($stack-id)}" is already empty.'; | ||
162 | } | ||
163 | |||
164 | $popped-context: nth($context-stack, -1); | ||
165 | |||
166 | @if length($context-stack) == 1 { | ||
167 | $context-stack: (); | ||
168 | } @else { | ||
169 | $context-stack: iro-list-slice($context-stack, 1, length($context-stack) - 1); | ||
170 | } | ||
171 | |||
172 | $iro-context-stacks: map-merge($iro-context-stacks, ($stack-id: $context-stack)) !global; | ||
173 | |||
174 | @return $popped-context; | ||
175 | } | ||
176 | |||
177 | /// | ||
178 | /// Assert that a context stack must contain one of the given context IDs. | ||
179 | /// | ||
180 | /// @param {string} $stack-id - ID of context stack to use | ||
181 | /// @param {list} $context-ids - Context IDs | ||
182 | /// @param {bool} $check-head-only [false] - If false, all items will be checked. If true, only the head will be checked. | ||
183 | /// | ||
184 | /// @throw If assertion fails | ||
185 | /// | ||
186 | @mixin iro-context-assert-stack-must-contain($stack-id, $context-ids, $check-head-only: false) { | ||
187 | @if not iro-context-stack-contains($stack-id, $context-ids, $check-head-only) { | ||
188 | @error 'Must be called inside of contexts "#{inspect($context-ids)}".'; | ||
189 | } | ||
190 | } | ||
191 | |||
192 | /// | ||
193 | /// Assert that a context stack must not contain any of the given context IDs. | ||
194 | /// | ||
195 | /// @param {string} $stack-id - ID of context stack to use | ||
196 | /// @param {list} $context-ids - Context IDs | ||
197 | /// @param {bool} $check-head-only [false] - If false, all items will be checked. If true, only the head will be checked. | ||
198 | /// | ||
199 | /// @throw If assertion fails | ||
200 | /// | ||
201 | @mixin iro-context-assert-stack-must-not-contain($stack-id, $context-ids, $check-head-only: false) { | ||
202 | @if iro-context-stack-contains($stack-id, $context-ids, $check-head-only) { | ||
203 | @error 'Must not be called inside of contexts "#{inspect($context-ids)}".'; | ||
204 | } | ||
205 | } | ||
206 | |||
207 | /// | ||
208 | /// Check if a context stack contains one of the given context IDs. | ||
209 | /// | ||
210 | /// @param {string} $stack-id - ID of context stack to use | ||
211 | /// @param {list} $context-ids - Context IDs | ||
212 | /// @param {bool} $check-head-only [false] - If false, all items will be checked. If true, only the head will be checked. | ||
213 | /// | ||
214 | /// @return {bool} `true` if the context stack contains one of the context IDs, otherwise `false` | ||
215 | /// | ||
216 | @function iro-context-stack-contains($stack-id, $context-ids, $check-head-only: false) { | ||
217 | @if not map-has-key($iro-context-stacks, $stack-id) { | ||
218 | @error 'Context stack "#{inspect($stack-id)}" does not exist.'; | ||
219 | } | ||
220 | |||
221 | $context-stack: map-get($iro-context-stacks, $stack-id); | ||
222 | |||
223 | @if length($context-stack) == 0 { | ||
224 | @return false; | ||
225 | } | ||
226 | |||
227 | $end-idx: if($check-head-only, length($context-stack), 1); | ||
228 | |||
229 | @for $i from length($context-stack) through $end-idx { | ||
230 | $context: nth($context-stack, $i); | ||
231 | |||
232 | @each $chk-context in $context-ids { | ||
233 | @if nth($context, 1) == $chk-context { | ||
234 | @return true; | ||
235 | } | ||
236 | } | ||
237 | } | ||
238 | |||
239 | @return false; | ||
240 | } | ||
241 | |||
242 | /// | ||
243 | /// Assert that a context stack must contain a number of contexts smaller than $max-count. | ||
244 | /// | ||
245 | /// @param {string} $stack-id - ID of context stack to use | ||
246 | /// @param {number} $max-count - Maximum number ofg contexts in context stack | ||
247 | /// | ||
248 | /// @throw If assertion fails | ||
249 | /// | ||
250 | @mixin iro-context-assert-stack-count($stack-id, $max-count) { | ||
251 | @if iro-context-stack-count($stack-id) > $max-count { | ||
252 | @error 'Maximum context count "#{inspect($max-count)}" exceeded.'; | ||
253 | } | ||
254 | } | ||
255 | |||
256 | /// | ||
257 | /// Get the number of contexts from a context stack. | ||
258 | /// | ||
259 | /// @param {string} $stack-id - ID of context stack to use | ||
260 | /// | ||
261 | /// @return {number} The number of contexts | ||
262 | /// | ||
263 | @function iro-context-stack-count($stack-id) { | ||
264 | @if not map-has-key($iro-context-stacks, $stack-id) { | ||
265 | @error 'Context stack "#{inspect($stack-id)}" does not exist.'; | ||
266 | } | ||
267 | |||
268 | $context-stack: map-get($iro-context-stacks, $stack-id); | ||
269 | |||
270 | @return length($context-stack); | ||
271 | } | ||
272 | |||
273 | /// | ||
274 | /// Get a specific context from the stack. | ||
275 | /// | ||
276 | /// @param {string} $stack-id - ID of context stack to use | ||
277 | /// @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. | ||
278 | /// | ||
279 | /// @return {list} Null if no match was found, otherwise a list with two items: 1. context ID, 2. context data. | ||
280 | /// | ||
281 | @function iro-context-get($stack-id, $type-or-level: null) { | ||
282 | @if not map-has-key($iro-context-stacks, $stack-id) { | ||
283 | @error 'Context stack "#{inspect($stack-id)}" does not exist.'; | ||
284 | } | ||
285 | |||
286 | $context-stack: map-get($iro-context-stacks, $stack-id); | ||
287 | |||
288 | @if length($context-stack) == 0 { | ||
289 | @return null; | ||
290 | } | ||
291 | |||
292 | @if type-of($type-or-level) == number { | ||
293 | $context: nth($context-stack, -$type-or-level); | ||
294 | |||
295 | @return $context; | ||
296 | } @else { | ||
297 | @for $i from -1 through -(length($context-stack)) { | ||
298 | $context: nth($context-stack, $i); | ||
299 | |||
300 | @if type-of($type-or-level) == list { | ||
301 | @for $j from 1 through length($type-or-level) { | ||
302 | $ctx: nth($type-or-level, $j); | ||
303 | |||
304 | @if nth($context, 1) == $ctx { | ||
305 | @return $context; | ||
306 | } | ||
307 | } | ||
308 | } @else if nth($context, 1) == $type-or-level { | ||
309 | @return $context; | ||
310 | } | ||
311 | } | ||
312 | } | ||
313 | |||
314 | @return null; | ||
315 | } | ||
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 @@ | |||
1 | //// | ||
2 | /// Easing. | ||
3 | /// | ||
4 | /// A collection of easing functions which are commonly used for animations. | ||
5 | /// This code is based on https://github.com/gre/bezier-easing. | ||
6 | /// | ||
7 | /// @group Easing | ||
8 | /// | ||
9 | /// @access public | ||
10 | //// | ||
11 | |||
12 | /// | ||
13 | /// @access private | ||
14 | /// | ||
15 | $iro-cubic-bezier-sample-pool: (); | ||
16 | |||
17 | /// | ||
18 | /// Sample pool size for cubic bezier calculations. | ||
19 | /// | ||
20 | $iro-cubic-bezier-sample-pool-size: 10 !default; | ||
21 | |||
22 | /// | ||
23 | /// Minimum slope required to use the Newton-Raphson method for cubic bezier calculations. | ||
24 | /// | ||
25 | $iro-cubic-bezier-newton-min-slope: 0.001 !default; | ||
26 | |||
27 | /// | ||
28 | /// Number of iterations of the Newton-Raphson method. | ||
29 | /// | ||
30 | $iro-cubic-bezier-newton-iters: 4 !default; | ||
31 | |||
32 | /// | ||
33 | /// Precision of the subdivision method for cubic bezier calculations. | ||
34 | /// | ||
35 | $iro-cubic-bezier-subdiv-precision: 0.0000001 !default; | ||
36 | |||
37 | /// | ||
38 | /// Maximum iterations of the subdivision method for cubic bezier calculations. | ||
39 | /// | ||
40 | $iro-cubic-bezier-subdiv-max-iters: 10 !default; | ||
41 | |||
42 | /// | ||
43 | /// A cubic bezier function identical to the CSS cubic-bezier function. | ||
44 | /// | ||
45 | /// @param {number} $x1 - X of first point | ||
46 | /// @param {number} $y1 - Y of first point | ||
47 | /// @param {number} $x2 - X of second point | ||
48 | /// @param {number} $y2 - Y of second point | ||
49 | /// @param {number} $x - Progress between 0 and 1 inclusive | ||
50 | /// | ||
51 | /// @return {number} | ||
52 | /// | ||
53 | @function iro-cubic-bezier($x1, $y1, $x2, $y2, $x) { | ||
54 | // | ||
55 | // Cover simple cases | ||
56 | // | ||
57 | |||
58 | @if ($x1 == $y1) and ($x2 == $y2) { | ||
59 | @return $x; | ||
60 | } | ||
61 | @if $x == 0 { | ||
62 | @return 0; | ||
63 | } | ||
64 | @if $x == 1 { | ||
65 | @return 1; | ||
66 | } | ||
67 | |||
68 | // | ||
69 | // Generate samples | ||
70 | // | ||
71 | |||
72 | $sample-pool-key: $x1 + '_' + $x2; | ||
73 | |||
74 | @if not map-has-key($iro-cubic-bezier-sample-pool, $sample-pool-key) { | ||
75 | $samples: (); | ||
76 | |||
77 | @for $i from 0 through $iro-cubic-bezier-sample-pool-size { | ||
78 | $samples: append($samples, iro-cubic-bezier-func($x1, $x2, $i / $iro-cubic-bezier-sample-pool-size)); | ||
79 | } | ||
80 | |||
81 | $iro-cubic-bezier-sample-pool: map-merge($iro-cubic-bezier-sample-pool, ($sample-pool-key: $samples)) !global; | ||
82 | } | ||
83 | |||
84 | // | ||
85 | // Calculate cubic bezier | ||
86 | // | ||
87 | |||
88 | @return iro-cubic-bezier-func($y1, $y2, iro-cubic-bezier-t-for-x($x1, $x2, $x)); | ||
89 | } | ||
90 | |||
91 | /// | ||
92 | /// @access private | ||
93 | /// | ||
94 | @function iro-cubic-bezier-func-a($p1, $p2) { | ||
95 | @return 1 - 3 * $p2 + 3 * $p1; | ||
96 | } | ||
97 | |||
98 | /// | ||
99 | /// @access private | ||
100 | /// | ||
101 | @function iro-cubic-bezier-func-b($p1, $p2) { | ||
102 | @return 3 * $p2 - 6 * $p1; | ||
103 | } | ||
104 | |||
105 | /// | ||
106 | /// @access private | ||
107 | /// | ||
108 | @function iro-cubic-bezier-func-c($p1) { | ||
109 | @return 3 * $p1; | ||
110 | } | ||
111 | |||
112 | /// | ||
113 | /// One-dimensional cubic bezier function. | ||
114 | /// | ||
115 | /// @access private | ||
116 | /// | ||
117 | @function iro-cubic-bezier-func($p1, $p2, $t) { | ||
118 | @return ((iro-cubic-bezier-func-a($p1, $p2) * $t + iro-cubic-bezier-func-b($p1, $p2)) * $t + iro-cubic-bezier-func-c($p1)) * $t; | ||
119 | } | ||
120 | |||
121 | /// | ||
122 | /// Derivative of the one-dimensional cubic bezier function. | ||
123 | /// | ||
124 | /// @access private | ||
125 | /// | ||
126 | @function iro-cubic-bezier-func-slope($p1, $p2, $t) { | ||
127 | @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); | ||
128 | } | ||
129 | |||
130 | /// | ||
131 | /// Newton-Raphson method to calculate the t parameter for a given x parameter. | ||
132 | /// | ||
133 | /// @access private | ||
134 | /// | ||
135 | @function iro-cubic-bezier-newton-raphson($x1, $x2, $x, $t) { | ||
136 | @for $i from 1 through $iro-cubic-bezier-newton-iters { | ||
137 | $cur-slope: iro-cubic-bezier-func-slope($x1, $x2, $t); | ||
138 | |||
139 | @if $cur-slope == 0 { | ||
140 | @return $t; | ||
141 | } | ||
142 | |||
143 | $cur-x: iro-cubic-bezier-func($x1, $x2, $t) - $x; | ||
144 | $t: $t - $cur-x / $cur-slope; | ||
145 | } | ||
146 | |||
147 | @return $t; | ||
148 | } | ||
149 | |||
150 | /// | ||
151 | /// Subdivision method to calculate the t parameter for a given x parameter. | ||
152 | /// | ||
153 | /// @access private | ||
154 | /// | ||
155 | @function iro-cubic-bezier-binary-subdivide($x1, $x2, $x, $a, $b) { | ||
156 | $cur-x: 0; | ||
157 | $cur-t: 0; | ||
158 | $i: 0; | ||
159 | |||
160 | @while $i < $iro-cubic-bezier-subdiv-max-iters { | ||
161 | $cur-t: $a + ($b - $a) / 2; | ||
162 | $cur-x: iro-cubic-bezier-func($x1, $x2, $cur-t) - $x; | ||
163 | |||
164 | @if $cur-x > 0 { | ||
165 | $b: $cur-t; | ||
166 | } @else { | ||
167 | $a: $cur-t; | ||
168 | } | ||
169 | |||
170 | @if abs($cur-x) < $iro-cubic-bezier-subdiv-precision { | ||
171 | @return $cur-t; | ||
172 | } | ||
173 | } | ||
174 | |||
175 | @return $cur-t; | ||
176 | } | ||
177 | |||
178 | /// | ||
179 | /// Calculate the t parameter for a given x parameter. | ||
180 | /// | ||
181 | /// @access private | ||
182 | /// | ||
183 | @function iro-cubic-bezier-t-for-x($x1, $x2, $x) { | ||
184 | $sample-pool-key: $x1 + '_' + $x2; | ||
185 | $samples: map-get($iro-cubic-bezier-sample-pool, $sample-pool-key); | ||
186 | |||
187 | $intv-start: 0; | ||
188 | $cur-sample: 1; | ||
189 | $last-sample: $iro-cubic-bezier-sample-pool-size; | ||
190 | |||
191 | @while ($cur-sample != $last-sample) and (nth($samples, $cur-sample) <= $x) { | ||
192 | $intv-start: $intv-start + (1 / $iro-cubic-bezier-sample-pool-size); | ||
193 | $cur-sample: $cur-sample + 1; | ||
194 | } | ||
195 | $cur-sample: $cur-sample - 1; | ||
196 | |||
197 | $dist: ($x - nth($samples, $cur-sample)) / (nth($samples, $cur-sample + 1) - nth($samples, $cur-sample)); | ||
198 | $guess-t: $intv-start + $dist / $iro-cubic-bezier-sample-pool-size; | ||
199 | |||
200 | $init-slope: iro-cubic-bezier-func-slope($x1, $x2, $guess-t); | ||
201 | @if $init-slope >= $iro-cubic-bezier-newton-min-slope { | ||
202 | @return iro-cubic-bezier-newton-raphson($x1, $x2, $x, $guess-t); | ||
203 | } @else if $init-slope == 0 { | ||
204 | @return $guess-t; | ||
205 | } @else { | ||
206 | @return iro-cubic-bezier-binary-subdivide($x1, $x2, $x, $intv-start, $intv-start + 1 / $iro-cubic-bezier-sample-pool-size); | ||
207 | } | ||
208 | } | ||
209 | |||
210 | /// | ||
211 | /// Sinusoidal easing function (in direction). | ||
212 | /// | ||
213 | /// @param {number} $x - Progress between 0 and 1 inclusive | ||
214 | /// | ||
215 | /// @return {number} | ||
216 | /// | ||
217 | @function iro-ease($x) { | ||
218 | @return iro-cubic-bezier(0.25, 0.1, 0.25, 1, $x); | ||
219 | } | ||
220 | |||
221 | /// | ||
222 | /// Sinusoidal easing function (in direction). | ||
223 | /// | ||
224 | /// @param {number} $x - Progress between 0 and 1 inclusive | ||
225 | /// | ||
226 | /// @return {number} | ||
227 | /// | ||
228 | @function iro-ease-in($x) { | ||
229 | @return iro-cubic-bezier(0.42, 0, 1, 1, $x); | ||
230 | } | ||
231 | |||
232 | /// | ||
233 | /// Sinusoidal easing function (out direction). | ||
234 | /// | ||
235 | /// @param {number} $x - Progress between 0 and 1 inclusive | ||
236 | /// | ||
237 | /// @return {number} | ||
238 | /// | ||
239 | @function iro-ease-out($x) { | ||
240 | @return iro-cubic-bezier(0, 0, 0.58, 1, $x); | ||
241 | } | ||
242 | |||
243 | /// | ||
244 | /// Sinusoidal easing function (in-out direction). | ||
245 | /// | ||
246 | /// @param {number} $x - Progress between 0 and 1 inclusive | ||
247 | /// | ||
248 | /// @return {number} | ||
249 | /// | ||
250 | @function iro-ease-in-out($x) { | ||
251 | @return iro-cubic-bezier(0.42, 0, 0.58, 1, $x); | ||
252 | } | ||
253 | |||
254 | /// | ||
255 | /// Sinusoidal easing function (in direction). | ||
256 | /// | ||
257 | /// @param {number} $x - Progress between 0 and 1 inclusive | ||
258 | /// | ||
259 | /// @return {number} | ||
260 | /// | ||
261 | @function iro-ease-in-sine($x) { | ||
262 | @return iro-cubic-bezier(0.47, 0, 0.745, 0.715, $x); | ||
263 | } | ||
264 | |||
265 | /// | ||
266 | /// Sinusoidal easing function (out direction). | ||
267 | /// | ||
268 | /// @param {number} $x - Progress between 0 and 1 inclusive | ||
269 | /// | ||
270 | /// @return {number} | ||
271 | /// | ||
272 | @function iro-ease-out-sine($x) { | ||
273 | @return iro-cubic-bezier(0.39, 0.575, 0.565, 1, $x); | ||
274 | } | ||
275 | |||
276 | /// | ||
277 | /// Sinusoidal easing function (in-out direction). | ||
278 | /// | ||
279 | /// @param {number} $x - Progress between 0 and 1 inclusive | ||
280 | /// | ||
281 | /// @return {number} | ||
282 | /// | ||
283 | @function iro-ease-in-out-sine($x) { | ||
284 | @return iro-cubic-bezier(0.445, 0.05, 0.55, 0.95, $x); | ||
285 | } | ||
286 | |||
287 | /// | ||
288 | /// Quadratic easing function (in direction). | ||
289 | /// | ||
290 | /// @param {number} $x - Progress between 0 and 1 inclusive | ||
291 | /// | ||
292 | /// @return {number} | ||
293 | /// | ||
294 | @function iro-ease-in-quad($x) { | ||
295 | @return iro-cubic-bezier(0.55, 0.085, 0.68, 0.53, $x); | ||
296 | } | ||
297 | |||
298 | /// | ||
299 | /// Quadratic easing function (out direction). | ||
300 | /// | ||
301 | /// @param {number} $x - Progress between 0 and 1 inclusive | ||
302 | /// | ||
303 | /// @return {number} | ||
304 | /// | ||
305 | @function iro-ease-out-quad($x) { | ||
306 | @return iro-cubic-bezier(0.25, 0.46, 0.45, 0.94, $x); | ||
307 | } | ||
308 | |||
309 | /// | ||
310 | /// Quadratic easing function (in-out direction). | ||
311 | /// | ||
312 | /// @param {number} $x - Progress between 0 and 1 inclusive | ||
313 | /// | ||
314 | /// @return {number} | ||
315 | /// | ||
316 | @function iro-ease-in-out-quad($x) { | ||
317 | @return iro-cubic-bezier(0.455, 0.03, 0.515, 0.955, $x); | ||
318 | } | ||
319 | |||
320 | /// | ||
321 | /// Cubic easing function (in direction). | ||
322 | /// | ||
323 | /// @param {number} $x - Progress between 0 and 1 inclusive | ||
324 | /// | ||
325 | /// @return {number} | ||
326 | /// | ||
327 | @function iro-ease-in-cubic($x) { | ||
328 | @return iro-cubic-bezier(0.55, 0.055, 0.675, 0.19, $x); | ||
329 | } | ||
330 | |||
331 | /// | ||
332 | /// Cubic easing function (out direction). | ||
333 | /// | ||
334 | /// @param {number} $x - Progress between 0 and 1 inclusive | ||
335 | /// | ||
336 | /// @return {number} | ||
337 | /// | ||
338 | @function iro-ease-out-cubic($x) { | ||
339 | @return iro-cubic-bezier(0.215, 0.61, 0.355, 1, $x); | ||
340 | } | ||
341 | |||
342 | /// | ||
343 | /// Cubic easing function (in-out direction). | ||
344 | /// | ||
345 | /// @param {number} $x - Progress between 0 and 1 inclusive | ||
346 | /// | ||
347 | /// @return {number} | ||
348 | /// | ||
349 | @function iro-ease-in-out-cubic($x) { | ||
350 | @return iro-cubic-bezier(0.645, 0.045, 0.355, 1, $x); | ||
351 | } | ||
352 | |||
353 | /// | ||
354 | /// Quart easing function (in direction). | ||
355 | /// | ||
356 | /// @param {number} $x - Progress between 0 and 1 inclusive | ||
357 | /// | ||
358 | /// @return {number} | ||
359 | /// | ||
360 | @function iro-ease-in-quart($x) { | ||
361 | @return iro-cubic-bezier(0.895, 0.03, 0.685, 0.22, $x); | ||
362 | } | ||
363 | |||
364 | /// | ||
365 | /// Quart easing function (out direction). | ||
366 | /// | ||
367 | /// @param {number} $x - Progress between 0 and 1 inclusive | ||
368 | /// | ||
369 | /// @return {number} | ||
370 | /// | ||
371 | @function iro-ease-out-quart($x) { | ||
372 | @return iro-cubic-bezier(0.165, 0.84, 0.44, 1, $x); | ||
373 | } | ||
374 | |||
375 | /// | ||
376 | /// Quart easing function (in-out direction). | ||
377 | /// | ||
378 | /// @param {number} $x - Progress between 0 and 1 inclusive | ||
379 | /// | ||
380 | /// @return {number} | ||
381 | /// | ||
382 | @function iro-ease-in-out-quart($x) { | ||
383 | @return iro-cubic-bezier(0.77, 0, 0.175, 1, $x); | ||
384 | } | ||
385 | |||
386 | /// | ||
387 | /// Quint easing function (in direction). | ||
388 | /// | ||
389 | /// @param {number} $x - Progress between 0 and 1 inclusive | ||
390 | /// | ||
391 | /// @return {number} | ||
392 | /// | ||
393 | @function iro-ease-in-quint($x) { | ||
394 | @return iro-cubic-bezier(0.755, 0.05, 0.855, 0.06, $x); | ||
395 | } | ||
396 | |||
397 | /// | ||
398 | /// Quint easing function (out direction). | ||
399 | /// | ||
400 | /// @param {number} $x - Progress between 0 and 1 inclusive | ||
401 | /// | ||
402 | /// @return {number} | ||
403 | /// | ||
404 | @function iro-ease-out-quint($x) { | ||
405 | @return iro-cubic-bezier(0.23, 1, 0.32, 1, $x); | ||
406 | } | ||
407 | |||
408 | /// | ||
409 | /// Quint easing function (in-out direction). | ||
410 | /// | ||
411 | /// @param {number} $x - Progress between 0 and 1 inclusive | ||
412 | /// | ||
413 | /// @return {number} | ||
414 | /// | ||
415 | @function iro-ease-in-out-quint($x) { | ||
416 | @return iro-cubic-bezier(0.86, 0, 0.07, 1, $x); | ||
417 | } | ||
418 | |||
419 | /// | ||
420 | /// Exponential easing function (in direction). | ||
421 | /// | ||
422 | /// @param {number} $x - Progress between 0 and 1 inclusive | ||
423 | /// | ||
424 | /// @return {number} | ||
425 | /// | ||
426 | @function iro-ease-in-expo($x) { | ||
427 | @return iro-cubic-bezier(0.95, 0.05, 0.795, 0.035, $x); | ||
428 | } | ||
429 | |||
430 | /// | ||
431 | /// Exponential easing function (out direction). | ||
432 | /// | ||
433 | /// @param {number} $x - Progress between 0 and 1 inclusive | ||
434 | /// | ||
435 | /// @return {number} | ||
436 | /// | ||
437 | @function iro-ease-out-expo($x) { | ||
438 | @return iro-cubic-bezier(0.19, 1, 0.22, 1, $x); | ||
439 | } | ||
440 | |||
441 | /// | ||
442 | /// Exponential easing function (in-out direction). | ||
443 | /// | ||
444 | /// @param {number} $x - Progress between 0 and 1 inclusive | ||
445 | /// | ||
446 | /// @return {number} | ||
447 | /// | ||
448 | @function iro-ease-in-out-expo($x) { | ||
449 | @return iro-cubic-bezier(1, 0, 0, 1, $x); | ||
450 | } | ||
451 | |||
452 | /// | ||
453 | /// Circular easing function (in direction). | ||
454 | /// | ||
455 | /// @param {number} $x - Progress between 0 and 1 inclusive | ||
456 | /// | ||
457 | /// @return {number} | ||
458 | /// | ||
459 | @function iro-ease-in-circ($x) { | ||
460 | @return iro-cubic-bezier(0.6, 0.04, 0.98, 0.335, $x); | ||
461 | } | ||
462 | |||
463 | /// | ||
464 | /// Circular easing function (out direction). | ||
465 | /// | ||
466 | /// @param {number} $x - Progress between 0 and 1 inclusive | ||
467 | /// | ||
468 | /// @return {number} | ||
469 | /// | ||
470 | @function iro-ease-out-circ($x) { | ||
471 | @return iro-cubic-bezier(0.075, 0.82, 0.165, 1, $x); | ||
472 | } | ||
473 | |||
474 | /// | ||
475 | /// Circular easing function (in-out direction). | ||
476 | /// | ||
477 | /// @param {number} $x - Progress between 0 and 1 inclusive | ||
478 | /// | ||
479 | /// @return {number} | ||
480 | /// | ||
481 | @function iro-ease-in-out-circ($x) { | ||
482 | @return iro-cubic-bezier(0.785, 0.135, 0.15, 0.86, $x); | ||
483 | } | ||
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 @@ | |||
1 | //// | ||
2 | /// Various functions. | ||
3 | /// | ||
4 | /// This file contains various and mostly unrelated functions. Some of which | ||
5 | /// are used in this framework, while others are just there and may be used. | ||
6 | /// | ||
7 | /// @group Functions | ||
8 | /// | ||
9 | /// @access public | ||
10 | //// | ||
11 | |||
12 | /// | ||
13 | /// Replace a substring with a new string. | ||
14 | /// | ||
15 | /// @param {string} $string - String to search | ||
16 | /// @param {string} $search - Substring that gets replaced | ||
17 | /// @param {string} $replace - String that replaces $search | ||
18 | /// | ||
19 | /// @return {string} A string with all instances of $search replaced with $replace | ||
20 | /// | ||
21 | @function iro-str-replace($string, $search, $replace) { | ||
22 | $index: str-index($string, $search); | ||
23 | |||
24 | @if $index { | ||
25 | @return str-slice($string, 1, $index - 1) + $replace + iro-str-replace(str-slice($string, $index + str-length($search)), $search, $replace); | ||
26 | } | ||
27 | |||
28 | @return $string; | ||
29 | } | ||
30 | |||
31 | /// | ||
32 | /// Concatenate all items from $list. | ||
33 | /// | ||
34 | /// @param {list} $list | ||
35 | /// @param {number} $glue - Delimiter | ||
36 | /// | ||
37 | /// @return {string} | ||
38 | /// | ||
39 | @function iro-str-implode($list, $glue: '') { | ||
40 | $result: ''; | ||
41 | |||
42 | @each $item in $list { | ||
43 | $result: $result + if(length($item) > 1, iro-str-implode($item, $glue), $item); | ||
44 | |||
45 | @if $item != nth($list, length($list)) { | ||
46 | $result: $result + $glue; | ||
47 | } | ||
48 | } | ||
49 | |||
50 | @return $result; | ||
51 | } | ||
52 | |||
53 | /// | ||
54 | /// Extract a subset from the given list. | ||
55 | /// | ||
56 | /// @param {list} $list | ||
57 | /// @param {number} $start [1] - Indices before this value will be discarded | ||
58 | /// @param {number} $end [length($list)] - Indices starting after this value will be discarded | ||
59 | /// | ||
60 | /// @return {list} A slice of the list | ||
61 | /// | ||
62 | @function iro-list-slice($list, $start: 1, $end: length($list)) { | ||
63 | $result: (); | ||
64 | |||
65 | @for $i from $start through $end { | ||
66 | @if $i != 0 { | ||
67 | $result: append($result, nth($list, $i), list-separator($list)); | ||
68 | } | ||
69 | } | ||
70 | |||
71 | @return $result; | ||
72 | } | ||
73 | |||
74 | /// | ||
75 | /// Add a new item to the beginning of a list. | ||
76 | /// | ||
77 | /// @param {list} $list | ||
78 | /// @param {number} $value | ||
79 | /// | ||
80 | /// @return {list} A list with $value at the beginning, followed by the other items | ||
81 | /// | ||
82 | @function iro-list-prepend($list, $value) { | ||
83 | $result: append((), $value, list-separator($list)); | ||
84 | |||
85 | @if length($list) > 0 { | ||
86 | @for $i from 1 through length($list) { | ||
87 | $result: append($result, nth($list, $i), list-separator($list)); | ||
88 | } | ||
89 | } | ||
90 | |||
91 | @return $result; | ||
92 | } | ||
93 | |||
94 | /// | ||
95 | /// Sort numeric items in a list. | ||
96 | /// | ||
97 | /// The implementation is based on the algorithm on the German Wikipedia article | ||
98 | /// about quicksort: https://de.wikipedia.org/wiki/Quicksort#Pseudocode | ||
99 | /// | ||
100 | /// @param {list} $l | ||
101 | /// | ||
102 | /// @return {list} Sorted list | ||
103 | /// | ||
104 | @function iro-quicksort($l, $left: 1, $right: length($l)) { | ||
105 | @if $left < $right { | ||
106 | $pvr: iro-quicksort-partition($l, $left, $right); | ||
107 | $pivot: nth($pvr, 1); | ||
108 | $l: nth($pvr, 2); | ||
109 | $l: iro-quicksort($l, $left, $pivot); | ||
110 | $l: iro-quicksort($l, $pivot + 1, $right); | ||
111 | } | ||
112 | |||
113 | @return $l; | ||
114 | } | ||
115 | |||
116 | /// | ||
117 | /// @access private | ||
118 | /// | ||
119 | @function iro-quicksort-partition($l, $left, $right) { | ||
120 | $start: true; | ||
121 | $i: $left; | ||
122 | $j: $right - 1; | ||
123 | $pivot: nth($l, $right); | ||
124 | |||
125 | @while ($i < $j) or $start { | ||
126 | @while (nth($l, $i) < $pivot) and ($i < $right - 1) { | ||
127 | $i: $i + 1; | ||
128 | } | ||
129 | |||
130 | @while (nth($l, $j)>= $pivot) and ($j > $left) { | ||
131 | $j: $j - 1; | ||
132 | } | ||
133 | |||
134 | @if $i < $j { | ||
135 | $i-val: nth($l, $i); | ||
136 | $l: set-nth($l, $i, nth($l, $j)); | ||
137 | $l: set-nth($l, $j, $i-val); | ||
138 | } | ||
139 | |||
140 | $start: false; | ||
141 | } | ||
142 | |||
143 | @if nth($l, $i) > $pivot { | ||
144 | $i-val: nth($l, $i); | ||
145 | $l: set-nth($l, $i, nth($l, $right)); | ||
146 | $l: set-nth($l, $right, $i-val); | ||
147 | } | ||
148 | |||
149 | @return $i $l; | ||
150 | } | ||
151 | |||
152 | /// | ||
153 | /// Try to get the value for the given key from the given map. If it doesn't contain that key, | ||
154 | /// return the provided default value instead. | ||
155 | /// | ||
156 | /// @param {map} $map | ||
157 | /// @param {string} $key | ||
158 | /// @param {any} $default | ||
159 | /// | ||
160 | /// @return {any} Either the value assigned to $key or $default | ||
161 | /// | ||
162 | @function iro-map-get-default($map, $key, $default) { | ||
163 | @return if(map-has-key($map, $key), map-get($map, $key), $default); | ||
164 | } | ||
165 | |||
166 | /// | ||
167 | /// Get the value for a map within a map (or deeper). | ||
168 | /// | ||
169 | /// @param {map} $map | ||
170 | /// @param {string | list} $key | ||
171 | /// @param {any} $default [null] | ||
172 | /// | ||
173 | /// @return {any} Either the value assigned to $key or $default | ||
174 | /// | ||
175 | @function iro-map-get-deep($map, $key, $default: null) { | ||
176 | $value: null; | ||
177 | |||
178 | @if type-of($key) == list { | ||
179 | $value: $map; | ||
180 | |||
181 | @each $k in $key { | ||
182 | $value: map-get($value, $k); | ||
183 | |||
184 | @if $value == null { | ||
185 | @return $default; | ||
186 | } | ||
187 | } | ||
188 | } @else { | ||
189 | $value: map-get($map, $key); | ||
190 | |||
191 | @if $value == null { | ||
192 | @return $default; | ||
193 | } | ||
194 | } | ||
195 | |||
196 | @return $value; | ||
197 | } | ||
198 | |||
199 | /// | ||
200 | /// Merge two maps recursively. | ||
201 | /// | ||
202 | /// @param {map} $map1 | ||
203 | /// @param {map} $map2 | ||
204 | /// | ||
205 | /// @return {map} The result of a recursive merge of $map1 and $map2 | ||
206 | /// | ||
207 | @function iro-map-merge-recursive($map1, $map2) { | ||
208 | @if type-of($map1) != map or type-of($map2) != map { | ||
209 | @error 'Two maps expected.'; | ||
210 | } | ||
211 | |||
212 | $result: $map1; | ||
213 | |||
214 | @each $key, $value in $map2 { | ||
215 | @if type-of(map-get($result, $key)) == map and type-of($value) == map { | ||
216 | $result: map-merge($result, ($key: iro-map-merge-recursive(map-get($result, $key), $value))); | ||
217 | } @else { | ||
218 | $result: map-merge($result, ($key: $value)); | ||
219 | } | ||
220 | } | ||
221 | |||
222 | @return $result; | ||
223 | } | ||
224 | |||
225 | /// | ||
226 | /// Get the contents of a map as a user-friendly string representation. | ||
227 | /// | ||
228 | /// @param {map} $map | ||
229 | /// | ||
230 | /// @return {string} | ||
231 | /// | ||
232 | @function iro-map-print($map) { | ||
233 | $output: ''; | ||
234 | |||
235 | @each $key, $value in $map { | ||
236 | $value-str: ''; | ||
237 | |||
238 | @if type-of($value) == map { | ||
239 | $value-str: '[ ' + iro-map-print($value) + ' ]'; | ||
240 | } @else if type-of($value) == list { | ||
241 | $value-str: '[ ' + iro-str-implode($value, ', ') + ' ]'; | ||
242 | } @else if type-of($value) == string { | ||
243 | $value-str: '\'' + $value + '\''; | ||
244 | } @else { | ||
245 | $value-str: $value; | ||
246 | } | ||
247 | |||
248 | @if $output == '' { | ||
249 | $output: $key + ': ' + $value-str; | ||
250 | } @else { | ||
251 | $output: $output + ', ' + $key + ': ' + $value-str; | ||
252 | } | ||
253 | } | ||
254 | |||
255 | @return $output; | ||
256 | } | ||
257 | |||
258 | /// | ||
259 | /// Check if the given selector ends with one of the provided suffixes. | ||
260 | /// | ||
261 | /// @param {selector} $selector | ||
262 | /// @param {selector} $suffixes | ||
263 | /// | ||
264 | /// @return {bool} `true` if the selector matches at least one suffix, otherwise `false`. | ||
265 | /// | ||
266 | @function iro-selector-suffix-match($selector, $suffixes) { | ||
267 | $match: true; | ||
268 | |||
269 | @each $sel in $selector { | ||
270 | @if $match { | ||
271 | $sel-match: false; | ||
272 | |||
273 | @each $suffix in $suffixes { | ||
274 | @if not $sel-match { | ||
275 | $suf-match: true; | ||
276 | |||
277 | @for $i from 1 through length($suffix) { | ||
278 | @if $suf-match and (nth($sel, -$i) != nth($suffix, -$i)) { | ||
279 | $suf-match: false; | ||
280 | } | ||
281 | } | ||
282 | |||
283 | @if $suf-match { | ||
284 | $sel-match: true; | ||
285 | } | ||
286 | } | ||
287 | } | ||
288 | |||
289 | @if not $sel-match { | ||
290 | $match: false; | ||
291 | } | ||
292 | } | ||
293 | } | ||
294 | |||
295 | @return $match; | ||
296 | } | ||
297 | |||
298 | /// | ||
299 | /// Remove the unit from any variable. | ||
300 | /// | ||
301 | /// @param {any} $n | ||
302 | /// | ||
303 | /// @return {number} Unit-less variable | ||
304 | /// | ||
305 | @function iro-strip-unit($n) { | ||
306 | @return $n / ($n * 0 + 1); | ||
307 | } | ||
308 | |||
309 | /// | ||
310 | /// Convert a pixel value to a rem value. | ||
311 | /// | ||
312 | /// @param {number} $size - Pixel value to convert | ||
313 | /// @param {number} $base [$iro-root-size] - Reference base font size used for conversion | ||
314 | /// | ||
315 | /// @return {number} Pixel value converted to rem | ||
316 | /// | ||
317 | @function iro-px-to-rem($size, $base: $iro-root-size) { | ||
318 | @return $size / $base * 1rem; | ||
319 | } | ||
320 | |||
321 | /// | ||
322 | /// A mixin with the sole purpose of letting you use temporary variables without polluting the global namespace. | ||
323 | /// | ||
324 | /// @content | ||
325 | /// | ||
326 | @mixin iro-execute { | ||
327 | @content; | ||
328 | } | ||
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 @@ | |||
1 | //// | ||
2 | /// Smoother background gradients. | ||
3 | /// | ||
4 | /// The default background gradients produced by any browser have a quite harsh transition between | ||
5 | /// colors. This is especially apparent if you, for example, use a strong fade-out gradient to make | ||
6 | /// text in front of a background more readable. | ||
7 | /// | ||
8 | /// The function in this file generates smoother gradients by using easing functions of the user's | ||
9 | /// choice. | ||
10 | /// It's essentially a more flexible alternative to the PostCSS plugin "PostCSS Easing Gradients": | ||
11 | /// https://github.com/larsenwork/postcss-easing-gradients | ||
12 | /// | ||
13 | /// @group Background gradients | ||
14 | /// | ||
15 | /// @access public | ||
16 | //// | ||
17 | |||
18 | /// | ||
19 | /// Number of intermediate color stops generated to achieve easing. | ||
20 | /// A higher value results in better quality, but also much more generated code. | ||
21 | /// | ||
22 | /// @type number | ||
23 | /// | ||
24 | $iro-easing-gradient-steps: 10 !default; | ||
25 | |||
26 | /// | ||
27 | /// Generate a new easing background gradient. | ||
28 | /// This function is intended to be as similar as possible to the newly proposed syntax for | ||
29 | /// linear-gradient and radial-gradient which includes easing hints. | ||
30 | /// | ||
31 | /// @param {string} $type - Either 'linear' or 'radial', which means the gradient will be either a linear-gradient or a radial-gradient. | ||
32 | /// @param {string} $dir - The direction of the gradient. Depending on $type, this value must be a valid direction for linear-gradient or radial-gradient. | ||
33 | /// @param {color | list} $stop - A color stop as used for linear-gradient or radial-gradient. | ||
34 | /// @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. | ||
35 | /// | ||
36 | /// @return {string} A linear-gradient or radial-gradient with an alternative transitioning behavior. | ||
37 | /// | ||
38 | /// @throw If $type is invalid | ||
39 | /// | ||
40 | /// @link https://github.com/w3c/csswg-drafts/issues/1332 The new CSSWG proposal | ||
41 | /// | ||
42 | /// @example scss - A smoother linear gradient | ||
43 | /// .background { | ||
44 | /// background-image: iro-easing-gradient( | ||
45 | /// linear, | ||
46 | /// to top, | ||
47 | /// #000, | ||
48 | /// in-out-sine, | ||
49 | /// transparent | ||
50 | /// ); | ||
51 | /// } | ||
52 | /// | ||
53 | /// // Generates: | ||
54 | /// | ||
55 | /// .background { | ||
56 | /// background-image: linear-gradient( | ||
57 | /// to top, | ||
58 | /// black 0%, | ||
59 | /// rgba(0, 0, 0, 0.975528) 10%, | ||
60 | /// rgba(0, 0, 0, 0.904508) 20%, | ||
61 | /// rgba(0, 0, 0, 0.793893) 30%, | ||
62 | /// rgba(0, 0, 0, 0.654508) 40%, | ||
63 | /// rgba(0, 0, 0, 0.5) 50%, | ||
64 | /// rgba(0, 0, 0, 0.345492) 60%, | ||
65 | /// rgba(0, 0, 0, 0.206107) 70%, | ||
66 | /// rgba(0, 0, 0, 0.0954915) 80%, | ||
67 | /// rgba(0, 0, 0, 0.0244717) 90%, | ||
68 | /// rgba(0, 0, 0, 3.78257e-11) 100% | ||
69 | /// ); | ||
70 | /// } | ||
71 | /// | ||
72 | /// @example scss - A smoother radial gradient | ||
73 | /// .background { | ||
74 | /// background-image: iro-easing-gradient( | ||
75 | /// radial, | ||
76 | /// 50em 16em at 0 0, | ||
77 | /// #000, | ||
78 | /// in-out-sine, | ||
79 | /// transparent | ||
80 | /// ); | ||
81 | /// } | ||
82 | /// | ||
83 | /// // Generates: | ||
84 | /// | ||
85 | /// .background { | ||
86 | /// background-image: radial-gradient( | ||
87 | /// 50em 16em at 0 0, | ||
88 | /// black 0%, | ||
89 | /// rgba(0, 0, 0, 0.975528) 10%, | ||
90 | /// rgba(0, 0, 0, 0.904508) 20%, | ||
91 | /// rgba(0, 0, 0, 0.793893) 30%, | ||
92 | /// rgba(0, 0, 0, 0.654508) 40%, | ||
93 | /// rgba(0, 0, 0, 0.5) 50%, | ||
94 | /// rgba(0, 0, 0, 0.345492) 60%, | ||
95 | /// rgba(0, 0, 0, 0.206107) 70%, | ||
96 | /// rgba(0, 0, 0, 0.0954915) 80%, | ||
97 | /// rgba(0, 0, 0, 0.0244717) 90%, | ||
98 | /// rgba(0, 0, 0, 3.78257e-11) 100% | ||
99 | /// ); | ||
100 | /// } | ||
101 | /// | ||
102 | /// @example scss - A smoother linear gradient with complex color positions | ||
103 | /// .background { | ||
104 | /// background-image: iro-easing-gradient( | ||
105 | /// linear, | ||
106 | /// to top, | ||
107 | /// #000 20%, | ||
108 | /// in-out-sine, | ||
109 | /// transparent calc(20% + 25em) | ||
110 | /// ); | ||
111 | /// } | ||
112 | /// | ||
113 | /// // Generates: | ||
114 | /// | ||
115 | /// .background { | ||
116 | /// background-image: linear-gradient( | ||
117 | /// to top, | ||
118 | /// black 20%, | ||
119 | /// rgba(0, 0, 0, 0.975528) calc(20% + (20% + 25em - 20%) * 0.1), | ||
120 | /// rgba(0, 0, 0, 0.904508) calc(20% + (20% + 25em - 20%) * 0.2), | ||
121 | /// rgba(0, 0, 0, 0.793893) calc(20% + (20% + 25em - 20%) * 0.3), | ||
122 | /// rgba(0, 0, 0, 0.654508) calc(20% + (20% + 25em - 20%) * 0.4), | ||
123 | /// rgba(0, 0, 0, 0.5) calc(20% + (20% + 25em - 20%) * 0.5), | ||
124 | /// rgba(0, 0, 0, 0.345492) calc(20% + (20% + 25em - 20%) * 0.6), | ||
125 | /// rgba(0, 0, 0, 0.206107) calc(20% + (20% + 25em - 20%) * 0.7), | ||
126 | /// rgba(0, 0, 0, 0.0954915) calc(20% + (20% + 25em - 20%) * 0.8), | ||
127 | /// rgba(0, 0, 0, 0.0244717) calc(20% + (20% + 25em - 20%) * 0.9), | ||
128 | /// transparent calc(20% + 25em)) | ||
129 | /// ); | ||
130 | /// } | ||
131 | /// | ||
132 | @function iro-easing-gradient($type, $dir, $stop, $stops...) { | ||
133 | $pos-template: null; | ||
134 | $stops: iro-list-prepend($stops, $stop); | ||
135 | |||
136 | $last-positioned-stop: 1; | ||
137 | $generated-stops: (); | ||
138 | |||
139 | // | ||
140 | // Generate gradient | ||
141 | // | ||
142 | |||
143 | @for $i from 1 through length($stops) { | ||
144 | $stop: nth($stops, $i); | ||
145 | |||
146 | @if $i == 1 { | ||
147 | @if not iro-easing-gradient-is-color-stop($stop) { | ||
148 | @error 'The first color stop argument must be a color stop.'; | ||
149 | } | ||
150 | |||
151 | @if type-of($stop) == color { | ||
152 | // | ||
153 | // The first color stop is unpositioned. The default position for the first | ||
154 | // color stop is 0, which is explicitly added for easier calculations. | ||
155 | // | ||
156 | |||
157 | $stop: $stop 0; | ||
158 | $stops: set-nth($stops, $i, $stop); | ||
159 | } | ||
160 | |||
161 | $generated-stops: append($generated-stops, iro-str-implode($stop, ' ')); | ||
162 | } @else if iro-easing-gradient-is-positioned-color-stop($stop) or ($i == length($stops)) { | ||
163 | @if not iro-easing-gradient-is-color-stop($stop) { | ||
164 | @error 'The last color stop argument must be a color stop.'; | ||
165 | } | ||
166 | |||
167 | // | ||
168 | // Either the current stops list item is a positioned color stop, or the end of | ||
169 | // the stops list has been reached. | ||
170 | // | ||
171 | |||
172 | @if (type-of($stop) == color) and ($i == length($stops)) { | ||
173 | // | ||
174 | // The current stop is an unpositioned color stop, which means this is the end | ||
175 | // of the stops list. The default position for the last color stop is 100%, which | ||
176 | // is explicitly added for easier calculations. | ||
177 | // | ||
178 | |||
179 | $stop: $stop 100%; | ||
180 | $stops: set-nth($stops, $i, $stop); | ||
181 | } | ||
182 | |||
183 | // | ||
184 | // Now the current color stop is guaranteed to be a positioned color stop. | ||
185 | // | ||
186 | |||
187 | @if $i > $last-positioned-stop + 1 { | ||
188 | // | ||
189 | // There is at least one stops list item (unpositioned color stop or easing function) | ||
190 | // between the last positioned color stop and the current stops list item. Interpolate | ||
191 | // the positions of all stops list items that are color stops. | ||
192 | // | ||
193 | |||
194 | $interpolated-stops: iro-easing-gradient-interpolate-stop-positions( | ||
195 | nth($stops, $last-positioned-stop), | ||
196 | iro-list-slice($stops, $last-positioned-stop + 1, $i - 1), | ||
197 | $stop | ||
198 | ); | ||
199 | |||
200 | $new-stops: join( | ||
201 | iro-list-slice($stops, 1, $last-positioned-stop), | ||
202 | $interpolated-stops | ||
203 | ); | ||
204 | $new-stops: join( | ||
205 | $new-stops, | ||
206 | iro-list-slice($stops, $i) | ||
207 | ); | ||
208 | $stops: $new-stops; | ||
209 | } | ||
210 | |||
211 | // | ||
212 | // Now all color stops between this one and the last positioned one have | ||
213 | // interpolated positions. | ||
214 | // Next task is to perform an easing transition between all color stops that | ||
215 | // have an easing function specified. The rest can be left alone since the | ||
216 | // browser will automatically apply a linear transition between them. | ||
217 | // | ||
218 | |||
219 | $j: $last-positioned-stop + 1; | ||
220 | @while $j <= $i { | ||
221 | $easing: null; | ||
222 | $prev-stop: nth($stops, $j - 1); | ||
223 | $next-stop: nth($stops, $j); | ||
224 | |||
225 | @if not iro-easing-gradient-is-color-stop($next-stop) { | ||
226 | $j: $j + 1; | ||
227 | |||
228 | $easing: $next-stop; | ||
229 | $next-stop: nth($stops, $j); | ||
230 | |||
231 | @if not iro-easing-gradient-is-color-stop($next-stop) { | ||
232 | @error 'There can be at most one interpolation hint between to color stops.'; | ||
233 | } | ||
234 | } | ||
235 | |||
236 | @if $easing != null { | ||
237 | @if type-of($easing) == number { | ||
238 | @error 'Midpoint shifts are not supported.'; | ||
239 | } | ||
240 | |||
241 | $easing-func: null; | ||
242 | $easing-args: (); | ||
243 | |||
244 | @if type-of($easing) == list { | ||
245 | $easing-args: iro-list-slice($easing, 2); | ||
246 | $easing: nth($easing, 1); | ||
247 | } | ||
248 | |||
249 | $generated-stops: join( | ||
250 | $generated-stops, | ||
251 | iro-easing-gradient-ease-stops($prev-stop, $next-stop, $easing, $easing-args) | ||
252 | ); | ||
253 | } @else { | ||
254 | $generated-stops: append($generated-stops, iro-str-implode($next-stop, ' ')); | ||
255 | } | ||
256 | |||
257 | $j: $j + 1; | ||
258 | } | ||
259 | |||
260 | $last-positioned-stop: $i; | ||
261 | } | ||
262 | } | ||
263 | |||
264 | @if $type == 'linear' { | ||
265 | @return linear-gradient($dir, unquote(iro-str-implode($generated-stops, ', '))); | ||
266 | } @else if $type == 'radial' { | ||
267 | @return radial-gradient($dir, unquote(iro-str-implode($generated-stops, ', '))); | ||
268 | } @else { | ||
269 | @error 'Invalid gradient type: #{inspect($type)}.'; | ||
270 | } | ||
271 | } | ||
272 | |||
273 | /// | ||
274 | /// Alias for iro-easing-gradient('linear',...) | ||
275 | /// | ||
276 | /// @see {function} iro-easing-gradient | ||
277 | /// | ||
278 | @function iro-easing-linear-gradient($dir, $stop, $stops...) { | ||
279 | @return iro-easing-gradient('linear', $dir, $stop, $stops...); | ||
280 | } | ||
281 | |||
282 | /// | ||
283 | /// Alias for iro-easing-gradient('radial',...) | ||
284 | /// | ||
285 | /// @see {function} iro-easing-gradient | ||
286 | /// | ||
287 | @function iro-easing-radial-gradient($dir, $stop, $stops...) { | ||
288 | @return iro-easing-gradient('radial', $dir, $stop, $stops...); | ||
289 | } | ||
290 | |||
291 | /// | ||
292 | /// Generate a smooth transition from one color stop to another using the provided easing function. | ||
293 | /// | ||
294 | /// @access private | ||
295 | /// | ||
296 | @function iro-easing-gradient-ease-stops($prev-stop, $next-stop, $easing, $easing-args: ()) { | ||
297 | @if $easing == 'steps' { | ||
298 | $steps: null; | ||
299 | $jump: null; | ||
300 | |||
301 | @if length($easing-args) > 1 { | ||
302 | $steps: nth($easing-args, 1); | ||
303 | $jump: nth($easing-args, 2); | ||
304 | } @else { | ||
305 | $steps: nth($easing-args, 1); | ||
306 | $jump: jump-end; | ||
307 | } | ||
308 | |||
309 | @return iro-easing-gradient-steps-stops($prev-stop, $next-stop, $steps, $jump); | ||
310 | } @else { | ||
311 | $easing-func: null; | ||
312 | @if function-exists('iro-' + $easing) { | ||
313 | $easing-func: get-function('iro-' + $easing); | ||
314 | } @else { | ||
315 | $easing-func: get-function($easing); | ||
316 | } | ||
317 | |||
318 | @return iro-easing-gradient-bezier-stops($prev-stop, $next-stop, $easing-func, $easing-args); | ||
319 | } | ||
320 | } | ||
321 | |||
322 | /// | ||
323 | /// Generate a smooth transition from one color stop to another using the provided cubic-bezier function. | ||
324 | /// | ||
325 | /// @access private | ||
326 | /// | ||
327 | @function iro-easing-gradient-bezier-stops($prev-stop, $next-stop, $easing-func, $easing-args: ()) { | ||
328 | $prev-stop-color: nth($prev-stop, 1); | ||
329 | $prev-stop-pos: nth($prev-stop, 2); | ||
330 | $next-stop-color: nth($next-stop, 1); | ||
331 | $next-stop-pos: nth($next-stop, 2); | ||
332 | |||
333 | $stops: (); | ||
334 | |||
335 | @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) { | ||
336 | // | ||
337 | // The transition color stop positions can be statically calculated. | ||
338 | // | ||
339 | |||
340 | $distance: $next-stop-pos - $prev-stop-pos; | ||
341 | |||
342 | @for $i from 1 through $iro-easing-gradient-steps { | ||
343 | $perc: $i / $iro-easing-gradient-steps; | ||
344 | |||
345 | $color: null; | ||
346 | $pos: $prev-stop-pos + $perc * $distance; | ||
347 | @if $perc == 1 { | ||
348 | $color: $next-stop-color; | ||
349 | } @else { | ||
350 | $color: mix($next-stop-color, $prev-stop-color, call($easing-func, append($easing-args, $perc)...) * 100%); | ||
351 | } | ||
352 | |||
353 | $stops: append($stops, $color + ' ' + $pos); | ||
354 | } | ||
355 | } @else { | ||
356 | // | ||
357 | // The transition color stop positions have to be dynamically calculated with the calc() function. | ||
358 | // | ||
359 | |||
360 | @if type-of($prev-stop-pos) != number { | ||
361 | // must be calc() | ||
362 | @if (type-of($prev-stop-pos) != string) or (str-index($prev-stop-pos, 'calc(') != 1) { | ||
363 | @error 'Invalid color stop position: #{inspect($prev-stop-pos)}'; | ||
364 | } | ||
365 | |||
366 | $prev-stop-pos: str-slice($prev-stop-pos, 6, str-length($prev-stop-pos) - 1); | ||
367 | } | ||
368 | |||
369 | @if type-of($next-stop-pos) != number { | ||
370 | // must be calc() | ||
371 | @if (type-of($next-stop-pos) != string) or (str-index($next-stop-pos, 'calc(') != 1) { | ||
372 | @error 'Invalid color stop position: #{inspect($next-stop-pos)}'; | ||
373 | } | ||
374 | |||
375 | $next-stop-pos: str-slice($next-stop-pos, 6, str-length($next-stop-pos) - 1); | ||
376 | } | ||
377 | |||
378 | @for $i from 1 through $iro-easing-gradient-steps { | ||
379 | $perc: $i / $iro-easing-gradient-steps; | ||
380 | |||
381 | $color: null; | ||
382 | $pos: null; | ||
383 | @if $perc == 1 { | ||
384 | $color: $next-stop-color; | ||
385 | $pos: calc(#{$next-stop-pos}); | ||
386 | } @else { | ||
387 | $color: mix($next-stop-color, $prev-stop-color, call($easing-func, append($easing-args, $perc)...) * 100%); | ||
388 | $pos: calc(#{$prev-stop-pos} + (#{$next-stop-pos} - #{$prev-stop-pos}) * #{$perc}); | ||
389 | } | ||
390 | |||
391 | $stops: append($stops, $color + ' ' + $pos); | ||
392 | } | ||
393 | } | ||
394 | |||
395 | @return $stops; | ||
396 | } | ||
397 | |||
398 | /// | ||
399 | /// Generate a step transition from one color stop to another. | ||
400 | /// | ||
401 | /// @access private | ||
402 | /// | ||
403 | @function iro-easing-gradient-steps-stops($prev-stop, $next-stop, $steps, $jump: jump-end) { | ||
404 | $prev-stop-color: nth($prev-stop, 1); | ||
405 | $prev-stop-pos: nth($prev-stop, 2); | ||
406 | $next-stop-color: nth($next-stop, 1); | ||
407 | $next-stop-pos: nth($next-stop, 2); | ||
408 | |||
409 | $stops: (); | ||
410 | |||
411 | @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) { | ||
412 | // | ||
413 | // The transition color stop positions can be statically calculated. | ||
414 | // | ||
415 | |||
416 | $distance: $next-stop-pos - $prev-stop-pos; | ||
417 | |||
418 | @for $i from 1 through $steps { | ||
419 | $x1: ($i - 1) / $steps; | ||
420 | $x2: $i / $steps; | ||
421 | $y: null; | ||
422 | |||
423 | @if $jump == jump-start { | ||
424 | $y: $i / $steps; | ||
425 | } @else if $jump == jump-end { | ||
426 | $y: ($i - 1) / $steps; | ||
427 | } @else if $jump == jump-both { | ||
428 | $y: $i / ($steps + 1); | ||
429 | } @else if $jump == jump-none { | ||
430 | $y: ($i - 1) / ($steps - 1); | ||
431 | } @else { | ||
432 | @error 'Invalid $jump: #{inspect($jump)}'; | ||
433 | } | ||
434 | |||
435 | $color: null; | ||
436 | $pos1: if($x1 == 0, $prev-stop-pos, $prev-stop-pos + $x1 * $distance); | ||
437 | $pos2: if($x2 == 1, $next-stop-pos, $prev-stop-pos + $x2 * $distance); | ||
438 | |||
439 | @if $y == 0 { | ||
440 | $color: $prev-stop-color; | ||
441 | } @else if $y == 1 { | ||
442 | $color: $next-stop-color; | ||
443 | } @else { | ||
444 | $color: mix($next-stop-color, $prev-stop-color, $y * 100%); | ||
445 | } | ||
446 | |||
447 | $stops: append($stops, $color + ' ' + $pos1); | ||
448 | $stops: append($stops, $color + ' ' + $pos2); | ||
449 | } | ||
450 | } @else { | ||
451 | // | ||
452 | // The transition color stop positions have to be dynamically calculated with the calc() function. | ||
453 | // | ||
454 | |||
455 | @if type-of($prev-stop-pos) != number { | ||
456 | // must be calc() | ||
457 | @if (type-of($prev-stop-pos) != string) or (str-index($prev-stop-pos, 'calc(') != 1) { | ||
458 | @error 'Invalid color stop position: #{inspect($prev-stop-pos)}'; | ||
459 | } | ||
460 | |||
461 | $prev-stop-pos: str-slice($prev-stop-pos, 6, str-length($prev-stop-pos) - 1); | ||
462 | } | ||
463 | |||
464 | @if type-of($next-stop-pos) != number { | ||
465 | // must be calc() | ||
466 | @if (type-of($next-stop-pos) != string) or (str-index($next-stop-pos, 'calc(') != 1) { | ||
467 | @error 'Invalid color stop position: #{inspect($next-stop-pos)}'; | ||
468 | } | ||
469 | |||
470 | $next-stop-pos: str-slice($next-stop-pos, 6, str-length($next-stop-pos) - 1); | ||
471 | } | ||
472 | |||
473 | @for $i from 1 through $steps { | ||
474 | $x1: ($i - 1) / $steps; | ||
475 | $x2: $i / $steps; | ||
476 | $y: null; | ||
477 | |||
478 | @if $jump == jump-start { | ||
479 | $y: $i / $steps; | ||
480 | } @else if $jump == jump-end { | ||
481 | $y: ($i - 1) / $steps; | ||
482 | } @else if $jump == jump-both { | ||
483 | $y: $i / ($steps + 1); | ||
484 | } @else if $jump == jump-none { | ||
485 | $y: ($i - 1) / ($steps - 1); | ||
486 | } @else { | ||
487 | @error 'Invalid $jump: #{inspect($jump)}'; | ||
488 | } | ||
489 | |||
490 | $color: null; | ||
491 | $pos1: if($x1 == 0, $prev-stop-pos, calc(#{$prev-stop-pos} + (#{$next-stop-pos} - #{$prev-stop-pos}) * #{$x1})); | ||
492 | $pos2: if($x2 == 1, $next-stop-pos, calc(#{$prev-stop-pos} + (#{$next-stop-pos} - #{$prev-stop-pos}) * #{$x2})); | ||
493 | |||
494 | @if $y == 0 { | ||
495 | $color: $prev-stop-color; | ||
496 | } @else if $y == 1 { | ||
497 | $color: $next-stop-color; | ||
498 | } @else { | ||
499 | $color: mix($next-stop-color, $prev-stop-color, $y * 100%); | ||
500 | } | ||
501 | |||
502 | $stops: append($stops, $color + ' ' + $pos1); | ||
503 | $stops: append($stops, $color + ' ' + $pos2); | ||
504 | } | ||
505 | } | ||
506 | |||
507 | @return $stops; | ||
508 | } | ||
509 | |||
510 | /// | ||
511 | /// Interpolate the positions of multiple color stops between two color stops whose positions are set. | ||
512 | /// | ||
513 | /// @access private | ||
514 | /// | ||
515 | @function iro-easing-gradient-interpolate-stop-positions($prev-stop, $stops, $next-stop) { | ||
516 | $prev-stop-pos: nth($prev-stop, 2); | ||
517 | $next-stop-pos: nth($next-stop, 2); | ||
518 | |||
519 | $stops-num: 0; | ||
520 | @for $i from 1 through length($stops) { | ||
521 | $stop: nth($stops, $i); | ||
522 | @if iro-easing-gradient-is-color-stop($stop) { | ||
523 | $stops-num: $stops-num + 1; | ||
524 | } | ||
525 | } | ||
526 | |||
527 | $i: 1; | ||
528 | $cur-stop-num: 1; | ||
529 | |||
530 | @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) { | ||
531 | // | ||
532 | // The color stop positions can be statically calculated. | ||
533 | // | ||
534 | |||
535 | $distance: $next-stop-pos - $prev-stop-pos; | ||
536 | |||
537 | @for $i from 1 through length($stops) { | ||
538 | $stop: nth($stops, $i); | ||
539 | @if iro-easing-gradient-is-color-stop($stop) { | ||
540 | $pos: $prev-stop-pos + $distance / ($stops-num + 1) * $cur-stop-num; | ||
541 | $stops: set-nth($stops, $i, $stop $pos); | ||
542 | |||
543 | $cur-stop-num: $cur-stop-num + 1; | ||
544 | } | ||
545 | } | ||
546 | } @else { | ||
547 | // | ||
548 | // The color stop positions have to be dynamically calculated with the calc() function. | ||
549 | // | ||
550 | |||
551 | @if type-of($prev-stop-pos) != number { | ||
552 | // must be calc() | ||
553 | @if (type-of($prev-stop-pos) != string) or (str-index($prev-stop-pos, 'calc(') != 1) { | ||
554 | @error 'Invalid color stop position: #{inspect($prev-stop-pos)}'; | ||
555 | } | ||
556 | |||
557 | $prev-stop-pos: str-slice($prev-stop-pos, 6, str-length($prev-stop-pos) - 1); | ||
558 | } | ||
559 | |||
560 | @if type-of($next-stop-pos) != number { | ||
561 | // must be calc() | ||
562 | @if (type-of($next-stop-pos) != string) or (str-index($next-stop-pos, 'calc(') != 1) { | ||
563 | @error 'Invalid color stop position: #{inspect($next-stop-pos)}'; | ||
564 | } | ||
565 | |||
566 | $next-stop-pos: str-slice($next-stop-pos, 6, str-length($next-stop-pos) - 1); | ||
567 | } | ||
568 | |||
569 | @for $i from 1 through length($stops) { | ||
570 | $stop: nth($stops, $i); | ||
571 | @if iro-easing-gradient-is-color-stop($stop) { | ||
572 | $perc: $cur-stop-num / ($stops-num + 1); | ||
573 | $pos: calc(#{$prev-stop-pos} + (#{$next-stop-pos} - #{$prev-stop-pos}) * #{$perc}); | ||
574 | $stops: set-nth($stops, $i, $stop $pos); | ||
575 | |||
576 | $cur-stop-num: $cur-stop-num + 1; | ||
577 | } | ||
578 | } | ||
579 | } | ||
580 | |||
581 | @return $stops; | ||
582 | } | ||
583 | |||
584 | /// | ||
585 | /// Check if the input is a valid color stop. | ||
586 | /// | ||
587 | /// @access private | ||
588 | /// | ||
589 | @function iro-easing-gradient-is-color-stop($input) { | ||
590 | @return (type-of($input) == color) or iro-easing-gradient-is-positioned-color-stop($input); | ||
591 | } | ||
592 | |||
593 | /// | ||
594 | /// Check if the input is a valid positioned color stop. | ||
595 | /// | ||
596 | /// @access private | ||
597 | /// | ||
598 | @function iro-easing-gradient-is-positioned-color-stop($input) { | ||
599 | @return (type-of($input) == list) and (type-of(nth($input, 1)) == color); | ||
600 | } | ||
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 @@ | |||
1 | //// | ||
2 | /// Harmony. | ||
3 | /// | ||
4 | /// Contains functions to make a design appear more harmonic. | ||
5 | /// | ||
6 | /// @group Harmony | ||
7 | /// | ||
8 | /// @access public | ||
9 | //// | ||
10 | |||
11 | /// | ||
12 | /// Adjust a value to a modular scale. | ||
13 | /// | ||
14 | /// For a more sophisticated solution, check out [modularscale-sass](https://github.com/modularscale/modularscale-sass). | ||
15 | /// | ||
16 | /// @link http://alistapart.com/article/more-meaningful-typography An article about modular scales by Tim Brown | ||
17 | /// | ||
18 | /// @param {number} $times - Number of iterations. If positive, $base will be multiplied with $ratio. If negative, $base will be divided by $ratio. | ||
19 | /// @param {number | list} $base - Single base value or, for a multi-stranded modular scale, a list of base values | ||
20 | /// @param {number} $ratio - Ratio | ||
21 | /// | ||
22 | /// @return {number} | ||
23 | /// | ||
24 | @function iro-harmony-modular-scale($times, $base, $ratio) { | ||
25 | @if type-of($base) == number { | ||
26 | @return $base * iro-math-pow($ratio, $times); | ||
27 | } | ||
28 | |||
29 | $main-base: nth($base, 1); | ||
30 | $norm-bases: (); | ||
31 | |||
32 | @each $b in iro-list-slice($base, 2) { | ||
33 | @if $b > $main-base { | ||
34 | @while $b > $main-base { | ||
35 | $b: $b / $ratio; | ||
36 | } | ||
37 | $b: $b * $ratio; | ||
38 | } @else if $b < $main-base { | ||
39 | @while $b < $main-base { | ||
40 | $b: $b * $ratio; | ||
41 | } | ||
42 | } | ||
43 | |||
44 | $norm-bases: append($norm-bases, $b); | ||
45 | } | ||
46 | |||
47 | $all-bases: append($norm-bases, $main-base); | ||
48 | $all-bases: iro-quicksort($all-bases); | ||
49 | |||
50 | $base-index: $times % length($all-bases) + 1; | ||
51 | $exp: floor($times / length($all-bases)); | ||
52 | |||
53 | @return nth($all-bases, $base-index) * iro-math-pow($ratio, $exp); | ||
54 | } | ||
55 | |||
56 | /// | ||
57 | /// Combine responsive properties with modular scales to achieve responsive modular scales. | ||
58 | /// | ||
59 | /// @param {string | list} $props - Property or list of properties to set | ||
60 | /// @param {number} $times - Number of iterations. See iro-harmony-modular-scale for more information. | ||
61 | /// @param {number} $responsive-map - A map with keys = viewports and values = modular scales | ||
62 | /// @param {bool} $fluid [true] - If enabled, property values will smoothly transition from one viewport to the next | ||
63 | /// | ||
64 | /// @see {function} iro-harmony-modular-scale | ||
65 | /// | ||
66 | /// @example scss - Responsive font sizes between 2 viewports based on modular scales | ||
67 | /// $ms: ( | ||
68 | /// 320px: (1rem 2rem, 1.1), | ||
69 | /// 640px: (1rem 2rem, 1.2) | ||
70 | /// ); | ||
71 | /// | ||
72 | /// h1 { | ||
73 | /// @include iro-responsive-modular-scale(font-size, 3, $ms); | ||
74 | /// } | ||
75 | /// | ||
76 | /// h2 { | ||
77 | /// @include iro-responsive-modular-scale(font-size, 2, $ms); | ||
78 | /// } | ||
79 | /// | ||
80 | /// h3 { | ||
81 | /// @include iro-responsive-modular-scale(font-size, 1, $ms); | ||
82 | /// } | ||
83 | /// | ||
84 | @mixin iro-responsive-modular-scale($props, $times, $responsive-map, $fluid: true) { | ||
85 | $new-map: (); | ||
86 | |||
87 | @each $key, $value in $responsive-map { | ||
88 | $new-map: map-merge($new-map, ( | ||
89 | $key: iro-harmony-modular-scale($times, $value...) | ||
90 | )); | ||
91 | } | ||
92 | |||
93 | @include iro-responsive-property($props, $new-map, $fluid); | ||
94 | } | ||
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 @@ | |||
1 | //// | ||
2 | /// Basic mathematical functions. | ||
3 | /// | ||
4 | /// @group Math functions | ||
5 | /// | ||
6 | /// @access public | ||
7 | //// | ||
8 | |||
9 | /// | ||
10 | /// Perform exponentiation. Only integer exponents are supported. | ||
11 | /// | ||
12 | /// @param {number} $base | ||
13 | /// @param {number} $exp | ||
14 | /// | ||
15 | /// @return {number} | ||
16 | /// | ||
17 | /// @example scss - Exponentiation with a positive exponent | ||
18 | /// $result: iro-math-pow(3, 2); // The value of $result is 3^2 = 9 | ||
19 | /// | ||
20 | /// @example scss - Exponentiation with a negative exponent | ||
21 | /// $result: iro-math-pow(2, -3); // The value of $result is 1/(2^3) = 1/8 | ||
22 | /// | ||
23 | @function iro-math-pow($base, $exp) { | ||
24 | $value: 1; | ||
25 | |||
26 | @if $exp > 0 { | ||
27 | @for $i from 1 through $exp { | ||
28 | $value: $value * $base; | ||
29 | } | ||
30 | } @else if $exp < 0 { | ||
31 | @for $i from 1 through -$exp { | ||
32 | $value: $value / $base; | ||
33 | } | ||
34 | } | ||
35 | |||
36 | @return $value; | ||
37 | } | ||
38 | |||
39 | /// | ||
40 | /// Clamp a number between a minimum and maximum value. | ||
41 | /// | ||
42 | /// @param {number} $value - Value to clamp | ||
43 | /// @param {number} $min - Minimum value | ||
44 | /// @param {number} $max - Maximum value | ||
45 | /// | ||
46 | /// @return {number} | ||
47 | /// | ||
48 | /// @example scss | ||
49 | /// $result: iro-math-clamp(20, 0, 10); // The value of $result is 10 | ||
50 | /// | ||
51 | /// @example scss | ||
52 | /// $result: iro-math-clamp(50, 20, 100); // The value of $result is 50 | ||
53 | /// | ||
54 | @function iro-math-clamp($value, $min, $max) { | ||
55 | @if $value < $min { | ||
56 | @return $min; | ||
57 | } | ||
58 | @if $value > $max { | ||
59 | @return $max; | ||
60 | } | ||
61 | @return $value; | ||
62 | } | ||
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 @@ | |||
1 | //// | ||
2 | /// Property trees. | ||
3 | /// | ||
4 | /// Property trees allow you to organize properties in a tree structure (internally nested maps). | ||
5 | /// The intended use is to store all your properties at the beginning and for the rest of the | ||
6 | /// stylesheet you just get them. | ||
7 | /// | ||
8 | /// @group Property trees | ||
9 | /// | ||
10 | /// @access public | ||
11 | //// | ||
12 | |||
13 | /// | ||
14 | /// The maximum depth of resolved iro-prop-ref() references. | ||
15 | /// | ||
16 | /// @type number | ||
17 | /// | ||
18 | $iro-props-native-assing-max-depth: 2 !default; | ||
19 | |||
20 | /// | ||
21 | /// Indicate if property names must start with two dashes (--). | ||
22 | /// This is required if property trees are also used for native CSS custom properties. | ||
23 | /// | ||
24 | /// @type bool | ||
25 | /// | ||
26 | $iro-props-enforce-double-dashes: true !default; | ||
27 | |||
28 | /// | ||
29 | /// Default tree name to use if no name is specified. | ||
30 | /// | ||
31 | /// @type string | ||
32 | /// | ||
33 | $iro-props-default-tree: 'default' !default; | ||
34 | |||
35 | /// | ||
36 | /// List of all created property trees. | ||
37 | /// | ||
38 | /// @type list | ||
39 | /// | ||
40 | /// @access private | ||
41 | /// | ||
42 | $iro-props-trees: (); | ||
43 | |||
44 | /// | ||
45 | /// Save a property tree. If a tree with the sane name already exists, the trees | ||
46 | /// will be merged. | ||
47 | /// | ||
48 | /// @param {map} $map - Map containing properties | ||
49 | /// @param {string} $tree [$iro-props-default-tree] - ID the map is saved as | ||
50 | /// @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. | ||
51 | /// | ||
52 | @mixin iro-props-save($map, $tree: $iro-props-default-tree, $merge: false) { | ||
53 | $noop: iro-props-save($map, $tree, $merge); | ||
54 | } | ||
55 | |||
56 | /// | ||
57 | /// Save a property tree. | ||
58 | /// | ||
59 | /// @param {map} $map - Map containing properties | ||
60 | /// @param {string} $tree [$iro-props-default-tree] - ID the map is saved as | ||
61 | /// @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. | ||
62 | /// | ||
63 | @function iro-props-save($map, $tree: $iro-props-default-tree, $merge: false) { | ||
64 | $prop-map: null; | ||
65 | |||
66 | @if $iro-props-enforce-double-dashes { | ||
67 | @if not iro-props-validate($map) { | ||
68 | @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.'; | ||
69 | } | ||
70 | } | ||
71 | |||
72 | @if map-has-key($iro-props-trees, $tree) { | ||
73 | @if $merge { | ||
74 | $map: iro-map-merge-recursive(map-get($iro-props-trees, $tree), $map); | ||
75 | } @else { | ||
76 | @error 'Property tree #{inspect($tree)} does already exist.'; | ||
77 | } | ||
78 | } | ||
79 | |||
80 | $iro-props-trees: map-merge($iro-props-trees, ($tree: $map)) !global; | ||
81 | |||
82 | @return null; | ||
83 | } | ||
84 | |||
85 | /// | ||
86 | /// Delete a property tree. | ||
87 | /// | ||
88 | /// @param {string} $tree [$iro-props-default-tree] - ID of the tree to be deleted | ||
89 | /// | ||
90 | @mixin iro-props-delete($tree: $iro-props-default-tree) { | ||
91 | $noop: iro-props-delete($tree); | ||
92 | } | ||
93 | |||
94 | /// | ||
95 | /// Unset a property tree. | ||
96 | /// | ||
97 | /// @param {string} $tree [$iro-props-default-tree] - ID of the tree to be deleted | ||
98 | /// | ||
99 | /// @throw If the property tree does not exist | ||
100 | /// | ||
101 | @function iro-props-delete($tree: $iro-props-default-tree) { | ||
102 | @if not map-has-key($iro-props-trees, $tree) { | ||
103 | @error 'Property tree "#{inspect($tree)}" does not exist.'; | ||
104 | } | ||
105 | |||
106 | $iro-props-trees: map-remove($iro-props-trees, $tree) !global; | ||
107 | |||
108 | @return null; | ||
109 | } | ||
110 | |||
111 | /// | ||
112 | /// Access a whole property or a subsection (i.e. value) of it. | ||
113 | /// | ||
114 | /// @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. | ||
115 | /// @param {string} $tree [$iro-props-default-tree] - ID of the property tree to use | ||
116 | /// @param {any} $default [null] - Default value to return of no match was found. If null, this function will throw an error instead. | ||
117 | /// | ||
118 | /// @return {any} Value assigned to property or $default | ||
119 | /// | ||
120 | /// @throw If there was no match for $key and $default is null | ||
121 | /// | ||
122 | @function iro-props-get($key: (), $tree: $iro-props-default-tree, $default: null) { | ||
123 | @if not map-has-key($iro-props-trees, $tree) { | ||
124 | @error 'Unknown tree "#{$tree}".'; | ||
125 | } | ||
126 | |||
127 | $result: map-get($iro-props-trees, $tree); | ||
128 | |||
129 | @if type-of($key) == list { | ||
130 | $stop: false; | ||
131 | |||
132 | @each $k in $key { | ||
133 | @if map-has-key($result, $k) and not $stop { | ||
134 | $result: map-get($result, $k); | ||
135 | |||
136 | @if type-of($result) == list and nth($result, 1) == 'iro-prop-ref' { | ||
137 | @if length($result) == 2 { | ||
138 | $result: iro-props-get($tree: nth($result, 2)); | ||
139 | } @else { | ||
140 | $result: iro-props-get(nth($result, 3), nth($result, 2)); | ||
141 | } | ||
142 | } | ||
143 | } @else { | ||
144 | $stop: true; | ||
145 | } | ||
146 | } | ||
147 | |||
148 | @if $stop { | ||
149 | $result: null; | ||
150 | } | ||
151 | } @else { | ||
152 | $result: map-get($result, $key); | ||
153 | |||
154 | @if type-of($result) == list and nth($result, 1) == 'iro-prop-ref' { | ||
155 | @if length($result) == 2 { | ||
156 | $result: iro-props-get($tree: nth($result, 2)); | ||
157 | } @else { | ||
158 | $result: iro-props-get(nth($result, 3), nth($result, 2)); | ||
159 | } | ||
160 | } | ||
161 | } | ||
162 | |||
163 | @if $result == null { | ||
164 | @if $default == null { | ||
165 | @error '"#{$key}" is null.'; | ||
166 | } @else { | ||
167 | @return $default; | ||
168 | } | ||
169 | } | ||
170 | |||
171 | @return $result; | ||
172 | } | ||
173 | |||
174 | /// | ||
175 | /// Generate a var() function call to get native CSS custom property. | ||
176 | /// | ||
177 | /// @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. | ||
178 | /// @param {string | null} $tree [null] - Optional tree to check if the property actually exists. | ||
179 | /// @param {any} $default [null] - Default value to return of no match was found. | ||
180 | /// | ||
181 | /// @return {string} var() | ||
182 | /// | ||
183 | @function iro-props-get-native($key, $tree: null, $default: null) { | ||
184 | @if $tree != null { | ||
185 | $noop: iro-props-get($key, $tree, $default); | ||
186 | } | ||
187 | |||
188 | $native-var: ''; | ||
189 | |||
190 | @if type-of($key) == list { | ||
191 | @each $subkey in $key { | ||
192 | $native-var: $native-var + $subkey; | ||
193 | } | ||
194 | } @else { | ||
195 | $native-var: $key; | ||
196 | } | ||
197 | |||
198 | @if $default == null { | ||
199 | @return var(#{$native-var}); | ||
200 | } @else { | ||
201 | @return var(#{$native-var}, #{$default}); | ||
202 | } | ||
203 | } | ||
204 | |||
205 | /// | ||
206 | /// Generate assignments for native CSS custom properties with the values from the specified tree. | ||
207 | /// | ||
208 | /// @param {string} $tree [$iro-props-default-tree] - ID of the property tree to use | ||
209 | /// @param {string} $root [()] - Sub-tree to use for assignment | ||
210 | /// | ||
211 | @mixin iro-props-assign-native($tree: $iro-props-default-tree, $root: (), $skip: ()) { | ||
212 | $map: iro-props-get($root, $tree); | ||
213 | $map: map-remove($map, $skip...); | ||
214 | |||
215 | @include iro-props-assign-native-internal($map); | ||
216 | } | ||
217 | |||
218 | /// | ||
219 | /// @access private | ||
220 | /// | ||
221 | @mixin iro-props-assign-native-internal($map, $prefix: '', $ref-depth: $iro-props-native-assing-max-depth) { | ||
222 | @each $key, $value in $map { | ||
223 | $rd: $ref-depth; | ||
224 | @if type-of($value) == list and nth($value, 1) == 'iro-prop-ref' { | ||
225 | @if $ref-depth != 0 { | ||
226 | $rd: $rd - 1; | ||
227 | @if length($value) == 2 { | ||
228 | $value: iro-props-get($tree: nth($value, 2)); | ||
229 | } @else { | ||
230 | $value: iro-props-get(nth($value, 3), nth($value, 2)); | ||
231 | } | ||
232 | } @else { | ||
233 | $value: null; | ||
234 | } | ||
235 | } | ||
236 | @if type-of($value) != map { | ||
237 | #{$prefix + $key}: #{$value}; | ||
238 | } @else { | ||
239 | @include iro-props-assign-native-internal($value, $prefix + $key, $rd); | ||
240 | } | ||
241 | } | ||
242 | } | ||
243 | |||
244 | /// | ||
245 | /// Validate property names. | ||
246 | /// | ||
247 | /// @access private | ||
248 | /// | ||
249 | @function iro-props-validate($map) { | ||
250 | @each $key, $value in $map { | ||
251 | @if str-index($key, '--') != 1 { | ||
252 | @return false; | ||
253 | } | ||
254 | |||
255 | @if type-of($value) == map { | ||
256 | @if not iro-props-validate($value) { | ||
257 | @return false; | ||
258 | } | ||
259 | } | ||
260 | } | ||
261 | |||
262 | @return true; | ||
263 | } | ||
264 | |||
265 | /// | ||
266 | /// Generate a reference to another tree. Dereferencing is lazy, so you may specify a tree that hasn't been created yet. | ||
267 | /// | ||
268 | /// @param {string} $tree [$iro-props-default-tree] - ID of the property tree to use | ||
269 | /// @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. | ||
270 | /// | ||
271 | /// @return {list} A special list that let's Ignis know that this is a lazy value. | ||
272 | /// | ||
273 | /// @throw If there was no match for $key and $default is null | ||
274 | /// | ||
275 | @function iro-props-ref($tree: $iro-props-default-tree, $key: null) { | ||
276 | @if $key == null { | ||
277 | @return ('iro-prop-ref' $tree); | ||
278 | } @else { | ||
279 | @return ('iro-prop-ref' $tree $key); | ||
280 | } | ||
281 | } | ||
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 @@ | |||
1 | //// | ||
2 | /// responsive properties. | ||
3 | /// | ||
4 | /// The mixins and functions in this file allow you to scale any px- or rem-based value depending on | ||
5 | /// the available viewport width. One popular use case is the dynamic scaling of fonts. | ||
6 | /// | ||
7 | /// The code in this file is based on an article by Niklas Postulart: | ||
8 | /// http://niklaspostulart.de/2015/10/sass-responsive-type-mixin | ||
9 | /// | ||
10 | /// The following adjustments were made: | ||
11 | /// - Support any property passed by the user, not just font-size | ||
12 | /// - Allow multiple target viewports / values | ||
13 | /// - Provide a variant of the mixin which integrates include-media for media queries | ||
14 | /// | ||
15 | /// @group Responsive | ||
16 | /// | ||
17 | /// @access public | ||
18 | //// | ||
19 | |||
20 | /// | ||
21 | /// If true, named viewports will be supported if a compatible $breakpoints map exists. | ||
22 | /// This is the case for [include-media](https://include-media.com/), for example. | ||
23 | /// | ||
24 | /// @type bool | ||
25 | /// | ||
26 | $iro-responsive-support-named-viewports: true !default; | ||
27 | |||
28 | /// | ||
29 | /// Context ID used for responsive environment-related mixins. | ||
30 | /// | ||
31 | /// @type string | ||
32 | /// | ||
33 | $iro-responsive-context-id: 'responsive' !default; | ||
34 | |||
35 | /// | ||
36 | /// Scale a property uniformly between a specific set of target viewports / values. | ||
37 | /// | ||
38 | /// @param {string | list} $props - Property or list of properties to set | ||
39 | /// @param {number} $responsive-map - A map with keys = viewports and values = target value | ||
40 | /// @param {bool} $fluid [true] - If enabled, property values will smoothly transition from one viewport to the next | ||
41 | /// @param {bool} $vertical [false] - If enabled, property viewport height will be used instead of viewport width | ||
42 | /// | ||
43 | /// @example scss - Responsive font-size between 2 viewports | ||
44 | /// .something { | ||
45 | /// @include iro-responsive-property(font-size, ( 320px: 20px, 720px: 30px )); | ||
46 | /// } | ||
47 | /// | ||
48 | /// // Generates: | ||
49 | /// | ||
50 | /// @media (min-width: 320px) and (max-width: 720px) { | ||
51 | /// .something { | ||
52 | /// font-size: calc(20px + 10 * ((100vw - 20px) / 400)); | ||
53 | /// } | ||
54 | /// } | ||
55 | /// | ||
56 | /// @media (max-width: 320px) { | ||
57 | /// .something { | ||
58 | /// font-size: 20px; | ||
59 | /// } | ||
60 | /// } | ||
61 | /// | ||
62 | /// @media (min-width: 720px) { | ||
63 | /// .something { | ||
64 | /// font-size: 30px; | ||
65 | /// } | ||
66 | /// } | ||
67 | /// | ||
68 | /// @example scss - Responsive font-size between 3 viewports | ||
69 | /// .something { | ||
70 | /// @include iro-responsive-property(font-size, ( 320px: 20px, 720px: 30px, 1280px: 40px )); | ||
71 | /// } | ||
72 | /// | ||
73 | /// // Generates: | ||
74 | /// | ||
75 | /// @media (min-width: 320px) and (max-width: 720px) { | ||
76 | /// .something { | ||
77 | /// font-size: calc(20px + 10 * ((100vw - 20px) / 400)); | ||
78 | /// } | ||
79 | /// } | ||
80 | /// | ||
81 | /// @media (min-width: 720px) and (max-width: 1280px) { | ||
82 | /// .something { | ||
83 | /// font-size: calc(30px + 10 * ((100vw - 30px) / 400)); | ||
84 | /// } | ||
85 | /// } | ||
86 | /// | ||
87 | /// @media (max-width: 320px) { | ||
88 | /// .something { | ||
89 | /// font-size: 20px; | ||
90 | /// } | ||
91 | /// } | ||
92 | /// | ||
93 | /// @media (min-width: 720px) { | ||
94 | /// .something { | ||
95 | /// font-size: 30px; | ||
96 | /// } | ||
97 | /// } | ||
98 | /// | ||
99 | @mixin iro-responsive-property($props, $responsive-map, $fluid: true, $vertical: false) { | ||
100 | @include iro-responsive-env(map-keys($responsive-map), $fluid, $vertical) { | ||
101 | @if type-of($props) == list { | ||
102 | @each $prop in $props { | ||
103 | #{$prop}: iro-responsive-set(map-values($responsive-map)); | ||
104 | } | ||
105 | } @else { | ||
106 | #{$props}: iro-responsive-set(map-values($responsive-map)); | ||
107 | } | ||
108 | } | ||
109 | } | ||
110 | |||
111 | /// | ||
112 | /// Create a new responsive environment by specifying a set of viewports. | ||
113 | /// Inside a responsive environment, use the iro-responsive-set function to make a property scale automatically. | ||
114 | /// | ||
115 | /// @param {list} $viewports - Viewports sorted in ascending order | ||
116 | /// @param {bool} $fluid [true] - If enabled, property values will smoothly transition from one viewport to the next | ||
117 | /// @param {bool} $vertical [false] - If enabled, property viewport height will be used instead of viewport width | ||
118 | /// | ||
119 | /// @content | ||
120 | /// | ||
121 | /// @see {function} iro-responsive-set | ||
122 | /// | ||
123 | /// @example scss - Responsive font-size between 2 viewports | ||
124 | /// .something { | ||
125 | /// @include iro-responsive-env((320px, 720px)) { | ||
126 | /// font-size: iro-responsive-set(20px, 30px); | ||
127 | /// } | ||
128 | /// } | ||
129 | /// | ||
130 | /// // Generates: | ||
131 | /// | ||
132 | /// @media (min-width: 320px) and (max-width: 720px) { | ||
133 | /// .something { | ||
134 | /// font-size: calc(20px + 10 * ((100vw - 20px) / 400)); | ||
135 | /// } | ||
136 | /// } | ||
137 | /// | ||
138 | /// @media (max-width: 320px) { | ||
139 | /// .something { | ||
140 | /// font-size: 20px; | ||
141 | /// } | ||
142 | /// } | ||
143 | /// | ||
144 | /// @media (min-width: 720px) { | ||
145 | /// .something { | ||
146 | /// font-size: 30px; | ||
147 | /// } | ||
148 | /// } | ||
149 | /// | ||
150 | @mixin iro-responsive-env($viewports, $fluid: true, $vertical: false) { | ||
151 | @if length($viewports) <= 1 { | ||
152 | @error '$viewports must contain at least two viewports.'; | ||
153 | } | ||
154 | |||
155 | $new-viewports: (); | ||
156 | |||
157 | @each $viewport in $viewports { | ||
158 | @if $iro-responsive-support-named-viewports and global-variable-exists(breakpoints) { | ||
159 | @if map-has-key($breakpoints, $viewport) { | ||
160 | $viewport: map-get($breakpoints, $viewport); | ||
161 | } | ||
162 | } | ||
163 | |||
164 | @if (type-of($viewport) != number) or unitless($viewport) { | ||
165 | @error '$viewports contains invalid viewports.'; | ||
166 | } | ||
167 | |||
168 | $new-viewports: append($new-viewports, $viewport); | ||
169 | } | ||
170 | |||
171 | $viewports: iro-quicksort($new-viewports); | ||
172 | |||
173 | @if $new-viewports != $viewports { | ||
174 | @error '$viewports was not sorted in ascending order.'; | ||
175 | } | ||
176 | |||
177 | @if $fluid { | ||
178 | $first-vp: nth($viewports, 1); | ||
179 | $last-vp: nth($viewports, length($viewports)); | ||
180 | |||
181 | @include iro-context-push($iro-responsive-context-id, 'env', ( | ||
182 | 'viewports': $viewports, | ||
183 | 'mode': set, | ||
184 | 'index': 1, | ||
185 | 'fluid': $fluid, | ||
186 | 'vertical': $vertical, | ||
187 | )); | ||
188 | |||
189 | @content; | ||
190 | |||
191 | @include iro-context-pop($iro-responsive-context-id); | ||
192 | |||
193 | @for $i from 1 to length($viewports) { | ||
194 | $prev-vp: nth($viewports, $i); | ||
195 | $next-vp: nth($viewports, $i + 1); | ||
196 | |||
197 | @if not $vertical { | ||
198 | @media (min-width: $prev-vp) and (max-width: $next-vp) { | ||
199 | @include iro-context-push($iro-responsive-context-id, 'env', ( | ||
200 | 'viewports': $viewports, | ||
201 | 'mode': transition, | ||
202 | 'index': $i, | ||
203 | 'fluid': $fluid, | ||
204 | 'vertical': $vertical, | ||
205 | )); | ||
206 | |||
207 | @content; | ||
208 | |||
209 | @include iro-context-pop($iro-responsive-context-id); | ||
210 | } | ||
211 | } @else { | ||
212 | @media (min-height: $prev-vp) and (max-height: $next-vp) { | ||
213 | @include iro-context-push($iro-responsive-context-id, 'env', ( | ||
214 | 'viewports': $viewports, | ||
215 | 'mode': transition, | ||
216 | 'index': $i, | ||
217 | 'fluid': $fluid, | ||
218 | 'vertical': $vertical, | ||
219 | )); | ||
220 | |||
221 | @content; | ||
222 | |||
223 | @include iro-context-pop($iro-responsive-context-id); | ||
224 | } | ||
225 | } | ||
226 | } | ||
227 | |||
228 | @if not $vertical { | ||
229 | @media (min-width: $last-vp) { | ||
230 | @include iro-context-push($iro-responsive-context-id, 'env', ( | ||
231 | 'viewports': $viewports, | ||
232 | 'mode': set, | ||
233 | 'index': length($viewports), | ||
234 | 'fluid': $fluid, | ||
235 | 'vertical': $vertical, | ||
236 | )); | ||
237 | |||
238 | @content; | ||
239 | |||
240 | @include iro-context-pop($iro-responsive-context-id); | ||
241 | } | ||
242 | } @else { | ||
243 | @media (min-height: $last-vp) { | ||
244 | @include iro-context-push($iro-responsive-context-id, 'env', ( | ||
245 | 'viewports': $viewports, | ||
246 | 'mode': set, | ||
247 | 'index': length($viewports), | ||
248 | 'fluid': $fluid, | ||
249 | 'vertical': $vertical, | ||
250 | )); | ||
251 | |||
252 | @content; | ||
253 | |||
254 | @include iro-context-pop($iro-responsive-context-id); | ||
255 | } | ||
256 | } | ||
257 | } @else { | ||
258 | @include iro-context-push($iro-responsive-context-id, 'env', ( | ||
259 | 'viewports': $viewports, | ||
260 | 'mode': set, | ||
261 | 'index': 1, | ||
262 | 'fluid': $fluid, | ||
263 | 'vertical': $vertical, | ||
264 | )); | ||
265 | |||
266 | @content; | ||
267 | |||
268 | @include iro-context-pop($iro-responsive-context-id); | ||
269 | |||
270 | @for $i from 2 through length($viewports) { | ||
271 | $vp: nth($viewports, $i); | ||
272 | |||
273 | @if not $vertical { | ||
274 | @media (min-width: $vp) { | ||
275 | @include iro-context-push($iro-responsive-context-id, 'env', ( | ||
276 | 'viewports': $viewports, | ||
277 | 'mode': set, | ||
278 | 'index': $i | ||
279 | )); | ||
280 | |||
281 | @content; | ||
282 | |||
283 | @include iro-context-pop($iro-responsive-context-id); | ||
284 | } | ||
285 | } @else { | ||
286 | @media (min-height: $vp) { | ||
287 | @include iro-context-push($iro-responsive-context-id, 'env', ( | ||
288 | 'viewports': $viewports, | ||
289 | 'mode': set, | ||
290 | 'index': $i | ||
291 | )); | ||
292 | |||
293 | @content; | ||
294 | |||
295 | @include iro-context-pop($iro-responsive-context-id); | ||
296 | } | ||
297 | } | ||
298 | } | ||
299 | } | ||
300 | } | ||
301 | |||
302 | /// | ||
303 | /// Make a property scale automatically inside a responsive environment. | ||
304 | /// | ||
305 | /// @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. | ||
306 | /// | ||
307 | /// @return {number|string} | ||
308 | /// | ||
309 | @function iro-responsive-set($values, $without-calc: false) { | ||
310 | $noop: iro-context-assert-stack-must-contain($iro-responsive-context-id, 'env'); | ||
311 | |||
312 | $data: nth(iro-context-get($iro-responsive-context-id, 'env'), 2); | ||
313 | $viewports: map-get($data, 'viewports'); | ||
314 | $mode: map-get($data, 'mode'); | ||
315 | $fluid: map-get($data, 'fluid'); | ||
316 | $vertical: map-get($data, 'vertical'); | ||
317 | |||
318 | @if length($values) != length($viewports) { | ||
319 | @error '$values must contain the same number of items as the responsive environment\'s $viewports.'; | ||
320 | } | ||
321 | |||
322 | @if $mode == set { | ||
323 | @return nth($values, map-get($data, 'index')); | ||
324 | } @else { | ||
325 | $index: map-get($data, 'index'); | ||
326 | $prev-vp: nth($viewports, $index); | ||
327 | $next-vp: nth($viewports, $index + 1); | ||
328 | $prev-value: nth($values, $index); | ||
329 | $next-value: nth($values, $index + 1); | ||
330 | |||
331 | @return iro-responsive-fluid-calc($prev-value, $next-value, $prev-vp, $next-vp, $vertical, $without-calc); | ||
332 | } | ||
333 | } | ||
334 | |||
335 | /// | ||
336 | /// Generate the calc() function that uniformly scales a value from $min-value to $max-value depending | ||
337 | /// on the viewport width. | ||
338 | /// | ||
339 | /// @param {number} $min-value - Minimum value | ||
340 | /// @param {number} $max-value - Maximum value | ||
341 | /// @param {number} $min-viewport - Minimum viewport size | ||
342 | /// @param {number} $max-viewport - Maximum viewport size | ||
343 | /// @param {bool} $vertical [false] - If enabled, property viewport height will be used instead of viewport width | ||
344 | /// | ||
345 | /// @access private | ||
346 | /// | ||
347 | @function iro-responsive-fluid-calc($min-value, $max-value, $min-viewport, $max-viewport, $vertical: false, $without-calc: false) { | ||
348 | $value-unit: unit($min-value); | ||
349 | $max-value-unit: unit($max-value); | ||
350 | $viewport-unit: unit($min-viewport); | ||
351 | $max-viewport-unit: unit($max-viewport); | ||
352 | |||
353 | @if $min-value == 0 { | ||
354 | $value-unit: $max-value-unit; | ||
355 | } | ||
356 | @if $max-value == 0 { | ||
357 | $max-value-unit: $value-unit; | ||
358 | } | ||
359 | @if $min-viewport == 0 { | ||
360 | $viewport-unit: $max-viewport-unit; | ||
361 | } | ||
362 | @if $max-viewport == 0 { | ||
363 | $max-viewport-unit: $viewport-unit; | ||
364 | } | ||
365 | |||
366 | @if ($value-unit != $max-value-unit) or ($viewport-unit != $max-viewport-unit) { | ||
367 | @error 'Units of $min-value and $max-value, $min-viewport and $max-viewport must match.'; | ||
368 | } | ||
369 | |||
370 | @if ($value-unit == rem) and ($viewport-unit == px) { | ||
371 | $min-viewport: iro-px-to-rem($min-viewport); | ||
372 | $max-viewport: iro-px-to-rem($max-viewport); | ||
373 | $viewport-unit: rem; | ||
374 | } @else if ($value-unit == px) and ($viewport-unit == rem) { | ||
375 | $min-value: iro-px-to-rem($min-value); | ||
376 | $max-value: iro-px-to-rem($max-value); | ||
377 | $value-unit: rem; | ||
378 | } | ||
379 | |||
380 | @if $value-unit != $viewport-unit { | ||
381 | @error 'This combination of units is not supported.'; | ||
382 | } | ||
383 | |||
384 | $value-diff: iro-strip-unit($max-value - $min-value); | ||
385 | $viewport-diff: iro-strip-unit($max-viewport - $min-viewport); | ||
386 | |||
387 | $calc: ''; | ||
388 | |||
389 | @if $min-value != 0 { | ||
390 | $calc: '#{$min-value} + '; | ||
391 | } | ||
392 | |||
393 | @if not $vertical { | ||
394 | $calc: unquote('#{$calc}#{$value-diff} * ((100vw - #{$min-viewport}) / #{$viewport-diff})'); | ||
395 | } @else { | ||
396 | $calc: unquote('#{$calc}#{$value-diff} * ((100vh - #{$min-viewport}) / #{$viewport-diff})'); | ||
397 | } | ||
398 | |||
399 | @if $without-calc { | ||
400 | @return $calc; | ||
401 | } @else { | ||
402 | @return calc(#{$calc}); | ||
403 | } | ||
404 | } | ||
405 | |||
406 | @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 @@ | |||
1 | //// | ||
2 | /// Variables. | ||
3 | /// | ||
4 | /// Global variables that are used throughout the framework. | ||
5 | /// | ||
6 | /// @group Global variables | ||
7 | /// | ||
8 | /// @access public | ||
9 | //// | ||
10 | |||
11 | /// | ||
12 | /// Reference root font size in px that is used for px -> rem conversions. | ||
13 | /// | ||
14 | /// @type number | ||
15 | /// | ||
16 | $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 @@ | |||
1 | //// | ||
2 | /// Shorter version of the bem-related mixins. Useful to reduce clutter. | ||
3 | /// | ||
4 | /// @group BEM shortcodes | ||
5 | /// | ||
6 | /// @access public | ||
7 | //// | ||
8 | |||
9 | /// | ||
10 | /// @alias iro-bem-block | ||
11 | /// | ||
12 | @mixin block($name, $type: null) { | ||
13 | @include iro-bem-block($name, $type: null) { | ||
14 | @content; | ||
15 | } | ||
16 | } | ||
17 | |||
18 | /// | ||
19 | /// @alias block | ||
20 | /// | ||
21 | @mixin b($name, $type: null) { | ||
22 | @include block($name, $type: null) { | ||
23 | @content; | ||
24 | } | ||
25 | } | ||
26 | |||
27 | /// | ||
28 | /// @alias iro-bem-object | ||
29 | /// | ||
30 | @mixin object($name) { | ||
31 | @include iro-bem-object($name) { | ||
32 | @content; | ||
33 | } | ||
34 | } | ||
35 | |||
36 | /// | ||
37 | /// @alias object | ||
38 | /// | ||
39 | @mixin ob($name) { | ||
40 | @include object($name) { | ||
41 | @content; | ||
42 | } | ||
43 | } | ||
44 | |||
45 | /// | ||
46 | /// @alias iro-bem-component | ||
47 | /// | ||
48 | @mixin component($name) { | ||
49 | @include iro-bem-component($name) { | ||
50 | @content; | ||
51 | } | ||
52 | } | ||
53 | |||
54 | /// | ||
55 | /// @alias component | ||
56 | /// | ||
57 | @mixin cb($name) { | ||
58 | @include component($name) { | ||
59 | @content; | ||
60 | } | ||
61 | } | ||
62 | |||
63 | /// | ||
64 | /// @alias iro-bem-layout | ||
65 | /// | ||
66 | @mixin layout($name) { | ||
67 | @include iro-bem-layout($name) { | ||
68 | @content; | ||
69 | } | ||
70 | } | ||
71 | |||
72 | /// | ||
73 | /// @alias layout | ||
74 | /// | ||
75 | @mixin lb($name) { | ||
76 | @include layout($name) { | ||
77 | @content; | ||
78 | } | ||
79 | } | ||
80 | |||
81 | /// | ||
82 | /// @alias iro-bem-utility | ||
83 | /// | ||
84 | @mixin utility($name) { | ||
85 | @include iro-bem-utility($name) { | ||
86 | @content; | ||
87 | } | ||
88 | } | ||
89 | |||
90 | /// | ||
91 | /// @alias utility | ||
92 | /// | ||
93 | @mixin ub($name) { | ||
94 | @include utility($name) { | ||
95 | @content; | ||
96 | } | ||
97 | } | ||
98 | |||
99 | /// | ||
100 | /// @alias iro-bem-scope | ||
101 | /// | ||
102 | @mixin scope($name) { | ||
103 | @include iro-bem-scope($name) { | ||
104 | @content; | ||
105 | } | ||
106 | } | ||
107 | |||
108 | /// | ||
109 | /// @alias scope | ||
110 | /// | ||
111 | @mixin sb($name) { | ||
112 | @include scope($name) { | ||
113 | @content; | ||
114 | } | ||
115 | } | ||
116 | |||
117 | /// | ||
118 | /// @alias iro-bem-theme | ||
119 | /// | ||
120 | @mixin theme($name) { | ||
121 | @include iro-bem-theme($name) { | ||
122 | @content; | ||
123 | } | ||
124 | } | ||
125 | |||
126 | /// | ||
127 | /// @alias theme | ||
128 | /// | ||
129 | @mixin tb($name) { | ||
130 | @include theme($name) { | ||
131 | @content; | ||
132 | } | ||
133 | } | ||
134 | |||
135 | /// | ||
136 | /// @alias iro-bem-js | ||
137 | /// | ||
138 | @mixin js($name) { | ||
139 | @include iro-bem-js($name) { | ||
140 | @content; | ||
141 | } | ||
142 | } | ||
143 | |||
144 | /// | ||
145 | /// @alias iro-bem-qa | ||
146 | /// | ||
147 | @mixin qa($name) { | ||
148 | @include iro-bem-qa($name) { | ||
149 | @content; | ||
150 | } | ||
151 | } | ||
152 | |||
153 | /// | ||
154 | /// @alias iro-bem-hack | ||
155 | /// | ||
156 | @mixin hack($name) { | ||
157 | @include iro-bem-hack($name) { | ||
158 | @content; | ||
159 | } | ||
160 | } | ||
161 | |||
162 | /// | ||
163 | /// @alias iro-bem-composed-of | ||
164 | /// | ||
165 | @mixin composed-of($block, $blocks...) { | ||
166 | @include iro-bem-composed-of($block, $blocks...) { | ||
167 | @content; | ||
168 | } | ||
169 | } | ||
170 | |||
171 | /// | ||
172 | /// @alias composed-of | ||
173 | /// | ||
174 | @mixin co($block, $blocks...) { | ||
175 | @include composed-of($block, $blocks...) { | ||
176 | @content; | ||
177 | } | ||
178 | } | ||
179 | |||
180 | /// | ||
181 | /// @alias iro-bem-element | ||
182 | /// | ||
183 | @mixin element($name, $names...) { | ||
184 | @include iro-bem-element($name, $names...) { | ||
185 | @content; | ||
186 | } | ||
187 | } | ||
188 | |||
189 | /// | ||
190 | /// @alias element | ||
191 | /// | ||
192 | @mixin e($name, $names...) { | ||
193 | @include element($name, $names...) { | ||
194 | @content; | ||
195 | } | ||
196 | } | ||
197 | |||
198 | /// | ||
199 | /// @alias iro-bem-related-element | ||
200 | /// | ||
201 | @mixin related-element($sign, $name, $names...) { | ||
202 | @include iro-bem-related-element($sign, $name, $names...) { | ||
203 | @content; | ||
204 | } | ||
205 | } | ||
206 | |||
207 | /// | ||
208 | /// @alias related-element | ||
209 | /// | ||
210 | @mixin re($sign, $name, $names...) { | ||
211 | @include related-element($sign, $name, $names...) { | ||
212 | @content; | ||
213 | } | ||
214 | } | ||
215 | |||
216 | /// | ||
217 | /// @alias iro-bem-sibling-element | ||
218 | /// | ||
219 | @mixin sibling-element($name, $names...) { | ||
220 | @include iro-bem-sibling-element($name, $names...) { | ||
221 | @content; | ||
222 | } | ||
223 | } | ||
224 | |||
225 | /// | ||
226 | /// @alias sibling-element | ||
227 | /// | ||
228 | @mixin se($name, $names...) { | ||
229 | @include sibling-element($name, $names...) { | ||
230 | @content; | ||
231 | } | ||
232 | } | ||
233 | |||
234 | /// | ||
235 | /// @alias iro-bem-next-element | ||
236 | /// | ||
237 | @mixin next-element($name, $names...) { | ||
238 | @include iro-bem-next-element($name, $names...) { | ||
239 | @content; | ||
240 | } | ||
241 | } | ||
242 | |||
243 | /// | ||
244 | /// @alias next-element | ||
245 | /// | ||
246 | @mixin ne($name, $names...) { | ||
247 | @include next-element($name, $names...) { | ||
248 | @content; | ||
249 | } | ||
250 | } | ||
251 | |||
252 | /// | ||
253 | /// @alias iro-bem-next-twin-element | ||
254 | /// | ||
255 | @mixin next-twin-element { | ||
256 | @include iro-bem-next-twin-element { | ||
257 | @content; | ||
258 | } | ||
259 | } | ||
260 | |||
261 | /// | ||
262 | /// @alias next-twin-element | ||
263 | /// | ||
264 | @mixin te { | ||
265 | @include next-twin-element { | ||
266 | @content; | ||
267 | } | ||
268 | } | ||
269 | |||
270 | /// | ||
271 | /// @alias iro-bem-modifier | ||
272 | /// | ||
273 | @mixin modifier($name, $names...) { | ||
274 | @include iro-bem-modifier($name, $names...) { | ||
275 | @content; | ||
276 | } | ||
277 | } | ||
278 | |||
279 | /// | ||
280 | /// @alias modifier | ||
281 | /// | ||
282 | @mixin m($name, $names...) { | ||
283 | @include modifier($name, $names...) { | ||
284 | @content; | ||
285 | } | ||
286 | } | ||
287 | |||
288 | /// | ||
289 | /// @alias iro-bem-suffix | ||
290 | /// | ||
291 | @mixin suffix($name) { | ||
292 | @include iro-bem-suffix($name) { | ||
293 | @content; | ||
294 | } | ||
295 | } | ||
296 | |||
297 | /// | ||
298 | /// @alias suffix | ||
299 | /// | ||
300 | @mixin s($name) { | ||
301 | @include suffix($name) { | ||
302 | @content; | ||
303 | } | ||
304 | } | ||
305 | |||
306 | /// | ||
307 | /// @alias iro-bem-is | ||
308 | /// | ||
309 | @mixin is($state, $states...) { | ||
310 | @include iro-bem-is($state, $states...) { | ||
311 | @content; | ||
312 | } | ||
313 | } | ||
314 | |||
315 | /// | ||
316 | /// @alias iro-bem-has | ||
317 | /// | ||
318 | @mixin has($state, $states...) { | ||
319 | @include iro-bem-has($state, $states...) { | ||
320 | @content; | ||
321 | } | ||
322 | } | ||
323 | |||
324 | /// | ||
325 | /// @alias iro-bem-at-theme | ||
326 | /// | ||
327 | @mixin at-theme($name, $names...) { | ||
328 | @include iro-bem-at-theme($name, $names...) { | ||
329 | @content; | ||
330 | } | ||
331 | } | ||
332 | |||
333 | /// | ||
334 | /// @alias theme | ||
335 | /// | ||
336 | @mixin at($name, $names...) { | ||
337 | @include at-theme($name, $names...) { | ||
338 | @content; | ||
339 | } | ||
340 | } | ||
341 | |||
342 | /// | ||
343 | /// @alias iro-bem-multi | ||
344 | /// | ||
345 | @mixin multi($first, $others...) { | ||
346 | @include iro-bem-multi($first, $others...) { | ||
347 | @content; | ||
348 | } | ||
349 | } | ||
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 @@ | |||
1 | //// | ||
2 | /// @group BEM | ||
3 | /// | ||
4 | /// @access public | ||
5 | //// | ||
6 | |||
7 | /// | ||
8 | /// Generate a new block. | ||
9 | /// | ||
10 | /// This mixin simply creates a new block with the name {namespace}_{name}, | ||
11 | /// where {namespace} is the prefix assigned to $type and {name} is the | ||
12 | /// block's name. | ||
13 | /// | ||
14 | /// @param {string} $name - Block name | ||
15 | /// @param {string} $type [null] - BEMIT namespace of the block | ||
16 | /// | ||
17 | /// @content | ||
18 | /// | ||
19 | /// @throw If $type is invalid | ||
20 | /// @throw If the block is preceded by another block, element, modifier or suffix | ||
21 | /// | ||
22 | /// @example scss - Creating a new block | ||
23 | /// @include iro-bem-block('something', 'component') { | ||
24 | /// /* some definitions */ | ||
25 | /// } | ||
26 | /// | ||
27 | /// // Generates: | ||
28 | /// | ||
29 | /// .c-something { | ||
30 | /// /* some definitions */ | ||
31 | /// } | ||
32 | /// | ||
33 | @mixin iro-bem-block($name, $type: null) { | ||
34 | $result: iro-bem-block($name, $type); | ||
35 | $selector: nth($result, 1); | ||
36 | $context: nth($result, 2); | ||
37 | |||
38 | @include iro-bem-validate( | ||
39 | 'block', | ||
40 | (name: $name, type: $type), | ||
41 | $selector, | ||
42 | $context | ||
43 | ); | ||
44 | |||
45 | @if $type != null { | ||
46 | $iro-bem-blocks: append($iro-bem-blocks, $name + '_' + $type) !global; | ||
47 | } @else { | ||
48 | $iro-bem-blocks: append($iro-bem-blocks, $name) !global; | ||
49 | } | ||
50 | |||
51 | @include iro-context-push($iro-bem-context-id, $context...); | ||
52 | @at-root #{$selector} { | ||
53 | @content; | ||
54 | } | ||
55 | @include iro-context-pop($iro-bem-context-id); | ||
56 | } | ||
57 | |||
58 | /// | ||
59 | /// Generate a new block. Check the respective mixin documentation for more information. | ||
60 | /// | ||
61 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
62 | /// | ||
63 | /// @see {mixin} iro-bem-block | ||
64 | /// | ||
65 | @function iro-bem-block($name, $type: null) { | ||
66 | // | ||
67 | // Possible outcomes: | ||
68 | // - ({b,e,m,s}) block | ||
69 | // | ||
70 | |||
71 | $noop: iro-context-assert-stack-count($iro-bem-context-id, $iro-bem-max-depth); | ||
72 | |||
73 | $selector: null; | ||
74 | $base-selector: null; | ||
75 | |||
76 | @if $type != null { | ||
77 | $namespace: map-get($iro-bem-namespaces, $type); | ||
78 | |||
79 | @if not $namespace { | ||
80 | @error '"#{$type}" is not a valid type.'; | ||
81 | } | ||
82 | |||
83 | $base-selector: selector-parse('.' + $namespace + '-' + $name); | ||
84 | |||
85 | @if $type != 'theme' or & { | ||
86 | $selector: $base-selector; | ||
87 | } @else if not & { | ||
88 | $selector: iro-bem-theme-selector($name); | ||
89 | } | ||
90 | } @else { | ||
91 | $base-selector: selector-parse('.' + $name); | ||
92 | $selector: $base-selector; | ||
93 | } | ||
94 | |||
95 | @if & { | ||
96 | $selector: selector-nest(&, $selector); | ||
97 | } | ||
98 | |||
99 | $context: 'block', ( | ||
100 | 'name': $name, | ||
101 | 'type': $type, | ||
102 | 'selector': $selector, | ||
103 | 'base-selector': $base-selector | ||
104 | ); | ||
105 | |||
106 | @return $selector $context; | ||
107 | } | ||
108 | |||
109 | /// | ||
110 | /// Generate a new object block. It's a shorthand for iro-bem-block($name, 'object'). | ||
111 | /// | ||
112 | /// @param {string} $name - Object block name | ||
113 | /// | ||
114 | /// @content | ||
115 | /// | ||
116 | @mixin iro-bem-object($name) { | ||
117 | @include iro-bem-block($name, 'object') { | ||
118 | @content; | ||
119 | } | ||
120 | } | ||
121 | |||
122 | /// | ||
123 | /// Generate a new object block. Check the respective mixin documentation for more information. | ||
124 | /// | ||
125 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
126 | /// | ||
127 | /// @see {mixin} iro-bem-object | ||
128 | /// | ||
129 | @function iro-bem-object($name) { | ||
130 | @return iro-bem-block($name, 'object'); | ||
131 | } | ||
132 | |||
133 | /// | ||
134 | /// Generate a new component block. It's a shorthand for iro-bem-block($name, 'component'). | ||
135 | /// | ||
136 | /// @param {string} $name - Component block name | ||
137 | /// | ||
138 | /// @content | ||
139 | /// | ||
140 | @mixin iro-bem-component($name) { | ||
141 | @include iro-bem-block($name, 'component') { | ||
142 | @content; | ||
143 | } | ||
144 | } | ||
145 | |||
146 | /// | ||
147 | /// Generate a new component block. Check the respective mixin documentation for more information. | ||
148 | /// | ||
149 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
150 | /// | ||
151 | /// @see {mixin} iro-bem-component | ||
152 | /// | ||
153 | @function iro-bem-component($name) { | ||
154 | @return iro-bem-block($name, 'component'); | ||
155 | } | ||
156 | |||
157 | /// | ||
158 | /// Generate a new layout block. It's a shorthand for iro-bem-block($name, 'layout'). | ||
159 | /// | ||
160 | /// @param {string} $name - Layout block name | ||
161 | /// | ||
162 | /// @content | ||
163 | /// | ||
164 | @mixin iro-bem-layout($name) { | ||
165 | @include iro-bem-block($name, 'layout') { | ||
166 | @content; | ||
167 | } | ||
168 | } | ||
169 | |||
170 | /// | ||
171 | /// Generate a new layout block. Check the respective mixin documentation for more information. | ||
172 | /// | ||
173 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
174 | /// | ||
175 | /// @see {mixin} iro-bem-layout | ||
176 | /// | ||
177 | @function iro-bem-layout($name) { | ||
178 | @return iro-bem-block($name, 'layout'); | ||
179 | } | ||
180 | |||
181 | /// | ||
182 | /// Generate a new utility block. It's a shorthand for iro-bem-block($name, 'utility'). | ||
183 | /// | ||
184 | /// @param {string} $name - Utility block name | ||
185 | /// | ||
186 | /// @content | ||
187 | /// | ||
188 | @mixin iro-bem-utility($name) { | ||
189 | @include iro-bem-block($name, 'utility') { | ||
190 | @content; | ||
191 | } | ||
192 | } | ||
193 | |||
194 | /// | ||
195 | /// Generate a new utility block. Check the respective mixin documentation for more information. | ||
196 | /// | ||
197 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
198 | /// | ||
199 | /// @see {mixin} iro-bem-utility | ||
200 | /// | ||
201 | @function iro-bem-utility($name) { | ||
202 | @return iro-bem-block($name, 'utility'); | ||
203 | } | ||
204 | |||
205 | /// | ||
206 | /// Generate a new scope block. It's a shorthand for iro-bem-block($name, 'scope'). | ||
207 | /// | ||
208 | /// @param {string} $name - Scope block name | ||
209 | /// | ||
210 | /// @content | ||
211 | /// | ||
212 | @mixin iro-bem-scope($name) { | ||
213 | @include iro-bem-block($name, 'scope') { | ||
214 | @content; | ||
215 | } | ||
216 | } | ||
217 | |||
218 | /// | ||
219 | /// Generate a new scope block. Check the respective mixin documentation for more information. | ||
220 | /// | ||
221 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
222 | /// | ||
223 | /// @see {mixin} iro-bem-scope | ||
224 | /// | ||
225 | @function iro-bem-scope($name) { | ||
226 | @return iro-bem-block($name, 'scope'); | ||
227 | } | ||
228 | |||
229 | /// | ||
230 | /// Generate a new theme block. It's a shorthand for iro-bem-block($name, 'theme'). | ||
231 | /// | ||
232 | /// @param {string} $name - Theme block name | ||
233 | /// | ||
234 | /// @content | ||
235 | /// | ||
236 | @mixin iro-bem-theme($name) { | ||
237 | @include iro-bem-block($name, 'theme') { | ||
238 | @content; | ||
239 | } | ||
240 | } | ||
241 | |||
242 | /// | ||
243 | /// Generate a new theme block. Check the respective mixin documentation for more information. | ||
244 | /// | ||
245 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
246 | /// | ||
247 | /// @see {mixin} iro-bem-theme | ||
248 | /// | ||
249 | @function iro-bem-theme($name) { | ||
250 | @return iro-bem-block($name, 'theme'); | ||
251 | } | ||
252 | |||
253 | /// | ||
254 | /// Generate a new JS block. It's a shorthand for iro-bem-block($name, 'js'). | ||
255 | /// | ||
256 | /// @param {string} $name - JS block name | ||
257 | /// | ||
258 | /// @content | ||
259 | /// | ||
260 | @mixin iro-bem-js($name) { | ||
261 | @include iro-bem-block($name, 'js') { | ||
262 | @content; | ||
263 | } | ||
264 | } | ||
265 | |||
266 | /// | ||
267 | /// Generate a new JS block. Check the respective mixin documentation for more information. | ||
268 | /// | ||
269 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
270 | /// | ||
271 | /// @see {mixin} iro-bem-js | ||
272 | /// | ||
273 | @function iro-bem-js($name) { | ||
274 | @return iro-bem-block($name, 'js'); | ||
275 | } | ||
276 | |||
277 | /// | ||
278 | /// Generate a new QA block. It's a shorthand for iro-bem-block($name, 'qa'). | ||
279 | /// | ||
280 | /// @param {string} $name - QA block name | ||
281 | /// | ||
282 | /// @content | ||
283 | /// | ||
284 | @mixin iro-bem-qa($name) { | ||
285 | @include iro-bem-block($name, 'qa') { | ||
286 | @content; | ||
287 | } | ||
288 | } | ||
289 | |||
290 | /// | ||
291 | /// Generate a new QA block. Check the respective mixin documentation for more information. | ||
292 | /// | ||
293 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
294 | /// | ||
295 | /// @see {mixin} iro-bem-qa | ||
296 | /// | ||
297 | @function iro-bem-qa($name) { | ||
298 | @return iro-bem-block($name, 'qa'); | ||
299 | } | ||
300 | |||
301 | /// | ||
302 | /// Generate a new hack block. It's a shorthand for iro-bem-block($name, 'hack'). | ||
303 | /// | ||
304 | /// @param {string} $name - Hack block name | ||
305 | /// | ||
306 | /// @content | ||
307 | /// | ||
308 | @mixin iro-bem-hack($name) { | ||
309 | @include iro-bem-block($name, 'hack') { | ||
310 | @content; | ||
311 | } | ||
312 | } | ||
313 | |||
314 | /// | ||
315 | /// Generate a new hack block. Check the respective mixin documentation for more information. | ||
316 | /// | ||
317 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
318 | /// | ||
319 | /// @see {mixin} iro-bem-hack | ||
320 | /// | ||
321 | @function iro-bem-hack($name) { | ||
322 | @return iro-bem-block($name, 'hack'); | ||
323 | } | ||
324 | |||
325 | /// | ||
326 | /// Assert that a block or element is composed of another block. In BEM, such a relationship is referred to | ||
327 | /// as a "mix": https://en.bem.info/methodology/key-concepts/#mix | ||
328 | /// | ||
329 | /// Compilation will fail if the foreign block doesn't exist. This way, you can ensure that blocks are | ||
330 | /// defined in the right order so that composed blocks/elements will actually override the foreign | ||
331 | /// declarations without having to artificially increase the specificity. | ||
332 | /// | ||
333 | /// @param {string | list} $block - Either first block name, or list with two items: 1. block name, 2. block type | ||
334 | /// @param {string | list} $blocks - Either other block names, or list with two items: 1. block name, 2. block type | ||
335 | /// | ||
336 | /// @throw If a block type is invalid | ||
337 | /// @throw If a block doesn't exist | ||
338 | /// | ||
339 | /// @example scss - Successful assertion | ||
340 | /// @include iro-bem-component('someBlock') { | ||
341 | /// /* some definitions */ | ||
342 | /// } | ||
343 | /// | ||
344 | /// @include iro-bem-component('anotherBlock') { | ||
345 | /// /* some definitions */ | ||
346 | /// | ||
347 | /// @include iro-bem-element('elem') { | ||
348 | /// @include iro-bem-composed-of('someBlock' 'component'); | ||
349 | /// | ||
350 | /// /* some definitions */ | ||
351 | /// } | ||
352 | /// } | ||
353 | /// | ||
354 | /// // Intended use: <div class="c-anotherBlock__elem c-someBlock">...</div> | ||
355 | /// | ||
356 | /// @example scss - Failing assertion | ||
357 | /// @include iro-bem-component('anotherBlock') { | ||
358 | /// /* some definitions */ | ||
359 | /// | ||
360 | /// @include iro-bem-element('elem') { | ||
361 | /// @include iro-bem-composed-of('someBlock' 'component'); | ||
362 | /// | ||
363 | /// /* some definitions */ | ||
364 | /// } | ||
365 | /// } | ||
366 | /// | ||
367 | /// @include iro-bem-component('someBlock') { | ||
368 | /// /* some definitions */ | ||
369 | /// } | ||
370 | /// | ||
371 | /// // Compilation will fail because c-someBlock is defined after c-anotherBlock__elem | ||
372 | /// | ||
373 | @mixin iro-bem-composed-of($block, $blocks...) { | ||
374 | @each $block in iro-list-prepend($blocks, $block) { | ||
375 | @if type-of($block) == string { | ||
376 | @if not index($iro-bem-blocks, $block) { | ||
377 | @error 'Block "#{$block}" does not exist.'; | ||
378 | } | ||
379 | } @else { | ||
380 | $name: nth($block, 1); | ||
381 | $type: nth($block, 2); | ||
382 | |||
383 | @if not map-get($iro-bem-namespaces, $type) { | ||
384 | @error '"#{$type}" is not a valid type.'; | ||
385 | } | ||
386 | |||
387 | @if not index($iro-bem-blocks, $name + '_' + $type) { | ||
388 | @error 'Block "#{$name}" does not exist.'; | ||
389 | } | ||
390 | } | ||
391 | } | ||
392 | } | ||
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 @@ | |||
1 | //// | ||
2 | /// @group BEM | ||
3 | /// | ||
4 | /// @access public | ||
5 | //// | ||
6 | |||
7 | @if $iro-bem-debug { | ||
8 | @each $type, $color in $iro-bem-debug-colors { | ||
9 | $namespace: map-get($iro-bem-namespaces, $type); | ||
10 | |||
11 | [class^='#{$namespace}-'], | ||
12 | [class*=' #{$namespace}-'] { | ||
13 | outline: 5px solid $color; | ||
14 | } | ||
15 | } | ||
16 | } | ||
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 @@ | |||
1 | //// | ||
2 | /// @group BEM | ||
3 | /// | ||
4 | /// @access public | ||
5 | //// | ||
6 | |||
7 | /// | ||
8 | /// Generate a new BEM element. | ||
9 | /// | ||
10 | /// The element will be generated according to the BEM naming convention. | ||
11 | /// If the parent selector doesn't match the block selector, the element will be | ||
12 | /// nested inside the parent selector. This means, you may nest elements inside | ||
13 | /// other elements, modifiers or any kind of selector such as &:hover. | ||
14 | /// | ||
15 | /// @param {string} $name - First element name | ||
16 | /// @param {string} $names - More element names | ||
17 | /// | ||
18 | /// @content | ||
19 | /// | ||
20 | /// @throw If the element is not preceded by a block, element, modifier or suffix. | ||
21 | /// | ||
22 | /// @example scss - Element for a block | ||
23 | /// @include iro-bem-component('block') { | ||
24 | /// /* some block definitions */ | ||
25 | /// | ||
26 | /// @include iro-bem-element('elem') { | ||
27 | /// /* some element definitions */ | ||
28 | /// } | ||
29 | /// } | ||
30 | /// | ||
31 | /// // Generates: | ||
32 | /// | ||
33 | /// .c-block { | ||
34 | /// /* some block definitions */ | ||
35 | /// } | ||
36 | /// | ||
37 | /// .c-block__elem { | ||
38 | /// /* some element definitions */ | ||
39 | /// } | ||
40 | /// | ||
41 | /// @example scss - Element that is affected by the user hovering the block | ||
42 | /// @include iro-bem-component('block') { | ||
43 | /// /* some block definitions */ | ||
44 | /// | ||
45 | /// @include iro-bem-element('elem') { | ||
46 | /// background-color: #eee; | ||
47 | /// } | ||
48 | /// | ||
49 | /// &:hover { | ||
50 | /// @include iro-bem-element('elem') { | ||
51 | /// background-color: #000; | ||
52 | /// } | ||
53 | /// } | ||
54 | /// } | ||
55 | /// | ||
56 | /// // Generates: | ||
57 | /// | ||
58 | /// .c-block { | ||
59 | /// /* some block definitions */ | ||
60 | /// } | ||
61 | /// | ||
62 | /// .c-block__elem { | ||
63 | /// background-color: #eee; | ||
64 | /// } | ||
65 | /// | ||
66 | /// .c-block:hover .c-block__elem { | ||
67 | /// background-color: #000; | ||
68 | /// } | ||
69 | /// | ||
70 | /// @example scss - Multiple elements | ||
71 | /// @include iro-bem-component('block') { | ||
72 | /// /* some block definitions */ | ||
73 | /// | ||
74 | /// @include iro-bem-element('elem1', 'elem2') { | ||
75 | /// /* some element definitions */ | ||
76 | /// } | ||
77 | /// } | ||
78 | /// | ||
79 | /// // Generates: | ||
80 | /// | ||
81 | /// .c-block { | ||
82 | /// /* some block definitions */ | ||
83 | /// } | ||
84 | /// | ||
85 | /// .c-block__elem1, .c-block__elem2 { | ||
86 | /// /* some element definitions */ | ||
87 | /// } | ||
88 | /// | ||
89 | @mixin iro-bem-element($name, $names...) { | ||
90 | $result: iro-bem-element($name, $names...); | ||
91 | $selector: nth($result, 1); | ||
92 | $context: nth($result, 2); | ||
93 | |||
94 | @include iro-bem-validate( | ||
95 | 'element', | ||
96 | (name: $name, names: $names), | ||
97 | $selector, | ||
98 | $context | ||
99 | ); | ||
100 | |||
101 | @include iro-context-push($iro-bem-context-id, $context...); | ||
102 | @at-root #{$selector} { | ||
103 | @content; | ||
104 | } | ||
105 | @include iro-context-pop($iro-bem-context-id); | ||
106 | } | ||
107 | |||
108 | /// | ||
109 | /// Generate a new BEM element. Check the respective mixin documentation for more information. | ||
110 | /// | ||
111 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
112 | /// | ||
113 | /// @see {mixin} iro-bem-element | ||
114 | /// | ||
115 | @function iro-bem-element($name, $names...) { | ||
116 | $noop: iro-context-assert-stack-count($iro-bem-context-id, $iro-bem-max-depth); | ||
117 | $noop: iro-context-assert-stack-must-contain($iro-bem-context-id, 'block'); | ||
118 | |||
119 | $parent-context: iro-context-get($iro-bem-context-id, 'block' 'element'); | ||
120 | |||
121 | $selector: (); | ||
122 | $parts-data: (); | ||
123 | |||
124 | @if nth($parent-context, 1) == 'element' { | ||
125 | @if $iro-bem-element-nesting-policy == 'disallow' { | ||
126 | @error 'Element nesting is forbidden.'; | ||
127 | } | ||
128 | |||
129 | @if $iro-bem-element-nesting-policy == 'append' { | ||
130 | $element-selector: map-get(nth($parent-context, 2), 'selector'); | ||
131 | |||
132 | @if not iro-selector-suffix-match(&, $element-selector) { | ||
133 | @error 'A nested element must be an immediate children of the parent element.'; | ||
134 | } | ||
135 | |||
136 | // | ||
137 | // Possible outcomes: | ||
138 | // - {e}__element | ||
139 | // - [manual selector] {e}__element | ||
140 | // | ||
141 | |||
142 | @each $name in join($name, $names) { | ||
143 | $sel: selector-append(&, $iro-bem-element-separator + $name); | ||
144 | $selector: join($selector, $sel, comma); | ||
145 | $parts-data: append($parts-data, ( | ||
146 | 'name': $name, | ||
147 | 'selector': $sel | ||
148 | )); | ||
149 | } | ||
150 | } | ||
151 | |||
152 | $parent-context: iro-context-get($iro-bem-context-id, 'block'); | ||
153 | } | ||
154 | |||
155 | @if length($selector) == 0 { | ||
156 | $parent-selector: map-get(nth($parent-context, 2), 'selector'); | ||
157 | |||
158 | @if iro-selector-suffix-match(&, $parent-selector) { | ||
159 | // | ||
160 | // Possible outcomes: | ||
161 | // - {b}__element | ||
162 | // - [manual selector] {b}__element | ||
163 | // | ||
164 | |||
165 | @each $name in join($name, $names) { | ||
166 | $sel: selector-append(&, $iro-bem-element-separator + $name); | ||
167 | $selector: join($selector, $sel, comma); | ||
168 | $parts-data: append($parts-data, ( | ||
169 | 'name': $name, | ||
170 | 'selector': $sel | ||
171 | )); | ||
172 | } | ||
173 | } @else { | ||
174 | // | ||
175 | // Possible outcomes: | ||
176 | // - {b} [manual selector] {b}__element | ||
177 | // - {e,m,s} ([manual selector]) {b}__element | ||
178 | // | ||
179 | |||
180 | @if nth($parent-context, 1) != 'block' { | ||
181 | $parent-context: iro-context-get($iro-bem-context-id, 'block'); | ||
182 | } | ||
183 | |||
184 | $block-base-selector: map-get(nth($parent-context, 2), 'base-selector'); | ||
185 | |||
186 | @each $name in join($name, $names) { | ||
187 | $sel: selector-nest(&, selector-append($block-base-selector, $iro-bem-element-separator + $name)); | ||
188 | $selector: join($selector, $sel, comma); | ||
189 | $parts-data: append($parts-data, ( | ||
190 | 'name': $name, | ||
191 | 'selector': $sel | ||
192 | )); | ||
193 | } | ||
194 | } | ||
195 | } | ||
196 | |||
197 | $context: 'element', ( | ||
198 | 'parts': $parts-data, | ||
199 | 'selector': $selector | ||
200 | ); | ||
201 | |||
202 | @return $selector $context; | ||
203 | } | ||
204 | |||
205 | /// | ||
206 | /// Generate a BEM element that is related to the current element. | ||
207 | /// | ||
208 | /// The generated element selector is appended to the current element selector. The $sign | ||
209 | /// determines the relationship. | ||
210 | /// | ||
211 | /// @param {string} $sign - Relationshop sign, either '+' or '~' | ||
212 | /// @param {string} $name - First element name | ||
213 | /// @param {string} $names - More element names | ||
214 | /// | ||
215 | /// @content | ||
216 | /// | ||
217 | /// @throw If the element is not preceded by an element. | ||
218 | /// | ||
219 | /// @example scss - A sibling element to a single element | ||
220 | /// @include iro-bem-component('block') { | ||
221 | /// @include iro-bem-element('elem') { | ||
222 | /// /* some element definitions */ | ||
223 | /// | ||
224 | /// @include iro-bem-related-element('~', 'sibling') { | ||
225 | /// /* some sibling element definitions */ | ||
226 | /// } | ||
227 | /// } | ||
228 | /// } | ||
229 | /// | ||
230 | /// // Generates: | ||
231 | /// | ||
232 | /// .c-block__elem { | ||
233 | /// /* some element definitions */ | ||
234 | /// } | ||
235 | /// | ||
236 | /// .c-block__elem ~ .c-block__sibling { | ||
237 | /// /* some sibling element definitions */ | ||
238 | /// } | ||
239 | /// | ||
240 | /// @example scss - A successor element to a single element | ||
241 | /// @include iro-bem-component('block') { | ||
242 | /// @include iro-bem-element('elem') { | ||
243 | /// /* some element definitions */ | ||
244 | /// | ||
245 | /// @include iro-bem-related-element('+', 'successor') { | ||
246 | /// /* some successor element definitions */ | ||
247 | /// } | ||
248 | /// } | ||
249 | /// } | ||
250 | /// | ||
251 | /// // Generates: | ||
252 | /// | ||
253 | /// .c-block__elem { | ||
254 | /// /* some element definitions */ | ||
255 | /// } | ||
256 | /// | ||
257 | /// .c-block__elem + .c-block__successor { | ||
258 | /// /* some successor element definitions */ | ||
259 | /// } | ||
260 | /// | ||
261 | /// @example scss - A successor element to multiple elements | ||
262 | /// @include iro-bem-component('block') { | ||
263 | /// @include iro-bem-element('elem1', 'elem2') { | ||
264 | /// /* some element definitions */ | ||
265 | /// | ||
266 | /// @include iro-bem-related-element('+', 'successor') { | ||
267 | /// /* some successor element definitions */ | ||
268 | /// } | ||
269 | /// } | ||
270 | /// } | ||
271 | /// | ||
272 | /// // Generates: | ||
273 | /// | ||
274 | /// .c-block__elem1, .c-block__elem2 { | ||
275 | /// /* some element definitions */ | ||
276 | /// } | ||
277 | /// | ||
278 | /// .c-block__elem1 + .c-block__successor, .c-block__elem2 + .c-block__successor { | ||
279 | /// /* some successor element definitions */ | ||
280 | /// } | ||
281 | /// | ||
282 | @mixin iro-bem-related-element($sign, $name, $names...) { | ||
283 | $result: iro-bem-related-element($sign, $name, $names...); | ||
284 | $selector: nth($result, 1); | ||
285 | $context: nth($result, 2); | ||
286 | |||
287 | @include iro-bem-validate( | ||
288 | 'related-element', | ||
289 | (sign: $sign, name: $name, names: $names), | ||
290 | $selector, | ||
291 | $context | ||
292 | ); | ||
293 | |||
294 | @include iro-context-push($iro-bem-context-id, $context...); | ||
295 | @at-root #{$selector} { | ||
296 | @content; | ||
297 | } | ||
298 | @include iro-context-pop($iro-bem-context-id); | ||
299 | } | ||
300 | |||
301 | /// | ||
302 | /// Generate a new BEM element that is related to the current element. | ||
303 | /// Check the respective mixin documentation for more information. | ||
304 | /// | ||
305 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
306 | /// | ||
307 | /// @see {mixin} iro-bem-related-element | ||
308 | /// | ||
309 | @function iro-bem-related-element($sign, $name, $names...) { | ||
310 | // | ||
311 | // Generating this selector is simple: Take the latest block context, use it | ||
312 | // to generate the element part, and insert it at the end of the current selector. | ||
313 | // Possible outcomes: | ||
314 | // - {e} ({m,s}) ([manual selector]) + {e} | ||
315 | // - {e} ({m,s}) ([manual selector]) ~ {e} | ||
316 | // | ||
317 | |||
318 | $noop: iro-context-assert-stack-count($iro-bem-context-id, $iro-bem-max-depth); | ||
319 | $noop: iro-context-assert-stack-must-contain($iro-bem-context-id, 'element'); | ||
320 | |||
321 | @if $sign != '+' and $sign != '~' { | ||
322 | @error 'Invalid relationship sign #{inspect($sign)}.'; | ||
323 | } | ||
324 | |||
325 | $block-context: iro-context-get($iro-bem-context-id, 'block'); | ||
326 | $block-base-selector: map-get(nth($block-context, 2), 'base-selector'); | ||
327 | |||
328 | $selector: (); | ||
329 | $parts-data: (); | ||
330 | |||
331 | @each $name in join($name, $names) { | ||
332 | $sel: selector-nest(&, $sign, selector-append($block-base-selector, $iro-bem-element-separator + $name)); | ||
333 | $selector: join($selector, $sel, comma); | ||
334 | $parts-data: append($parts-data, ( | ||
335 | 'name': $name, | ||
336 | 'selector': $sel | ||
337 | )); | ||
338 | } | ||
339 | |||
340 | $context: 'element', ( | ||
341 | 'parts': $parts-data, | ||
342 | 'selector': $selector | ||
343 | ); | ||
344 | |||
345 | @return $selector $context; | ||
346 | } | ||
347 | |||
348 | /// | ||
349 | /// Generate a BEM element that is a sibling of the current element. | ||
350 | /// | ||
351 | /// It's a shorthand for iro-bem-related-element('~', $name). | ||
352 | /// | ||
353 | /// @param {string} $name - First element name | ||
354 | /// @param {list} $names - List of more element names | ||
355 | /// | ||
356 | /// @content | ||
357 | /// | ||
358 | @mixin iro-bem-sibling-element($name, $names...) { | ||
359 | @include iro-bem-related-element('~', $name, $names...) { | ||
360 | @content; | ||
361 | } | ||
362 | } | ||
363 | |||
364 | /// | ||
365 | /// Generate a new BEM element that is a sibling of the current element. | ||
366 | /// Check the respective mixin documentation for more information. | ||
367 | /// | ||
368 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
369 | /// | ||
370 | /// @see {mixin} iro-bem-sibling-element | ||
371 | /// | ||
372 | @function iro-bem-sibling-element($name, $names...) { | ||
373 | @return iro-bem-related-element('~', $name, $names...); | ||
374 | } | ||
375 | |||
376 | /// | ||
377 | /// Generate a BEM element that is the successor of the current element. | ||
378 | /// | ||
379 | /// It's a shorthand for iro-bem-related-element('+', $name). | ||
380 | /// | ||
381 | /// @param {string} $name - First element name | ||
382 | /// @param {string} $names - More element names | ||
383 | /// | ||
384 | /// @content | ||
385 | /// | ||
386 | @mixin iro-bem-next-element($name, $names...) { | ||
387 | @include iro-bem-related-element('+', $name, $names...) { | ||
388 | @content; | ||
389 | } | ||
390 | } | ||
391 | |||
392 | /// | ||
393 | /// Generate a new BEM element that is the successor of the current element. | ||
394 | /// Check the respective mixin documentation for more information. | ||
395 | /// | ||
396 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
397 | /// | ||
398 | /// @see {mixin} iro-bem-next-element | ||
399 | /// | ||
400 | @function iro-bem-next-element($name, $names...) { | ||
401 | @return iro-bem-related-element('+', $name, $names...); | ||
402 | } | ||
403 | |||
404 | /// | ||
405 | /// Generate the current BEM element as a successor of itself. | ||
406 | /// | ||
407 | /// If this is applied to a single element, it behaves exactly the same as | ||
408 | /// iro-bem-related-element('+', name); | ||
409 | /// However, if it is applied to multiple elements, each twin element only will influence | ||
410 | /// their other twin, which is not replicable with iro-bem-related-element. | ||
411 | /// | ||
412 | /// @content | ||
413 | /// | ||
414 | /// @example scss - Two twin elements | ||
415 | /// @include iro-bem-component('block') { | ||
416 | /// @include iro-bem-element('elem') { | ||
417 | /// /* some element definitions */ | ||
418 | /// | ||
419 | /// @include iro-bem-next-twin-element { | ||
420 | /// /* some twin element definitions */ | ||
421 | /// } | ||
422 | /// } | ||
423 | /// } | ||
424 | /// | ||
425 | /// // Generates: | ||
426 | /// | ||
427 | /// .c-block__elem { | ||
428 | /// /* some element definitions */ | ||
429 | /// } | ||
430 | /// | ||
431 | /// .c-block__elem + .c-block__elem { | ||
432 | /// /* some twin element definitions */ | ||
433 | /// } | ||
434 | /// | ||
435 | /// @example scss - Multiple twin elements | ||
436 | /// @include iro-bem-component('block') { | ||
437 | /// @include iro-bem-element('elem1', 'elem2') { | ||
438 | /// /* some element definitions */ | ||
439 | /// | ||
440 | /// @include iro-bem-next-twin-element { | ||
441 | /// /* some twin element definitions */ | ||
442 | /// } | ||
443 | /// } | ||
444 | /// } | ||
445 | /// | ||
446 | /// // Generates: | ||
447 | /// | ||
448 | /// .c-block__elem1, .c-block__elem2 { | ||
449 | /// /* some element definitions */ | ||
450 | /// } | ||
451 | /// | ||
452 | /// .c-block__elem1 + .c-block__elem1, .c-block__elem2 + .c-block__elem2 { | ||
453 | /// /* some twin element definitions */ | ||
454 | /// } | ||
455 | /// | ||
456 | @mixin iro-bem-related-twin-element($sign) { | ||
457 | $result: iro-bem-related-twin-element($sign); | ||
458 | $selector: nth($result, 1); | ||
459 | $context: nth($result, 2); | ||
460 | |||
461 | @include iro-bem-validate( | ||
462 | 'next-twin-element', | ||
463 | (), | ||
464 | $selector, | ||
465 | $context | ||
466 | ); | ||
467 | |||
468 | @include iro-context-push($iro-bem-context-id, $context...); | ||
469 | @at-root #{$selector} { | ||
470 | @content; | ||
471 | } | ||
472 | @include iro-context-pop($iro-bem-context-id); | ||
473 | } | ||
474 | |||
475 | /// | ||
476 | /// Generate the current BEM element as a successor of itself. | ||
477 | /// Check the respective mixin documentation for more information. | ||
478 | /// | ||
479 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
480 | /// | ||
481 | /// @see {mixin} iro-bem-next-twin-element | ||
482 | /// | ||
483 | @function iro-bem-related-twin-element($sign) { | ||
484 | $noop: iro-context-assert-stack-count($iro-bem-context-id, $iro-bem-max-depth); | ||
485 | $noop: iro-context-assert-stack-must-contain($iro-bem-context-id, 'element'); | ||
486 | |||
487 | $element-context: iro-context-get($iro-bem-context-id, 'element'); | ||
488 | $element-selector: map-get(nth($element-context, 2), 'selector'); | ||
489 | |||
490 | $block-context: iro-context-get($iro-bem-context-id, 'block'); | ||
491 | $block-base-selector: map-get(nth($block-context, 2), 'base-selector'); | ||
492 | |||
493 | $selector: (); | ||
494 | $parts-data: (); | ||
495 | |||
496 | // | ||
497 | // To determine the twin for each element, iterate the sub-selectors from the current selector | ||
498 | // and check if it contains the currently inspected element. This has to be done with string | ||
499 | // comparison since none of Sass selector functions is of use here. | ||
500 | // Finally, the current twin will be appended to the extracted sub-selector as a successor | ||
501 | // element. | ||
502 | // | ||
503 | @each $part-data in map-get(nth($element-context, 2), 'parts') { | ||
504 | $part-selector: map-get($part-data, 'selector'); | ||
505 | $part-name: map-get($part-data, 'name'); | ||
506 | |||
507 | $sel: (); | ||
508 | @if iro-selector-suffix-match(&, $element-selector) { | ||
509 | // | ||
510 | // This mixin is included in the selector the last element mixin created. | ||
511 | // Possible outcomes: | ||
512 | // - {e} + {e} | ||
513 | // - [manual selector] {e} + {e} | ||
514 | // | ||
515 | |||
516 | @each $s in & { | ||
517 | @each $ps in $part-selector { | ||
518 | @if nth($s, -1) == nth($ps, -1) { | ||
519 | $sel-ent: selector-nest($s, $sign, selector-append($block-base-selector, $iro-bem-element-separator + $part-name)); | ||
520 | $sel: join($sel, $sel-ent, comma); | ||
521 | } | ||
522 | } | ||
523 | } | ||
524 | } @else { | ||
525 | // | ||
526 | // This mixin is NOT included in the selector the last element mixin created. | ||
527 | // Possible outcomes: | ||
528 | // - {e} {m,s} + {e} | ||
529 | // - {e} [manual selector] + {e} | ||
530 | // - {e} {m,s} [manual selector] + {e} | ||
531 | // | ||
532 | |||
533 | @each $s in & { | ||
534 | @each $ps in $part-selector { | ||
535 | @if str-index(inspect($s), inspect($ps)) { | ||
536 | $char-index: str-length(inspect($ps)) + 1; | ||
537 | $match: index(' ' ':' ',', str-slice(inspect($s), $char-index, $char-index)) != null; | ||
538 | |||
539 | @if not $match { | ||
540 | @each $separator in $iro-bem-element-separator $iro-bem-modifier-separator $iro-bem-suffix-separator { | ||
541 | @if str-slice(inspect($s), $char-index, $char-index + str-length($separator) - 1) == $separator { | ||
542 | $match: true; | ||
543 | } | ||
544 | } | ||
545 | } | ||
546 | |||
547 | @if $match { | ||
548 | $sel-ent: selector-nest($s, '+', selector-append($block-base-selector, $iro-bem-element-separator + $part-name)); | ||
549 | $sel: join($sel, $sel-ent, comma); | ||
550 | } | ||
551 | } | ||
552 | } | ||
553 | } | ||
554 | } | ||
555 | @if length($sel) != length($part-selector) { | ||
556 | @error 'Could not generate twin element selector.'; | ||
557 | } | ||
558 | |||
559 | $selector: join($selector, $sel, comma); | ||
560 | $parts-data: append($parts-data, ( | ||
561 | 'name': $part-name, | ||
562 | 'selector': $sel | ||
563 | )); | ||
564 | } | ||
565 | |||
566 | $context: 'element', ( | ||
567 | 'parts': $parts-data, | ||
568 | 'selector': $selector | ||
569 | ); | ||
570 | |||
571 | @return $selector $context; | ||
572 | } | ||
573 | |||
574 | /// | ||
575 | /// Generate the current BEM element as a sibling of itself. | ||
576 | /// | ||
577 | /// It's a shorthand for iro-bem-related-twin-element('~'). | ||
578 | /// | ||
579 | /// @content | ||
580 | /// | ||
581 | @mixin iro-bem-sibling-twin-element { | ||
582 | @include iro-bem-related-twin-element('~') { | ||
583 | @content; | ||
584 | } | ||
585 | } | ||
586 | |||
587 | /// | ||
588 | /// Generate the current BEM element as a sibling of itself. | ||
589 | /// Check the respective mixin documentation for more information. | ||
590 | /// | ||
591 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
592 | /// | ||
593 | /// @see {mixin} iro-bem-sibling-twin-element | ||
594 | /// | ||
595 | @function iro-bem-sibling-twin-element() { | ||
596 | @return iro-bem-related-twin-element('~'); | ||
597 | } | ||
598 | |||
599 | /// | ||
600 | /// Generate the current BEM element as a next sibling of itself. | ||
601 | /// | ||
602 | /// It's a shorthand for iro-bem-related-twin-element('+', $name). | ||
603 | /// | ||
604 | /// @content | ||
605 | /// | ||
606 | @mixin iro-bem-next-twin-element { | ||
607 | @include iro-bem-related-twin-element('+') { | ||
608 | @content; | ||
609 | } | ||
610 | } | ||
611 | |||
612 | /// | ||
613 | /// Generate the current BEM element as a next sibling of itself. | ||
614 | /// Check the respective mixin documentation for more information. | ||
615 | /// | ||
616 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
617 | /// | ||
618 | /// @see {mixin} iro-bem-next-twin-element | ||
619 | /// | ||
620 | @function iro-bem-next-twin-element() { | ||
621 | @return iro-bem-related-twin-element('+'); | ||
622 | } | ||
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 @@ | |||
1 | //// | ||
2 | /// @group BEM | ||
3 | /// | ||
4 | /// @access public | ||
5 | //// | ||
6 | |||
7 | /// | ||
8 | /// @access private | ||
9 | /// | ||
10 | @function iro-bem-theme-selector($name, $names...) { | ||
11 | $namespace: map-get($iro-bem-namespaces, 'theme'); | ||
12 | $selector: null; | ||
13 | |||
14 | @each $name in join($name, $names) { | ||
15 | $sel: '.' + $namespace + '-' + $name; | ||
16 | |||
17 | @if $selector == null { | ||
18 | $selector: join(selector-parse($sel), selector-parse('[class*=\' t-\'] ' + $sel), comma); | ||
19 | $selector: join($selector, selector-parse('[class^=\'t-\'] ' + $sel), comma); | ||
20 | } @else { | ||
21 | $selector: selector-nest($selector, $sel); | ||
22 | } | ||
23 | } | ||
24 | |||
25 | @return $selector; | ||
26 | } | ||
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 @@ | |||
1 | //// | ||
2 | /// @group BEM | ||
3 | /// | ||
4 | /// @access public | ||
5 | //// | ||
6 | |||
7 | /// | ||
8 | /// Generate a new BEM modifier. | ||
9 | /// | ||
10 | /// If the parent context is block or element, the modifier will modify said block or element according | ||
11 | /// to the BEM naming convention. | ||
12 | /// | ||
13 | /// If the parent context is a modifier or suffix, then the modifier will depend on said modifier or suffix. | ||
14 | /// Depending on $extend, the meaning of this dependency (and the resulting selector) varies: | ||
15 | /// If it's false (default), you signalize that the modifier also exists by itself, but it changes its | ||
16 | /// behavior when the parent modifier or suffix is set. | ||
17 | /// If it's true, you signalize that the modifier extends the parent modifier or suffix and can only be | ||
18 | /// used in conjunction with it. | ||
19 | /// | ||
20 | /// @param {string | list} $name - First element name or list with two items: 1. first element name, 2. bool indicating if the modifier is extending | ||
21 | /// @param {string | list} $names - More element names or lists with two items: 1. element name, 2. bool indicating if the modifier is extending | ||
22 | /// | ||
23 | /// @content | ||
24 | /// | ||
25 | /// @throw If the element is not preceded by a block, element, modifier or suffix. | ||
26 | /// | ||
27 | /// @example scss - Modifier that modifies a block or element | ||
28 | /// @include iro-bem-component('block') { | ||
29 | /// @include iro-bem-modifier('mod') { | ||
30 | /// background-color: #eee; | ||
31 | /// } | ||
32 | /// | ||
33 | /// @include iro-bem-element('elem') { | ||
34 | /// @include iro-bem-modifier('mod') { | ||
35 | /// background-color: #222; | ||
36 | /// } | ||
37 | /// } | ||
38 | /// } | ||
39 | /// | ||
40 | /// // Generates: | ||
41 | /// | ||
42 | /// .c-block--mod { | ||
43 | /// background-color: #eee; | ||
44 | /// } | ||
45 | /// | ||
46 | /// .c-block__elem--mod { | ||
47 | /// background-color: #222; | ||
48 | /// } | ||
49 | /// | ||
50 | /// @example scss - Modifier nested in another modifier, not extending | ||
51 | /// @include iro-bem-component('block') { | ||
52 | /// @include iro-bem-modifier('mod') { | ||
53 | /// background-color: #eee; | ||
54 | /// } | ||
55 | /// | ||
56 | /// @include iro-bem-modifier('dark') { | ||
57 | /// /* some definitions */ | ||
58 | /// | ||
59 | /// @include iro-bem-modifier('mod') { | ||
60 | /// background-color: #222; | ||
61 | /// } | ||
62 | /// } | ||
63 | /// } | ||
64 | /// | ||
65 | /// // Generates: | ||
66 | /// | ||
67 | /// .c-block--mod { | ||
68 | /// background-color: #eee; | ||
69 | /// } | ||
70 | /// | ||
71 | /// .c-block--dark { | ||
72 | /// /* some definitions */ | ||
73 | /// } | ||
74 | /// | ||
75 | /// .c-block--dark.c-block--mod { | ||
76 | /// background-color: #222; | ||
77 | /// } | ||
78 | /// | ||
79 | /// @example scss - Modifier nested in another modifier, extending | ||
80 | /// @include iro-bem-component('block') { | ||
81 | /// @include iro-bem-modifier('mod') { | ||
82 | /// background-color: #eee; | ||
83 | /// } | ||
84 | /// | ||
85 | /// @include iro-bem-modifier('dark') { | ||
86 | /// /* some definitions */ | ||
87 | /// | ||
88 | /// @include iro-bem-modifier('mod' true) { | ||
89 | /// background-color: #222; | ||
90 | /// } | ||
91 | /// } | ||
92 | /// } | ||
93 | /// | ||
94 | /// // Generates: | ||
95 | /// | ||
96 | /// .c-block--mod { | ||
97 | /// background-color: #eee; | ||
98 | /// } | ||
99 | /// | ||
100 | /// .c-block--dark { | ||
101 | /// /* some definitions */ | ||
102 | /// } | ||
103 | /// | ||
104 | /// .c-block--dark--mod { | ||
105 | /// background-color: #222; | ||
106 | /// } | ||
107 | /// | ||
108 | @mixin iro-bem-modifier($name, $names...) { | ||
109 | $result: iro-bem-modifier($name, $names...); | ||
110 | $selector: nth($result, 1); | ||
111 | $context: nth($result, 2); | ||
112 | |||
113 | @include iro-bem-validate( | ||
114 | 'modifier', | ||
115 | (name: $name, names: $names), | ||
116 | $selector, | ||
117 | $context | ||
118 | ); | ||
119 | |||
120 | @include iro-context-push($iro-bem-context-id, $context...); | ||
121 | @at-root #{$selector} { | ||
122 | @content; | ||
123 | } | ||
124 | @include iro-context-pop($iro-bem-context-id); | ||
125 | } | ||
126 | |||
127 | /// | ||
128 | /// Generate a new BEM modifier. Check the respective mixin documentation for more information. | ||
129 | /// | ||
130 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
131 | /// | ||
132 | /// @see {mixin} iro-bem-modifier | ||
133 | /// | ||
134 | @function iro-bem-modifier($name, $names...) { | ||
135 | $noop: iro-context-assert-stack-count($iro-bem-context-id, $iro-bem-max-depth); | ||
136 | $noop: iro-context-assert-stack-must-contain($iro-bem-context-id, 'block'); | ||
137 | |||
138 | $parent-context: iro-context-get($iro-bem-context-id, 'block' 'element' 'modifier' 'suffix'); | ||
139 | $parent-selector: map-get(nth($parent-context, 2), 'selector'); | ||
140 | $selector: (); | ||
141 | $parts-data: (); | ||
142 | |||
143 | @if not iro-selector-suffix-match(&, $parent-selector) { | ||
144 | // | ||
145 | // The current selector doesn't match the parent selector. | ||
146 | // The user manually added a selector between parent context and this modifier call. | ||
147 | // This case is forbidden because any outcome semantically wouldn't make sense: | ||
148 | // - {b,e,m,s} [manual selector] {b,e,m,s}--modifier | ||
149 | // - {b,e,m,s}--modifier [manual selector] | ||
150 | // The first case would make the modifier behave like an element. | ||
151 | // The second case is unintuitive, the code would be more clear by nesting the manual | ||
152 | // selector in the modifier instead. | ||
153 | // | ||
154 | |||
155 | @error 'A modifier must be an immediate child of the parent context'; | ||
156 | } | ||
157 | |||
158 | @each $name in iro-list-prepend($names, $name) { | ||
159 | $extend: false; | ||
160 | @if type-of($name) == list { | ||
161 | $extend: nth($name, 2); | ||
162 | $name: nth($name, 1); | ||
163 | } | ||
164 | |||
165 | @if index('block' 'element', nth($parent-context, 1)) or $extend == true { | ||
166 | // | ||
167 | // Either the parent context is block or element, or a modifier or suffix | ||
168 | // is to be extended. The modifier part can simply be appended. | ||
169 | // Possible outcomes: | ||
170 | // - {b,e,m,s}--modifier | ||
171 | // | ||
172 | |||
173 | $sel: selector-append(&, $iro-bem-modifier-separator + $name); | ||
174 | $selector: join($selector, $sel, comma); | ||
175 | $parts-data: append($parts-data, ( | ||
176 | 'name': $name, | ||
177 | 'selector': $sel | ||
178 | )); | ||
179 | } @else { | ||
180 | // | ||
181 | // Parent context is modifier, suffix or state and $extend is false. | ||
182 | // | ||
183 | |||
184 | $be-context: iro-context-get($iro-bem-context-id, 'block' 'element'); | ||
185 | |||
186 | @if nth($be-context, 1) == 'element' { | ||
187 | // | ||
188 | // Latest context is element. Since element contexts can consist of multiple single | ||
189 | // elements, inspect all elements and append its selector with the suffix "--$name". | ||
190 | // This has to be done with string comparison since none of Sass selector functions | ||
191 | // is of use here. | ||
192 | // Possible outcomes: | ||
193 | // - {m,s}.{e}--modifier | ||
194 | // | ||
195 | |||
196 | $nsel: (); | ||
197 | |||
198 | @each $elem-part-data in map-get(nth($be-context, 2), 'parts') { | ||
199 | $elem-part-selector: map-get($elem-part-data, 'selector'); | ||
200 | |||
201 | $sel: (); | ||
202 | @each $s in & { | ||
203 | @each $ps in $elem-part-selector { | ||
204 | @if str-index(inspect($s), inspect($ps) + $iro-bem-modifier-separator) or str-index(inspect($s), inspect($ps) + $iro-bem-suffix-separator) { | ||
205 | $sel: join($sel, selector-unify($s, selector-append($ps, $iro-bem-modifier-separator + $name)), comma); | ||
206 | } | ||
207 | } | ||
208 | } | ||
209 | @if length($sel) == 0 { | ||
210 | @error 'Could not generate modifier selector.'; | ||
211 | } | ||
212 | |||
213 | $nsel: join($nsel, $sel, comma); | ||
214 | } | ||
215 | |||
216 | $selector: join($selector, $nsel, comma); | ||
217 | $parts-data: append($parts-data, ( | ||
218 | 'name': $name, | ||
219 | 'selector': $nsel | ||
220 | )); | ||
221 | } @else { | ||
222 | // | ||
223 | // Latest context is block. Just append the modifier part. | ||
224 | // Possible outcomes: | ||
225 | // - {m,s}.{b}--modifier | ||
226 | // | ||
227 | |||
228 | $block-base-selector: map-get(nth($be-context, 2), 'base-selector'); | ||
229 | |||
230 | $sel: selector-append(&, $block-base-selector, $iro-bem-modifier-separator + $name); | ||
231 | $selector: join($selector, $sel, comma); | ||
232 | $parts-data: append($parts-data, ( | ||
233 | 'name': $name, | ||
234 | 'selector': $sel | ||
235 | )); | ||
236 | } | ||
237 | } | ||
238 | } | ||
239 | |||
240 | $context: 'modifier', ( | ||
241 | 'parts': $parts-data, | ||
242 | 'selector': $selector | ||
243 | ); | ||
244 | |||
245 | @return $selector $context; | ||
246 | } | ||
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 @@ | |||
1 | //// | ||
2 | /// @group BEM | ||
3 | /// | ||
4 | /// @access public | ||
5 | //// | ||
6 | |||
7 | /// | ||
8 | /// Generate multiple entities (BEM or not) at once. | ||
9 | /// | ||
10 | /// NOTE: This mixin does not generate perfectly optimized selectors in order to keep track of contexts. | ||
11 | /// | ||
12 | /// @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. | ||
13 | /// @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. | ||
14 | /// | ||
15 | /// @content | ||
16 | /// | ||
17 | /// @example scss - Creating multiple elements, a modifier and an anchor | ||
18 | /// @include iro-bem-object('buttonstrip') { | ||
19 | /// display: none; | ||
20 | /// | ||
21 | /// @include iro-bem-multi('modifier' 'mod', 'element' 'button' 'separator', '> a') { | ||
22 | /// display: block; | ||
23 | /// } | ||
24 | /// } | ||
25 | /// | ||
26 | /// // Generates: | ||
27 | /// | ||
28 | /// .o-buttonstrip { | ||
29 | /// display: none; | ||
30 | /// } | ||
31 | /// | ||
32 | /// .o-buttonstrip--mod { | ||
33 | /// display: block; | ||
34 | /// } | ||
35 | /// | ||
36 | /// .o-buttonstrip__button, { | ||
37 | /// .o-buttonstrip__separator { | ||
38 | /// display: block; | ||
39 | /// } | ||
40 | /// | ||
41 | /// .o-buttonstrip > a { | ||
42 | /// display: block; | ||
43 | /// } | ||
44 | /// | ||
45 | /// @example scss - Creating multiple elements, a modifier and an anchor - optional colons included | ||
46 | /// @include iro-bem-object('buttonstrip') { | ||
47 | /// display: none; | ||
48 | /// | ||
49 | /// @include iro-bem-multi('modifier:' 'mod', 'element:' 'button' 'separator', '> a') { | ||
50 | /// display: block; | ||
51 | /// } | ||
52 | /// } | ||
53 | /// | ||
54 | /// // Generates: | ||
55 | /// | ||
56 | /// .o-buttonstrip { | ||
57 | /// display: none; | ||
58 | /// } | ||
59 | /// | ||
60 | /// .o-buttonstrip--mod { | ||
61 | /// display: block; | ||
62 | /// } | ||
63 | /// | ||
64 | /// .o-buttonstrip__button, { | ||
65 | /// .o-buttonstrip__separator { | ||
66 | /// display: block; | ||
67 | /// } | ||
68 | /// | ||
69 | /// .o-buttonstrip > a { | ||
70 | /// display: block; | ||
71 | /// } | ||
72 | /// | ||
73 | @mixin iro-bem-multi($first, $others...) { | ||
74 | @include iro-context-assert-stack-count($iro-bem-context-id, $iro-bem-max-depth); | ||
75 | |||
76 | @each $entity in iro-list-prepend($others, $first) { | ||
77 | $is-manual-selector: false; | ||
78 | |||
79 | @if type-of($entity) == string and not function-exists('iro-bem-' + $entity) { | ||
80 | $is-manual-selector: true; | ||
81 | } | ||
82 | |||
83 | @if $is-manual-selector { | ||
84 | $sel: if(&, selector-nest(&, $entity), selector-parse($entity)); | ||
85 | |||
86 | @at-root #{$sel} { | ||
87 | @content; | ||
88 | } | ||
89 | } @else { | ||
90 | $entity-func-id: null; | ||
91 | |||
92 | @if type-of($entity) == list { | ||
93 | $entity-func-id: nth($entity, 1); | ||
94 | $entity: iro-list-slice($entity, 2); | ||
95 | } @else { | ||
96 | $entity-func-id: $entity; | ||
97 | $entity: (); | ||
98 | } | ||
99 | |||
100 | @if str-slice($entity-func-id, str-length($entity-func-id)) == ':' { | ||
101 | $entity-func-id: str-slice($entity-func-id, 1, str-length($entity-func-id) - 1); | ||
102 | } | ||
103 | |||
104 | $sel-func: null; | ||
105 | |||
106 | @if function-exists('iro-bem-' + $entity-func-id) { | ||
107 | $sel-func: get-function('iro-bem-' + $entity-func-id); | ||
108 | } @else if function-exists($entity-func-id) { | ||
109 | $sel-func: get-function($entity-func-id); | ||
110 | } | ||
111 | |||
112 | @if $sel-func == null { | ||
113 | @error 'Function "#{inspect($entity-func-id)}" was not found.'; | ||
114 | } | ||
115 | |||
116 | $entity-result: call($sel-func, $entity...); | ||
117 | $entity-result-selector: nth($entity-result, 1); | ||
118 | $entity-result-context: nth($entity-result, 2); | ||
119 | |||
120 | @if $entity-result-context != null { | ||
121 | @include iro-context-push($iro-bem-context-id, $entity-result-context...); | ||
122 | } | ||
123 | @at-root #{$entity-result-selector} { | ||
124 | @content; | ||
125 | } | ||
126 | @if $entity-result-context != null { | ||
127 | @include iro-context-pop($iro-bem-context-id); | ||
128 | } | ||
129 | } | ||
130 | } | ||
131 | } | ||
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 @@ | |||
1 | //// | ||
2 | /// @group BEM | ||
3 | /// | ||
4 | /// @access public | ||
5 | //// | ||
6 | |||
7 | /// | ||
8 | /// Create a new state rule. | ||
9 | /// | ||
10 | /// @param {string} $state - First state name | ||
11 | /// @param {list} $states - List of more state names | ||
12 | /// | ||
13 | /// @content | ||
14 | /// | ||
15 | /// @example scss - Using single is-state | ||
16 | /// @include iro-bem-object('menu') { | ||
17 | /// display: none; | ||
18 | /// | ||
19 | /// @include iro-bem-state('is', open') { | ||
20 | /// display: block; | ||
21 | /// } | ||
22 | /// } | ||
23 | /// | ||
24 | /// // Generates: | ||
25 | /// | ||
26 | /// .o-menu { | ||
27 | /// display: none; | ||
28 | /// } | ||
29 | /// | ||
30 | /// .o-menu.is-open { | ||
31 | /// display: block; | ||
32 | /// } | ||
33 | /// | ||
34 | /// @example scss - Using multiple is-states | ||
35 | /// @include iro-bem-object('menu') { | ||
36 | /// display: none; | ||
37 | /// | ||
38 | /// @include iro-bem-state('is', open', 'visible') { | ||
39 | /// display: block; | ||
40 | /// } | ||
41 | /// } | ||
42 | /// | ||
43 | /// // Generates: | ||
44 | /// | ||
45 | /// .o-menu { | ||
46 | /// display: none; | ||
47 | /// } | ||
48 | /// | ||
49 | /// .o-menu.is-open, | ||
50 | /// .o-menu.is-visible { | ||
51 | /// display: block; | ||
52 | /// } | ||
53 | /// | ||
54 | @mixin iro-bem-state($prefix, $state, $states...) { | ||
55 | $result: iro-bem-state($prefix, $state, $states...); | ||
56 | $selector: nth($result, 1); | ||
57 | $context: nth($result, 2); | ||
58 | |||
59 | @include iro-bem-validate( | ||
60 | 'state', | ||
61 | (prefix: $prefix, state: $state, states: $states), | ||
62 | $selector, | ||
63 | $context | ||
64 | ); | ||
65 | |||
66 | @include iro-context-push($iro-bem-context-id, $context...); | ||
67 | @at-root #{$selector} { | ||
68 | @content; | ||
69 | } | ||
70 | @include iro-context-pop($iro-bem-context-id); | ||
71 | } | ||
72 | |||
73 | /// | ||
74 | /// Generate a new state. | ||
75 | /// | ||
76 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
77 | /// | ||
78 | /// @see {mixin} iro-bem-has | ||
79 | /// | ||
80 | @function iro-bem-state($prefix, $state, $states...) { | ||
81 | $selector: (); | ||
82 | $parts-data: (); | ||
83 | |||
84 | @each $state in join($state, $states) { | ||
85 | $sel: selector-parse('.#{$prefix}-#{$state}'); | ||
86 | @if & { | ||
87 | $sel: selector-append(&, $sel); | ||
88 | } | ||
89 | $selector: join($selector, $sel, comma); | ||
90 | $parts-data: append($parts-data, ( | ||
91 | 'name': $state, | ||
92 | 'selector': $sel | ||
93 | )); | ||
94 | } | ||
95 | |||
96 | $context: 'state', ( | ||
97 | 'parts': $parts-data, | ||
98 | 'selector': $selector | ||
99 | ); | ||
100 | |||
101 | @return $selector $context; | ||
102 | } | ||
103 | |||
104 | /// | ||
105 | /// Create a new has-state modifier. | ||
106 | /// | ||
107 | /// It's a shorthand for iro-bem-state('is', $state, $states...). | ||
108 | /// | ||
109 | @mixin iro-bem-is($state, $states...) { | ||
110 | @include iro-bem-state('is', $state, $states...) { | ||
111 | @content; | ||
112 | } | ||
113 | } | ||
114 | |||
115 | /// | ||
116 | /// Generate a new is-state modifier. Check the respective mixin documentation for more information. | ||
117 | /// | ||
118 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
119 | /// | ||
120 | /// @see {mixin} iro-bem-is | ||
121 | /// | ||
122 | @function iro-bem-is($state, $states...) { | ||
123 | @return iro-bem-state('is', $state, $states...); | ||
124 | } | ||
125 | |||
126 | /// | ||
127 | /// Create a new has-state modifier. | ||
128 | /// | ||
129 | /// It's a shorthand for iro-bem-state('has', $state, $states...). | ||
130 | /// | ||
131 | @mixin iro-bem-has($state, $states...) { | ||
132 | @include iro-bem-state('has', $state, $states...) { | ||
133 | @content; | ||
134 | } | ||
135 | } | ||
136 | |||
137 | /// | ||
138 | /// Generate a new has-state modifier. Check the respective mixin documentation for more information. | ||
139 | /// | ||
140 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
141 | /// | ||
142 | /// @see {mixin} iro-bem-has | ||
143 | /// | ||
144 | @function iro-bem-has($state, $states...) { | ||
145 | @return iro-bem-state('has', $state, $states...); | ||
146 | } | ||
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 @@ | |||
1 | //// | ||
2 | /// @group BEM | ||
3 | /// | ||
4 | /// @access public | ||
5 | //// | ||
6 | |||
7 | /// | ||
8 | /// Generate a new suffix. | ||
9 | /// | ||
10 | /// @param {string} $name - Suffix name | ||
11 | /// | ||
12 | /// @content | ||
13 | /// | ||
14 | /// @throw If the element is not preceded by a block or modifier. | ||
15 | /// | ||
16 | /// @example scss - Using a suffix | ||
17 | /// @include iro-bem-utility('hidden') { | ||
18 | /// display: none; | ||
19 | /// | ||
20 | /// @media (max-width: 320px) { | ||
21 | /// @include iro-bem-suffix('phone') { | ||
22 | /// display: none; | ||
23 | /// } | ||
24 | /// } | ||
25 | /// | ||
26 | /// @media (max-width: 768px) { | ||
27 | /// @include iro-bem-suffix('tablet') { | ||
28 | /// display: none; | ||
29 | /// } | ||
30 | /// } | ||
31 | /// } | ||
32 | /// | ||
33 | /// // Generates: | ||
34 | /// | ||
35 | /// .u-hidden { | ||
36 | /// display: none; | ||
37 | /// } | ||
38 | /// | ||
39 | /// @media (max-width: 320px) { | ||
40 | /// .u-hidden@phone { | ||
41 | /// display: none; | ||
42 | /// } | ||
43 | /// } | ||
44 | /// | ||
45 | /// @media (max-width: 768px) { | ||
46 | /// .u-hidden@tablet { | ||
47 | /// display: none; | ||
48 | /// } | ||
49 | /// } | ||
50 | /// | ||
51 | @mixin iro-bem-suffix($name) { | ||
52 | $result: iro-bem-suffix($name); | ||
53 | $selector: nth($result, 1); | ||
54 | $context: nth($result, 2); | ||
55 | |||
56 | @include iro-bem-validate( | ||
57 | 'suffix', | ||
58 | (name: $name), | ||
59 | $selector, | ||
60 | $context | ||
61 | ); | ||
62 | |||
63 | @include iro-context-push($iro-bem-context-id, $context...); | ||
64 | @at-root #{$selector} { | ||
65 | @content; | ||
66 | } | ||
67 | @include iro-context-pop($iro-bem-context-id); | ||
68 | } | ||
69 | |||
70 | /// | ||
71 | /// Generate a new suffix. Check the respective mixin documentation for more information. | ||
72 | /// | ||
73 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
74 | /// | ||
75 | /// @see {mixin} iro-bem-suffix | ||
76 | /// | ||
77 | @function iro-bem-suffix($name) { | ||
78 | // | ||
79 | // Suffixes can be used on block, element and modifier. | ||
80 | // | ||
81 | |||
82 | $noop: iro-context-assert-stack-count($iro-bem-context-id, $iro-bem-max-depth); | ||
83 | $noop: iro-context-assert-stack-must-contain($iro-bem-context-id, 'block'); | ||
84 | $noop: iro-context-assert-stack-must-not-contain($iro-bem-context-id, 'suffix'); | ||
85 | |||
86 | $parent-context: iro-context-get($iro-bem-context-id, 'block' 'element' 'modifier'); | ||
87 | $parent-selector: map-get(nth($parent-context, 2), 'selector'); | ||
88 | |||
89 | @if not iro-selector-suffix-match(&, $parent-selector) { | ||
90 | // | ||
91 | // The current selector doesn't match the parent selector. | ||
92 | // The user manually added a selector between parent context and this suffix call. | ||
93 | // This case is forbidden because any outcome semantically wouldn't make sense: | ||
94 | // - {b,e,m} [manual selector] {b,e,m}@suffix | ||
95 | // - {b,e,m}@suffix [manual selector] | ||
96 | // The first case would make the modifier behave like an element. | ||
97 | // The second case is unintuitive, the code would be more clear by nesting the manual | ||
98 | // selector in the suffix instead. | ||
99 | // | ||
100 | |||
101 | @error 'A suffix must be an immediate child of the parent context'; | ||
102 | } | ||
103 | |||
104 | // | ||
105 | // The suffix part can simply be appended. | ||
106 | // Possible outcomes: | ||
107 | // - {b,e,m}@suffix | ||
108 | // | ||
109 | |||
110 | $selector: selector-append(&, $iro-bem-suffix-separator + $name); | ||
111 | |||
112 | $context: 'suffix', ( | ||
113 | 'name': $name, | ||
114 | 'selector': $selector | ||
115 | ); | ||
116 | |||
117 | @return $selector $context; | ||
118 | } | ||
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 @@ | |||
1 | //// | ||
2 | /// @group BEM | ||
3 | /// | ||
4 | /// @access public | ||
5 | //// | ||
6 | |||
7 | /// | ||
8 | /// Declare new rules for the current block for when this theme is active. | ||
9 | /// | ||
10 | /// @param {string} $name - First theme block name | ||
11 | /// @param {string} $names - More theme block names | ||
12 | /// | ||
13 | /// @content | ||
14 | /// | ||
15 | @mixin iro-bem-at-theme($name, $names...) { | ||
16 | $result: iro-bem-at-theme($name, $names...); | ||
17 | $selector: nth($result, 1); | ||
18 | $context: nth($result, 2); | ||
19 | |||
20 | @include iro-bem-validate( | ||
21 | 'at-theme', | ||
22 | (name: $name, names: $names), | ||
23 | $selector, | ||
24 | $context | ||
25 | ); | ||
26 | |||
27 | @include iro-context-push($iro-bem-context-id, $context...); | ||
28 | @at-root #{$selector} { | ||
29 | @content; | ||
30 | } | ||
31 | @include iro-context-pop($iro-bem-context-id); | ||
32 | } | ||
33 | |||
34 | /// | ||
35 | /// Declare new rules for the current block for when this theme is active. | ||
36 | /// Check the respective mixin documentation for more information. | ||
37 | /// | ||
38 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
39 | /// | ||
40 | /// @see {mixin} iro-bem-at-theme | ||
41 | /// | ||
42 | @function iro-bem-at-theme($name, $names...) { | ||
43 | $noop: iro-context-assert-stack-must-contain($iro-bem-context-id, 'block'); | ||
44 | |||
45 | $parent-context: iro-context-get($iro-bem-context-id, 'block'); | ||
46 | $parent-selector: map-get(nth($parent-context, 2), 'selector'); | ||
47 | |||
48 | @if not iro-selector-suffix-match(&, $parent-selector) { | ||
49 | @error 'An at-theme rule must be an immediate child of a block'; | ||
50 | } | ||
51 | |||
52 | $selector: iro-bem-theme-selector($name, $names...); | ||
53 | $selector: selector-nest($selector, &); | ||
54 | |||
55 | $context: 'at-theme', ( | ||
56 | 'name': join($name, $names), | ||
57 | 'selector': $selector | ||
58 | ); | ||
59 | |||
60 | @return $selector $context; | ||
61 | } | ||
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 @@ | |||
1 | //// | ||
2 | /// Validators are custom functions that will be called before a BEM entity is created. | ||
3 | /// They check if the current mixin usage is valid or not and thus they are a flexible way to | ||
4 | /// let you implement your own rules. | ||
5 | /// | ||
6 | /// Validator functions receive the following information: | ||
7 | /// - BEM entity type | ||
8 | /// - Arguments passed to the mixin | ||
9 | /// - The generated selector | ||
10 | /// - The generated context, if any | ||
11 | /// | ||
12 | /// Additionally, the context stack used by the BEM system can be examined. | ||
13 | /// | ||
14 | /// @group BEM | ||
15 | /// | ||
16 | /// @access public | ||
17 | //// | ||
18 | |||
19 | /// | ||
20 | /// A list of validator functions. | ||
21 | /// | ||
22 | /// @type list | ||
23 | /// | ||
24 | /// @access private | ||
25 | /// | ||
26 | $iro-bem-validators: (); | ||
27 | |||
28 | /// | ||
29 | /// Register one or multiple validator functions. | ||
30 | /// | ||
31 | /// A validator function is a function that accepts 4 arguments: | ||
32 | /// 1. BEM entity type (string) | ||
33 | /// 2. Arguments passed to the mixin (map) | ||
34 | /// 3. The generated selector (selector) | ||
35 | /// 4. The generated context (list, may be null) | ||
36 | /// | ||
37 | /// The function must return a list with two items: | ||
38 | /// 1. `true` if the mixin usage is valid, otherwise `false`, | ||
39 | /// 2. a string with a rejection reason (empty if the usage is valid). | ||
40 | /// | ||
41 | /// @param {string} $func-name - First function name. | ||
42 | /// @param {string} $func-names - Other function names. | ||
43 | /// | ||
44 | @mixin iro-bem-add-validator($func-name, $func-names...) { | ||
45 | $noop: iro-bem-add-validator($func-name, $func-names...); | ||
46 | } | ||
47 | |||
48 | /// | ||
49 | /// Register one or multiple validator functions. Check the respective mixin documentation for more information. | ||
50 | /// | ||
51 | /// @see {mixin} iro-bem-add-validator | ||
52 | /// | ||
53 | @function iro-bem-add-validator($func-name, $func-names...) { | ||
54 | @each $fn-name in join($func-name, $func-names) { | ||
55 | $fn: get-function($fn-name); | ||
56 | $iro-bem-validators: map-merge($iro-bem-validators, ($fn-name: $fn)) !global; | ||
57 | } | ||
58 | @return null; | ||
59 | } | ||
60 | |||
61 | /// | ||
62 | /// Unregister one or multiple validator functions. | ||
63 | /// | ||
64 | /// @param {string} $func-name - First function name. | ||
65 | /// @param {string} $func-names - Other function names. | ||
66 | /// | ||
67 | @mixin iro-bem-remove-validator($func-name, $func-names...) { | ||
68 | $noop: iro-bem-remove-validator($func-name, $func-names...); | ||
69 | } | ||
70 | |||
71 | /// | ||
72 | /// Unregister one or multiple validator functions. Check the respective mixin documentation for more information. | ||
73 | /// | ||
74 | /// @see {mixin} iro-bem-remove-validator | ||
75 | /// | ||
76 | @function iro-bem-remove-validator($func-name, $func-names...) { | ||
77 | $iro-bem-validators: map-remove($iro-bem-validators, $func-name, $func-names...) !global; | ||
78 | @return null; | ||
79 | } | ||
80 | |||
81 | /// | ||
82 | /// @access private | ||
83 | /// | ||
84 | @mixin iro-bem-validate($type, $args, $selector, $context) { | ||
85 | @each $id, $fn in $iro-bem-validators { | ||
86 | $result: call($fn, $type, $args, $selector, $context); | ||
87 | @if not nth($result, 1) { | ||
88 | @error 'A BEM validator rejected this mixin usage due to the following reason: #{nth($result, 2)}'; | ||
89 | } | ||
90 | } | ||
91 | } | ||
92 | |||
93 | // | ||
94 | // --------------------------------------------------------------------------------------------------------- | ||
95 | // Built-in validators | ||
96 | // --------------------------------------------------------------------------------------------------------- | ||
97 | // | ||
98 | |||
99 | /// | ||
100 | /// A validator that makes sure blocks are declared in the right order, determined by the | ||
101 | /// namespace used. | ||
102 | /// | ||
103 | @function iro-bem-validator--enforce-namespace-order($type, $args, $selector, $context) { | ||
104 | @if not global-variable-exists(iro-bem-namespace-order) { | ||
105 | $iro-bem-namespace-order: map-keys($iro-bem-namespaces) !global; | ||
106 | } | ||
107 | @if not global-variable-exists(iro-bem-cur-namespace-index) { | ||
108 | $iro-bem-cur-namespace-index: 1 !global; | ||
109 | } | ||
110 | |||
111 | @if $type == 'block' { | ||
112 | $block-type: map-get($args, 'type'); | ||
113 | |||
114 | @if $block-type != null { | ||
115 | $ns-index: index($iro-bem-namespace-order, $block-type); | ||
116 | |||
117 | @if $ns-index != null { | ||
118 | @if $ns-index < $iro-bem-cur-namespace-index { | ||
119 | @return false 'Namespace "#{$block-type}" comes before current namespace #{nth($iro-bem-namespace-order, $iro-bem-cur-namespace-index)}'; | ||
120 | } | ||
121 | |||
122 | $iro-bem-cur-namespace-index: $ns-index !global; | ||
123 | } | ||
124 | } | ||
125 | } | ||
126 | |||
127 | @return true ''; | ||
128 | } | ||
129 | |||
130 | /// | ||
131 | /// A validator that makes all BEM entities immutable. | ||
132 | /// | ||
133 | @function iro-bem-validator--immutable-entities($type, $args, $selector, $context) { | ||
134 | @if not global-variable-exists(iro-bem-generated-selectors) { | ||
135 | $iro-bem-generated-selectors: () !global; | ||
136 | } | ||
137 | |||
138 | $block-name: null; | ||
139 | $block-type: null; | ||
140 | $block-id: null; | ||
141 | |||
142 | @if $type == 'block' { | ||
143 | $block-name: map-get($args, 'name'); | ||
144 | $block-type: map-get($args, 'type'); | ||
145 | } @else { | ||
146 | $block-context: iro-context-get($iro-bem-context-id, 'block'); | ||
147 | $block-name: map-get(nth($block-context, 2), 'name'); | ||
148 | $block-type: map-get(nth($block-context, 2), 'type'); | ||
149 | } | ||
150 | |||
151 | @if $block-type != null { | ||
152 | $block-id: $block-name + '_' + $block-type; | ||
153 | } @else { | ||
154 | $block-id: $block-name; | ||
155 | } | ||
156 | |||
157 | @if $type == 'block' { | ||
158 | @if map-has-key($iro-bem-generated-selectors, $block-id) { | ||
159 | @return false 'Entity "#{$type}" with arguments [ #{iro-map-print($args)} ] was already defined.'; | ||
160 | } | ||
161 | |||
162 | $iro-bem-generated-selectors: map-merge($iro-bem-generated-selectors, ($block-id: ())) !global; | ||
163 | } @else { | ||
164 | $selectors: map-get($iro-bem-generated-selectors, $block-id); | ||
165 | |||
166 | @if index($selectors, $selector) { | ||
167 | @return false 'Entity "#{$type}" with arguments [ #{iro-map-print($args)} ] was already defined.'; | ||
168 | } | ||
169 | |||
170 | $selectors: append($selectors, $selector); | ||
171 | |||
172 | $iro-bem-generated-selectors: map-merge($iro-bem-generated-selectors, ($block-id: $selectors)) !global; | ||
173 | } | ||
174 | |||
175 | @return true ''; | ||
176 | } | ||
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 @@ | |||
1 | //// | ||
2 | /// @group BEM | ||
3 | /// | ||
4 | /// @access public | ||
5 | //// | ||
6 | |||
7 | /// | ||
8 | /// Separating character sequence for elements. | ||
9 | /// | ||
10 | /// @type string | ||
11 | /// | ||
12 | $iro-bem-element-separator: '__' !default; | ||
13 | |||
14 | /// | ||
15 | /// Separating character sequence for modifiers. | ||
16 | /// | ||
17 | /// @type string | ||
18 | /// | ||
19 | $iro-bem-modifier-separator: '--' !default; | ||
20 | |||
21 | /// | ||
22 | /// Separating character sequence for BEMIT suffixes. | ||
23 | /// | ||
24 | /// @type string | ||
25 | /// | ||
26 | $iro-bem-suffix-separator: '\\@' !default; | ||
27 | |||
28 | /// | ||
29 | /// Prefixes for all BEMIT namespaces. | ||
30 | /// | ||
31 | /// @prop {string} utility ['u'] - Utility namespace | ||
32 | /// @prop {string} object ['o'] - Object namespace | ||
33 | /// @prop {string} component ['c'] - Component namespace | ||
34 | /// @prop {string} layout ['l'] - Layout namespace | ||
35 | /// @prop {string} scope ['s'] - Scope namespace | ||
36 | /// @prop {string} theme ['t'] - Theme namespace | ||
37 | /// @prop {string} js ['js'] - JS namespace | ||
38 | /// @prop {string} qa ['qa'] - QA namespace | ||
39 | /// @prop {string} hack ['_'] - Hack namespace | ||
40 | /// | ||
41 | /// @type map | ||
42 | /// | ||
43 | $iro-bem-namespaces: ( | ||
44 | object: 'o', | ||
45 | component: 'c', | ||
46 | layout: 'l', | ||
47 | scope: 's', | ||
48 | theme: 't', | ||
49 | utility: 'u', | ||
50 | js: 'js', | ||
51 | qa: 'qa', | ||
52 | hack: '_' | ||
53 | ) !default; | ||
54 | |||
55 | /// | ||
56 | /// A list of all generated blocks. | ||
57 | /// | ||
58 | /// @type list | ||
59 | /// | ||
60 | /// @access private | ||
61 | /// | ||
62 | $iro-bem-blocks: (); | ||
63 | |||
64 | /// | ||
65 | /// Maximum nesting depth of BEM mixins. The large default value means there is no effective limit. | ||
66 | /// | ||
67 | /// @type number | ||
68 | /// | ||
69 | $iro-bem-max-depth: 99 !default; | ||
70 | |||
71 | /// | ||
72 | /// Indicates how nested elements should be handled. | ||
73 | /// | ||
74 | /// 'allow' means elements will be nested, i.e. the result will be {e} {b}__element. | ||
75 | /// 'disallow' means an error will be emitted. | ||
76 | /// 'append' means the element name will be appended to the parent element, i.e. the result will be {e}__element. | ||
77 | /// Any other value is treated as 'allow'. | ||
78 | /// | ||
79 | /// @type string | ||
80 | /// | ||
81 | $iro-bem-element-nesting-policy: 'allow' !default; | ||
82 | |||
83 | /// | ||
84 | /// Context ID used for all BEM-related mixins. | ||
85 | /// | ||
86 | /// @type string | ||
87 | /// | ||
88 | $iro-bem-context-id: 'bem' !default; | ||
89 | |||
90 | /// | ||
91 | /// Debug mode. | ||
92 | /// | ||
93 | /// @type bool | ||
94 | /// | ||
95 | $iro-bem-debug: false !default; | ||
96 | |||
97 | /// | ||
98 | /// Colors assigned to namespaces. | ||
99 | /// | ||
100 | /// @type map | ||
101 | /// | ||
102 | $iro-bem-debug-colors: ( | ||
103 | object: #ffa500, | ||
104 | component: #00f, | ||
105 | layout: #ff0, | ||
106 | utility: #008000, | ||
107 | hack: #f00 | ||
108 | ) !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 @@ | |||
1 | //// | ||
2 | /// Shorter version of the harmony-related functions. Useful to reduce clutter. | ||
3 | /// | ||
4 | /// @group Harmony shortcodes | ||
5 | /// | ||
6 | /// @access public | ||
7 | //// | ||
8 | |||
9 | /// | ||
10 | /// @alias iro-harmony-modular-scale | ||
11 | /// | ||
12 | @function modular-scale($times, $base, $ratio) { | ||
13 | @include iro-harmony-modular-scale($times, $base, $ratio); | ||
14 | } | ||
15 | |||
16 | /// | ||
17 | /// @alias modular-scale | ||
18 | /// | ||
19 | @function ms($times, $base, $ratio) { | ||
20 | @include modular-scale($times, $base, $ratio); | ||
21 | } | ||
22 | |||
23 | /// | ||
24 | /// @alias iro-responsive-modular-scale | ||
25 | /// | ||
26 | @mixin responsive-modular-scale($prop, $times, $responsive-map) { | ||
27 | @include iro-responsive-modular-scale($prop, $times, $responsive-map); | ||
28 | } | ||
29 | |||
30 | /// | ||
31 | /// @alias responsive-modular-scale | ||
32 | /// | ||
33 | @mixin responsive-ms($prop, $times, $responsive-map) { | ||
34 | @include responsive-modular-scale($prop, $times, $responsive-map); | ||
35 | } | ||
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 @@ | |||
1 | @import 'functions'; | ||
2 | @import 'math'; | ||
3 | @import 'vars'; | ||
4 | @import 'contexts'; | ||
5 | @import 'bem'; | ||
6 | @import 'props'; | ||
7 | @import 'harmony'; | ||
8 | @import 'responsive'; | ||
9 | @import 'easing'; | ||
10 | @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 @@ | |||
1 | @import 'functions'; | ||
2 | @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 @@ | |||
1 | //// | ||
2 | /// Shorter version of the responsive-related mixins. Useful to reduce clutter. | ||
3 | /// | ||
4 | /// @group Responsive shortcodes | ||
5 | /// | ||
6 | /// @access public | ||
7 | //// | ||
8 | |||
9 | /// | ||
10 | /// @alias iro-responsive-property | ||
11 | /// | ||
12 | @mixin responsive($props, $responsive-map, $fluid: true, $vertical: false) { | ||
13 | @include iro-responsive-property($props, $responsive-map, $fluid, $vertical); | ||
14 | } | ||