gerillass 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +23 -0
  2. package/SKILL.md +57 -5
  3. package/gerillass.json +315 -66
  4. package/package.json +1 -1
  5. package/scss/library/_after.scss +15 -0
  6. package/scss/library/_all-text-inputs.scss +1 -1
  7. package/scss/library/_background-dots.scss +55 -9
  8. package/scss/library/_background-image.scss +31 -5
  9. package/scss/library/_background-stripes.scss +43 -4
  10. package/scss/library/_before.scss +15 -0
  11. package/scss/library/_border-radius.scss +25 -1
  12. package/scss/library/_brand-logo.scss +24 -1
  13. package/scss/library/_breakpoint.scss +34 -0
  14. package/scss/library/_container-query.scss +73 -1
  15. package/scss/library/_counter.scss +24 -2
  16. package/scss/library/_focus-ring.scss +34 -0
  17. package/scss/library/_font-face.scss +61 -3
  18. package/scss/library/_index.scss +3 -0
  19. package/scss/library/_line-clamp.scss +14 -1
  20. package/scss/library/_loadify.scss +7 -0
  21. package/scss/library/_motion-safe.scss +28 -0
  22. package/scss/library/_remove.scss +4 -1
  23. package/scss/library/_reset-css.scss +7 -5
  24. package/scss/library/_screen-agent.scss +27 -0
  25. package/scss/library/_text-image.scss +24 -1
  26. package/scss/library/_tokens.scss +111 -0
  27. package/scss/library/_triangle.scss +109 -9
  28. package/scss/utilities/_clear-unit.scss +4 -0
  29. package/scss/utilities/_convert-to-em.scss +6 -0
  30. package/scss/utilities/_convert-to-number.scss +15 -0
  31. package/scss/utilities/_font-sizer.scss +14 -0
  32. package/scss/utilities/_remify.scss +7 -0
  33. package/scss/utilities/_validate-length.scss +7 -0
  34. package/scss/utilities/_validate-ratio.scss +25 -2
@@ -1,8 +1,10 @@
1
1
  @charset "UTF-8";
2
2
 
3
3
  @use "sass:list";
4
+ @use "sass:map";
4
5
  @use "sass:meta";
5
6
  @use "sass:string";
7
+ @use "../maps/map-for-font-formats" as *;
6
8
  @use "../utilities/font-source" as *;
7
9
 
8
10
  @mixin font-face(
@@ -10,7 +12,8 @@
10
12
  $file-path,
11
13
  $font-style: normal,
12
14
  $font-weight: 400,
13
- $file-formats: eot woff2 woff ttf svg
15
+ $file-formats: eot woff2 woff ttf svg,
16
+ $font-display: null
14
17
  ) {
15
18
  @if & {
16
19
  @error "You must call the mixin at the root level of your style sheet, not in the `#{&+'{'+'}'}` selector.";
@@ -18,10 +21,29 @@
18
21
  @error "`#{$font-family}` is not a valid $font-family for `font-face`. Pass the family name as a string, such as `\"Inter\"`.";
19
22
  } @else if meta.type-of($file-path) != "string" {
20
23
  @error "`#{$file-path}` is not a valid $file-path for `font-face`. Pass the path to the font files without an extension, as a string.";
24
+ } @else if string.slice(meta.inspect($file-path), 1, 1) != "\"" and string.index($file-path, "var(") {
25
+ // The path is written into url("..."), where var() is only text: the
26
+ // browser would request a file literally named var(--x).woff2.
27
+ @error "`#{$file-path}` is not a valid $file-path for `font-face`. A custom property cannot supply a font path, because the path is written into url() as text. Pass the path as a string.";
28
+ } @else if string.slice(meta.inspect($font-family), 1, 1) != "\"" and string.index($font-family, "var(") {
29
+ // Measured in Chrome 152, a @font-face rule dropped `font-family: var(--f)`,
30
+ // and a rule with no family is never used. A quoted "var(--f)" is only a
31
+ // name, which the browser kept, so it is left alone like a quoted path.
32
+ @error "`#{$font-family}` is not a valid $font-family for `font-face`. A custom property cannot supply it, because a @font-face rule drops var() in a descriptor, and without a family the font is never used. Pass the family name as a string, such as `\"Inter\"`.";
21
33
  } @else {
22
34
 
23
35
  $list: ();
24
36
 
37
+ // The same rule dropped `font-style: var(--s)`, `font-weight: var(--w)` and
38
+ // `font-weight: 100 var(--w)`, so var() is refused anywhere in either,
39
+ // before $font-style is reinterpreted below.
40
+ @if string.index(meta.inspect($font-style), "var(") {
41
+ @error "`#{meta.inspect($font-style)}` is not a valid $font-style for `font-face`. A custom property cannot supply it, because a @font-face rule drops var() in a descriptor. Pass the style itself, such as `italic`.";
42
+ }
43
+ @if string.index(meta.inspect($font-weight), "var(") {
44
+ @error "`#{meta.inspect($font-weight)}` is not a valid $font-weight for `font-face`. A custom property cannot supply it, because a @font-face rule drops var() in a descriptor. Pass the weight itself, such as `700`, or a range such as `160 700`.";
45
+ }
46
+
25
47
  @if list.index(100 200 300 400 500 600 700 800 900, $font-style) {
26
48
  $font-weight: $font-style;
27
49
  $font-style: normal;
@@ -42,6 +64,27 @@
42
64
  }
43
65
  }
44
66
 
67
+ // $font-display comes last so the reinterpretation of $font-style above is
68
+ // left alone, and it is written only when given: the browser's default is
69
+ // what a call without it has always had. Measured in Chrome 152, a
70
+ // @font-face rule drops `font-display: "swap"` and `font-display: var(--d)`
71
+ // without a word, so a quoted keyword is unquoted and var() is refused.
72
+ @if $font-display != null {
73
+ @if meta.type-of($font-display) == "string" and string.index($font-display, "var(") {
74
+ @error "`#{meta.inspect($font-display)}` is not a valid $font-display for `font-face`. A custom property cannot supply it, because a @font-face rule drops var() in a descriptor. Pass one of: auto, block, swap, fallback, optional.";
75
+ } @else if not list.index("auto" "block" "swap" "fallback" "optional", $font-display) {
76
+ @error "`#{meta.inspect($font-display)}` is not a valid $font-display for `font-face`. Pass one of: auto, block, swap, fallback, optional.";
77
+ }
78
+ }
79
+
80
+ // fontSource returns nothing for a format it does not know, so a typo
81
+ // dropped that source and a list of nothing but typos dropped `src`.
82
+ @each $format in $file-formats {
83
+ @if not map.has-key($map-for-font-formats, $format) {
84
+ @error "`#{meta.inspect($format)}` is not a valid file format for `font-face`. Pass one or more of: #{map.keys($map-for-font-formats)}.";
85
+ }
86
+ }
87
+
45
88
  @font-face {
46
89
  font-family: $font-family;
47
90
  @if list.index($file-formats, eot) {
@@ -51,8 +94,23 @@
51
94
  $list: list.append($list, fontSource($font-family, $file-path, list.nth($file-formats, $i)), comma);
52
95
  }
53
96
  src: $list;
54
- font-style: $font-style;
55
- font-weight: $font-weight;
97
+ // A quoted keyword is written without its quotes. Measured in Chrome 152,
98
+ // `font-style: "italic"` and `font-weight: "bold"` were dropped, and this
99
+ // is what a quoted argument used to produce. The family keeps its quotes,
100
+ // since there they make it a name.
101
+ @if meta.type-of($font-style) == "string" {
102
+ font-style: string.unquote($font-style);
103
+ } @else {
104
+ font-style: $font-style;
105
+ }
106
+ @if meta.type-of($font-weight) == "string" {
107
+ font-weight: string.unquote($font-weight);
108
+ } @else {
109
+ font-weight: $font-weight;
110
+ }
111
+ @if $font-display {
112
+ font-display: string.unquote($font-display);
113
+ }
56
114
  @content;
57
115
  }
58
116
  }
@@ -25,11 +25,13 @@
25
25
  @forward "ellipsis";
26
26
  @forward "escape-to-parent";
27
27
  @forward "except";
28
+ @forward "focus-ring";
28
29
  @forward "font-face";
29
30
  @forward "hide";
30
31
  @forward "line-clamp";
31
32
  @forward "linear-gradient";
32
33
  @forward "loadify";
34
+ @forward "motion-safe";
33
35
  @forward "only";
34
36
  @forward "placeholder";
35
37
  @forward "placeholder-shown";
@@ -52,4 +54,5 @@
52
54
  @forward "text-selection";
53
55
  @forward "text-shadow";
54
56
  @forward "text-stroke";
57
+ @forward "tokens";
55
58
  @forward "triangle";
@@ -1,7 +1,19 @@
1
1
  @charset "UTF-8";
2
2
 
3
+ @use "sass:list";
3
4
  @use "sass:math";
4
5
  @use "sass:meta";
6
+ @use "sass:string";
7
+
8
+ // True for an unquoted call to one of $names, such as var(--x). A quoted string
9
+ // stays text, so `"var(--x)"` is not a call.
10
+ @function -is-css-function($value, $names) {
11
+ @if meta.type-of($value) != "string" or string.slice(meta.inspect($value), 1, 1) == "\"" {
12
+ @return false;
13
+ }
14
+ $open: string.index($value, "(");
15
+ @return $open != null and list.index($names, string.to-lower-case(string.slice($value, 1, $open - 1))) != null;
16
+ }
5
17
 
6
18
  // Truncates text after a number of lines. `ellipsis` does one line; this does
7
19
  // several, and needs five declarations rather than one because -webkit-line-clamp
@@ -20,7 +32,8 @@
20
32
  // So the standard property is emitted for the future and the prefixed trio is
21
33
  // what actually does the work today.
22
34
  @mixin line-clamp($lines: 3) {
23
- @if $lines != none {
35
+ // var() is resolved by the browser, so only a literal count is checked.
36
+ @if $lines != none and not -is-css-function($lines, "var" "env") {
24
37
  @if meta.type-of($lines) != "number" or not math.is-unitless($lines) {
25
38
  @error "`#{$lines}` is not a valid $lines for `line-clamp`. Pass a whole number of lines, or `none` to undo a clamp.";
26
39
  }
@@ -4,6 +4,13 @@
4
4
  @use "sass:meta";
5
5
  @use "../utilities/is-time" as *;
6
6
 
7
+ // `init` defines the %loadify placeholder, and every call extends it. Under the
8
+ // module system a selector can only extend a placeholder from a module it
9
+ // loads, so the file with the call must be the file that calls `init`, or must
10
+ // `@use` it. Anything else fails with "The target selector was not found".
11
+ // An entry file calling `init` does not help a partial it loads, since the
12
+ // partial does not load the entry file. Stylesheets using @import share one
13
+ // scope and are unaffected.
7
14
  @mixin loadify($params...) {
8
15
  @if not & {
9
16
  @if list.length($params) == 0 or (list.length($params) == 1 and list.nth($params, 1) == "init") {
@@ -0,0 +1,28 @@
1
+ @charset "UTF-8";
2
+
3
+ @use "sass:meta";
4
+
5
+ // Applies its content only for users who have not asked their system for less
6
+ // motion. Motion is opt-in here rather than switched off afterwards: the usual
7
+ // way round writes the animation and then overrides it under
8
+ // `prefers-reduced-motion: reduce`, and that override has to catch every
9
+ // animation and transition, including the ones added later. Written inside this
10
+ // block, nothing moves for a user who asked for less, without a list to keep.
11
+ //
12
+ // Two agent trials wrote this same wrapper by hand, as `motion-safe` and
13
+ // `motion-ok`. Both media queries were checked to parse in Chrome 152; the
14
+ // reduced-motion setting itself could not be emulated in the browser used.
15
+ //
16
+ // The trap is the resting state. An element hidden in its base rule and
17
+ // revealed by an animation inside the block stays hidden when the block does
18
+ // not apply, which is what `loadify` avoids by applying the end state. Keep the
19
+ // visible state in the base rule and the start of the motion in the keyframes.
20
+ @mixin motion-safe {
21
+ @if not meta.content-exists() {
22
+ @error "`motion-safe` wraps the motion you pass it, so call it with a block, such as `.card { @include motion-safe { transition: transform 0.2s; } }`.";
23
+ }
24
+
25
+ @media (prefers-reduced-motion: no-preference) {
26
+ @content;
27
+ }
28
+ }
@@ -8,9 +8,12 @@
8
8
  display: none;
9
9
  } @else if list.length($params) == 1 {
10
10
  $value: list.nth($params, 1);
11
- @include breakpoint($value) {
11
+ // `only` rather than the one-argument form, which compiles to the same
12
+ // query, so the warning below names `remove` and is printed once.
13
+ @include breakpoint(only, $value) {
12
14
  display: none;
13
15
  }
16
+ @warn "`remove(#{$value})` hides the element only at a viewport exactly that wide, a single pixel. Write `remove(only, #{$value})` for that width, or `remove(min, #{$value})` from it upwards. The one-argument form will be refused in 3.0.0.";
14
17
  } @else if list.length($params) == 2 {
15
18
  @if list.index("min" "max", list.nth($params, 1)) {
16
19
  $mode: list.nth($params, 1);
@@ -4,10 +4,12 @@
4
4
  @if & {
5
5
  @error "Please call the mixin at the root of your style sheet.";
6
6
  } @else {
7
- /* http://meyerweb.com/eric/tools/css/reset/
8
- v2.0 | 20110126
9
- License: none (public domain)
10
- */
7
+ // http://meyerweb.com/eric/tools/css/reset/
8
+ // v2.0 | 20110126
9
+ // License: none (public domain)
10
+ //
11
+ // Comments in this file are `//`: a /* */ comment inside a mixin is emitted
12
+ // into the stylesheet of everyone who includes it.
11
13
  html, body, div, span, applet, object, iframe,
12
14
  h1, h2, h3, h4, h5, h6, p, blockquote, pre,
13
15
  a, abbr, acronym, address, big, cite, code,
@@ -28,7 +30,7 @@
28
30
  font: inherit;
29
31
  vertical-align: baseline;
30
32
  }
31
- /* HTML5 display-role reset for older browsers */
33
+ // HTML5 display-role reset for older browsers
32
34
  article, aside, details, figcaption, figure,
33
35
  footer, header, hgroup, menu, main, nav, section {
34
36
  display: block;
@@ -1,6 +1,33 @@
1
1
  @charset "UTF-8";
2
2
 
3
+ @use "sass:list";
4
+ @use "sass:meta";
5
+ @use "sass:string";
6
+
7
+ // Returns the first value in $value that holds var(), looking inside lists and
8
+ // calculations, or null. Custom properties are not evaluated in a @media or
9
+ // @container condition, so a rule built from one never applies, and nothing
10
+ // warns. Measured in Chrome 152: `(min-width: var(--wide))` did not match with
11
+ // `--wide: 200px`, while `(min-width: 200px)` did.
12
+ @function -custom-property-in($value) {
13
+ @each $item in $value {
14
+ @if meta.type-of($item) == "list" or meta.type-of($item) == "arglist" {
15
+ $found: -custom-property-in($item);
16
+ @if $found {
17
+ @return $found;
18
+ }
19
+ } @else if string.index(meta.inspect($item), "var(") {
20
+ @return $item;
21
+ }
22
+ }
23
+ @return null;
24
+ }
25
+
3
26
  @mixin screen-agent($resolution) {
27
+ $custom-property: -custom-property-in($resolution);
28
+ @if $custom-property {
29
+ @error "`#{$custom-property}` cannot be used as a resolution: custom properties are not evaluated in a @media condition, so the rule would never apply. Pass `1x`, `2x`, `3x` or a resolution such as `2dppx`.";
30
+ }
4
31
  @if $resolution == "1x" {
5
32
  @media (min-resolution: 96dpi) {
6
33
  @content;
@@ -1,8 +1,31 @@
1
1
  @charset "UTF-8";
2
2
 
3
+ @use "sass:list";
4
+ @use "sass:meta";
5
+ @use "sass:string";
6
+
7
+ // An image path goes into url(). A var(), url(), image-set() or gradient is
8
+ // already an image, and wrapping it produced url(var(--x)), which loads nothing.
9
+ @function -image($value) {
10
+ @if -is-css-function($value, "var" "url" "image-set" "linear-gradient" "radial-gradient" "conic-gradient" "repeating-linear-gradient" "repeating-radial-gradient" "repeating-conic-gradient") {
11
+ @return $value;
12
+ }
13
+ @return url($value);
14
+ }
15
+
16
+ // True for an unquoted call to one of $names, such as var(--x). A quoted string
17
+ // stays text, so `"var(--x)"` is not a call.
18
+ @function -is-css-function($value, $names) {
19
+ @if meta.type-of($value) != "string" or string.slice(meta.inspect($value), 1, 1) == "\"" {
20
+ @return false;
21
+ }
22
+ $open: string.index($value, "(");
23
+ @return $open != null and list.index($names, string.to-lower-case(string.slice($value, 1, $open - 1))) != null;
24
+ }
25
+
3
26
  @mixin text-image($image: null) {
4
27
  @if $image {
5
- background-image: url($image);
28
+ background-image: -image($image);
6
29
  }
7
30
  background-size: cover;
8
31
  background-position: center;
@@ -0,0 +1,111 @@
1
+ @charset "UTF-8";
2
+
3
+ @use "sass:list";
4
+ @use "sass:meta";
5
+ @use "sass:string";
6
+
7
+ // Characters a custom property name cannot hold. Measured in Chrome 152: a
8
+ // declaration named `--space-0.5`, `--w-1/2`, `--a b` or `--#fff` was dropped,
9
+ // and `--font:body` became a property called `--font`. Letters, digits,
10
+ // hyphens, underscores and non-ASCII letters all survived.
11
+ $-forbidden: " " "." "," "/" "#" ":" ";" "(" ")" "[" "]" "{" "}" "!" "@" "$" "%" "^" "&" "*" "+" "=" "<" ">" "?" "\\" "|" "~" "`" "'" "\"";
12
+
13
+ @function -forbidden-in($text) {
14
+ @each $character in $-forbidden {
15
+ @if string.index($text, $character) {
16
+ @return $character;
17
+ }
18
+ }
19
+ @return null;
20
+ }
21
+
22
+ // Writes a value the way Sass writes it in an ordinary declaration. A custom
23
+ // property value is interpolated, and neither obvious spelling matches that:
24
+ // plain interpolation drops the quotes of a string, and meta.inspect keeps them
25
+ // but prints numbers at full precision, so oklch(0.637 0.237 25.331) came out
26
+ // with a hue of 25.331000000000017deg and math.div(1, 3) as 0.3333333333333333.
27
+ // So only strings go through meta.inspect, and a list is walked into.
28
+ @function -serialize($value) {
29
+ @if meta.type-of($value) == "string" {
30
+ @return meta.inspect($value);
31
+ }
32
+ @if meta.type-of($value) == "list" or meta.type-of($value) == "arglist" {
33
+ $joiner: " ";
34
+ @if list.separator($value) == "comma" {
35
+ $joiner: ", ";
36
+ } @else if list.separator($value) == "slash" {
37
+ $joiner: " / ";
38
+ }
39
+ $out: "";
40
+ @each $item in $value {
41
+ @if $out != "" {
42
+ $out: $out + $joiner;
43
+ }
44
+ $out: $out + -serialize($item);
45
+ }
46
+ @if list.is-bracketed($value) {
47
+ $out: "[" + $out + "]";
48
+ }
49
+ @return $out;
50
+ }
51
+ @return "#{$value}";
52
+ }
53
+
54
+ // Writes a Sass map out as custom properties, which two agent trials wrote by
55
+ // hand for a light and a dark palette. Three things it does that interpolating
56
+ // the map in a loop does not:
57
+ //
58
+ // A null is left out. `--focus: null` is a value, not an absence: measured in
59
+ // Chrome 152, `color: var(--focus, red)` did not fall back to red.
60
+ //
61
+ // A quoted string keeps its quotes, through -serialize above. With plain
62
+ // interpolation `"→"` became `→`, and `content: var(--arrow)` then computed to
63
+ // none.
64
+ //
65
+ // A nested map is refused rather than flattened, so a later version can decide
66
+ // how to flatten it without changing what a call already produces.
67
+ @mixin tokens($map, $prefix: null) {
68
+ @if not & {
69
+ @error "`tokens` writes custom properties into the rule it is called in, so call it inside a selector, such as `:root { @include tokens((bg: #fff), color); }`.";
70
+ }
71
+ @if meta.type-of($map) != "map" {
72
+ @error "`#{meta.inspect($map)}` is not a valid $map for `tokens`. Pass a map of names to values, such as `(bg: #fff, text: #111)`.";
73
+ }
74
+ // Checked by its characters, not its type, like the names below: Sass reads
75
+ // an unquoted `blue` or `red` as a colour, and `tokens($blue, blue)` has to
76
+ // work. A colour written `#00f` or `rgb()` is still caught by `#` and `(`.
77
+ @if $prefix != null {
78
+ @if not list.index("string" "color" "number", meta.type-of($prefix)) or string.length("#{$prefix}") == 0 {
79
+ @error "`#{meta.inspect($prefix)}` is not a valid $prefix for `tokens`. Pass a name, such as `color`, or leave it out.";
80
+ }
81
+ $character: -forbidden-in("#{$prefix}");
82
+ @if $character {
83
+ @error "`#{$prefix}` is not a valid $prefix for `tokens`: a custom property name cannot contain `#{$character}`. Use letters, digits, hyphens and underscores.";
84
+ }
85
+ }
86
+
87
+ @each $name, $value in $map {
88
+ $text: "#{$name}";
89
+ @if string.length($text) == 0 {
90
+ @error "`#{meta.inspect($name)}` is not a valid token name for `tokens`. Pass a name such as `bg` or `500`.";
91
+ }
92
+ $character: -forbidden-in($text);
93
+ @if $character {
94
+ @error "`#{$text}` is not a valid token name for `tokens`: a custom property name cannot contain `#{$character}`. Use letters, digits, hyphens and underscores, such as `space-0_5`.";
95
+ }
96
+ @if meta.type-of($value) == "map" {
97
+ @error "`#{$text}` holds a map, which `tokens` does not flatten. Pass each group on its own, such as `@include tokens($blue, blue)` for `$blue: (500: #3b82f6)`.";
98
+ }
99
+ @if meta.type-of($value) == "list" and list.length($value) == 0 {
100
+ @error "`#{$text}` has an empty value in `tokens`. Pass a value, or null to leave the property out.";
101
+ }
102
+
103
+ @if $value != null {
104
+ $property: --#{$text};
105
+ @if $prefix {
106
+ $property: --#{$prefix}-#{$text};
107
+ }
108
+ #{$property}: #{-serialize($value)};
109
+ }
110
+ }
111
+ }
@@ -1,13 +1,82 @@
1
1
  @charset "UTF-8";
2
2
 
3
3
  @use "sass:list";
4
- @use "sass:math";
4
+ @use "sass:meta";
5
+ @use "sass:string";
5
6
  @use "../lists/list-of-directions" as *;
6
- @use "../utilities/is-color" as *;
7
+
8
+ // A size Sass can use inside calc(): a number, a calculation such as calc(),
9
+ // min() or clamp(), or var() and env(), which only the browser can resolve.
10
+ // Anything else would pass silently through calc(): `calc(nonsense * 5)`
11
+ // compiles.
12
+ @function -is-size($value) {
13
+ @if meta.type-of($value) == "number" or meta.type-of($value) == "calculation" {
14
+ @return true;
15
+ }
16
+ @if meta.type-of($value) == "string" and string.slice(meta.inspect($value), 1, 1) != "\"" {
17
+ $open: string.index($value, "(");
18
+ @return $open != null and list.index("var" "env", string.to-lower-case(string.slice($value, 1, $open - 1))) != null;
19
+ }
20
+ @return false;
21
+ }
22
+
23
+ // A value CSS will take as a colour: a Sass colour, currentColor, or a function
24
+ // that returns a colour when the browser resolves it. A list passes when every
25
+ // item does, as it did through isColor.
26
+ //
27
+ // inherit, initial, unset and revert are left out on purpose. The mixin writes
28
+ // the colour into a border-color shorthand beside `transparent`, and a CSS-wide
29
+ // keyword is only valid as the whole value: `transparent transparent inherit`
30
+ // is rejected by the browser. Checked with CSS.supports in Chrome 152.
31
+ //
32
+ // Deliberately looser than isColor, which stays strict because tint and shade
33
+ // pass its result to color.mix, and that needs a real Sass colour. A border
34
+ // colour does not. The functions are named rather than "anything with a
35
+ // parenthesis", which would let url() through as a colour.
36
+ @function -is-css-color($value) {
37
+ @if list.length($value) > 1 {
38
+ @each $item in $value {
39
+ @if not -is-css-color($item) {
40
+ @return false;
41
+ }
42
+ }
43
+ @return true;
44
+ }
45
+ @if meta.type-of($value) == "color" {
46
+ @return true;
47
+ }
48
+ @if meta.type-of($value) == "string" and string.slice(meta.inspect($value), 1, 1) != "\"" {
49
+ $text: string.to-lower-case($value);
50
+ @if $text == "currentcolor" {
51
+ @return true;
52
+ }
53
+ $open: string.index($text, "(");
54
+ @if $open and list.index("var" "color-mix" "light-dark" "env", string.slice($text, 1, $open - 1)) {
55
+ @return true;
56
+ }
57
+ }
58
+ @return false;
59
+ }
60
+
61
+ // The logical directions point along the writing direction rather than a fixed
62
+ // side: inline-end is right in a left-to-right page and left in a right-to-left
63
+ // one. They are kept in a list of their own on purpose. $list-of-directions is
64
+ // shared with border-radius, whose corner branches have no final @else, so a
65
+ // keyword added there without a branch would be accepted and emit nothing.
66
+ $-logical-directions: ("inline-start", "inline-end", "block-start", "block-end");
7
67
 
8
68
  @mixin triangle($direction: "bottom", $color: black, $size: 10px 8px) {
9
- @if list.index($list-of-directions, $direction) {
10
- @if isColor($color){
69
+ @if list.index($list-of-directions, $direction) or list.index($-logical-directions, $direction) {
70
+ // Halving a size in calc() rather than math.div() keeps var() and clamp()
71
+ // working instead of emitting `var(--w)/2`, which is not valid CSS.
72
+ @each $item in $size {
73
+ @if not -is-size($item) {
74
+ @error "`#{meta.inspect($size)}` is not a valid $size for `triangle`. Pass one or two lengths, or var(), env() or a calculation in place of either.";
75
+ }
76
+ }
77
+ @if not -is-css-color($color) {
78
+ @error "`#{meta.inspect($color)}` is not a valid $color for `triangle`. Pass a colour, `currentColor`, or a CSS function such as `var(--accent)`, `color-mix(...)` or `light-dark(...)`.";
79
+ } @else {
11
80
  content: "";
12
81
  height: 0;
13
82
  width: 0;
@@ -15,32 +84,63 @@
15
84
  border-style: solid;
16
85
  @if $direction == "top"{
17
86
  border-color: transparent transparent $color;
18
- border-width: 0 math.div(list.nth($size, 1), 2) if(list.length($size) == 2, list.nth($size, 2), list.nth($size, 1));
87
+ border-width: 0 calc(list.nth($size, 1) / 2) if(list.length($size) == 2, list.nth($size, 2), list.nth($size, 1));
19
88
  } @else if $direction == "top-right" {
20
89
  border-color: transparent $color transparent transparent;
21
90
  border-width: 0 list.nth($size, 1) list.nth($size, 1) 0;
22
91
  } @else if $direction == "right" {
23
92
  border-color: transparent transparent transparent $color;
24
- border-width: math.div(if(list.length($size) == 2, list.nth($size, 2), list.nth($size, 1)), 2) 0 math.div(if(list.length($size) == 2, list.nth($size, 2), list.nth($size, 1)), 2) list.nth($size, 1);
93
+ border-width: calc(if(list.length($size) == 2, list.nth($size, 2), list.nth($size, 1)) / 2) 0 calc(if(list.length($size) == 2, list.nth($size, 2), list.nth($size, 1)) / 2) list.nth($size, 1);
25
94
  } @else if $direction == "bottom-right" {
26
95
  border-color: transparent transparent $color;
27
96
  border-width: 0 0 list.nth($size, 1) list.nth($size, 1);
28
97
  } @else if $direction == "bottom" {
29
98
  border-color: $color transparent transparent;
30
- border-width: if(list.length($size) == 2, list.nth($size, 2), list.nth($size, 1)) math.div(list.nth($size, 1), 2) 0;
99
+ border-width: if(list.length($size) == 2, list.nth($size, 2), list.nth($size, 1)) calc(list.nth($size, 1) / 2) 0;
31
100
  } @else if $direction == "bottom-left" {
32
101
  border-color: transparent transparent transparent $color;
33
102
  border-width: list.nth($size, 1) 0 0 list.nth($size, 1);
34
103
  } @else if $direction == "left" {
35
104
  border-color: transparent $color transparent transparent;
36
- border-width: math.div(if(list.length($size) == 2, list.nth($size, 2), list.nth($size, 1)), 2) list.nth($size, 1) math.div(if(list.length($size) == 2, list.nth($size, 2), list.nth($size, 1)), 2) 0;
105
+ border-width: calc(if(list.length($size) == 2, list.nth($size, 2), list.nth($size, 1)) / 2) list.nth($size, 1) calc(if(list.length($size) == 2, list.nth($size, 2), list.nth($size, 1)) / 2) 0;
37
106
  } @else if $direction == "top-left" {
38
107
  border-color: $color transparent transparent;
39
108
  border-width: list.nth($size, 1) list.nth($size, 1) 0 0;
109
+ } @else {
110
+ // The same shapes as right, left, bottom and top, drawn with logical
111
+ // borders so the browser picks the side. The first size is the length
112
+ // the triangle points along, the second the width across it.
113
+ $along: list.nth($size, 1);
114
+ $across: $along;
115
+ @if list.length($size) == 2 {
116
+ $across: list.nth($size, 2);
117
+ }
118
+ border-color: transparent;
119
+ @if $direction == "inline-end" {
120
+ border-inline-start-color: $color;
121
+ border-block-width: calc($across / 2);
122
+ border-inline-start-width: $along;
123
+ border-inline-end-width: 0;
124
+ } @else if $direction == "inline-start" {
125
+ border-inline-end-color: $color;
126
+ border-block-width: calc($across / 2);
127
+ border-inline-start-width: 0;
128
+ border-inline-end-width: $along;
129
+ } @else if $direction == "block-end" {
130
+ border-block-start-color: $color;
131
+ border-inline-width: calc($along / 2);
132
+ border-block-start-width: $across;
133
+ border-block-end-width: 0;
134
+ } @else {
135
+ border-block-end-color: $color;
136
+ border-inline-width: calc($along / 2);
137
+ border-block-start-width: 0;
138
+ border-block-end-width: $across;
139
+ }
40
140
  }
41
141
  @content;
42
142
  }
43
143
  } @else {
44
- @error "The argument for direction must be one of the followings: #{$list-of-directions}";
144
+ @error "The argument for direction must be one of the followings: #{list.join($list-of-directions, $-logical-directions)}";
45
145
  }
46
146
  }
@@ -1,7 +1,11 @@
1
1
  @charset "UTF-8";
2
2
 
3
3
  @use "sass:math";
4
+ @use "sass:meta";
4
5
 
5
6
  @function clearUnit($value) {
7
+ @if meta.type-of($value) != "number" {
8
+ @error "`#{meta.inspect($value)}` is not a valid $value for `clearUnit`. Pass a number, such as `24px`.";
9
+ }
6
10
  @return math.div($value, ($value * 0 + 1));
7
11
  }
@@ -2,8 +2,14 @@
2
2
 
3
3
  @use "sass:string";
4
4
  @use "sass:math";
5
+ @use "sass:meta";
5
6
 
6
7
  @function convertToEm($value) {
8
+ // Only a pixel length converts cleanly. A unitless number or another unit
9
+ // compiled to `calc(1.5 / 1px)em`, and a word or var() to `var(--x)/16pxem`.
10
+ @if meta.type-of($value) != "number" or math.unit($value) != "px" {
11
+ @error "`#{meta.inspect($value)}` is not a valid $value for `convertToEm`. Pass a pixel length, such as `24px`.";
12
+ }
7
13
  $base-value: 16px;
8
14
  $rem-value: math.div($value, $base-value);
9
15
  @return $rem-value + string.unquote("em");
@@ -11,6 +11,21 @@
11
11
  }
12
12
 
13
13
  $string-numerals: "0" "1" "2" "3" "4" "5" "6" "7" "8" "9";
14
+
15
+ // Every character has to be a digit before the parse runs. Otherwise an
16
+ // empty string or a sign failed with Sass's own "Undefined operation", and a
17
+ // letter or a space was folded into nonsense such as `40-1`. The empty case
18
+ // is checked first because `@for $i from 1 through 0` counts down, not zero
19
+ // times.
20
+ @if string.length($value) == 0 {
21
+ @error "`\"\"` is not a valid $value for `convertToNumber`. Pass a string made only of digits, such as `\"42\"`.";
22
+ }
23
+ @for $i from 1 through string.length($value) {
24
+ @if not list.index($string-numerals, string.slice($value, $i, $i)) {
25
+ @error "`#{meta.inspect($value)}` is not a valid $value for `convertToNumber`. Pass a string made only of digits, such as `\"42\"`.";
26
+ }
27
+ }
28
+
14
29
  $calc: 0;
15
30
 
16
31
  @for $i from 1 through string.length($value) {
@@ -1,5 +1,19 @@
1
1
  @charset "UTF-8";
2
2
 
3
+ @use "sass:math";
4
+ @use "sass:meta";
5
+
3
6
  @function fontSizer($size, $time) {
7
+ @if meta.type-of($size) != "number" {
8
+ @error "`#{meta.inspect($size)}` is not a valid $size for `fontSizer`. Pass a length, such as `16px`.";
9
+ }
10
+ @if meta.type-of($time) != "number" {
11
+ @error "`#{meta.inspect($time)}` is not a valid $time for `fontSizer`. Pass a multiplier, such as `1.5`.";
12
+ }
13
+ // Two units multiply into a unit squared, which Sass writes as
14
+ // `calc(16px * 1px)`: not a length anywhere in CSS.
15
+ @if not math.is-unitless($size) and not math.is-unitless($time) {
16
+ @error "`fontSizer(#{$size}, #{$time})` multiplies two units, which is not a length. Pass the multiplier without a unit, such as `1.5`.";
17
+ }
4
18
  @return $size * $time;
5
19
  }