gerillass 2.3.0 → 3.0.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 (55) hide show
  1. package/README.md +46 -7
  2. package/SKILL.md +37 -12
  3. package/gerillass.json +527 -145
  4. package/package.json +1 -1
  5. package/scss/internal/_color-problem.scss +99 -0
  6. package/scss/internal/_color-stops-problem.scss +94 -0
  7. package/scss/internal/_condition-width.scss +22 -0
  8. package/scss/internal/_custom-property-in.scss +25 -0
  9. package/scss/internal/_gradient.scss +425 -0
  10. package/scss/internal/_image-value.scss +68 -0
  11. package/scss/internal/_is-condition-value.scss +94 -0
  12. package/scss/internal/_is-css-function.scss +21 -0
  13. package/scss/internal/_is-size.scss +22 -0
  14. package/scss/internal/_keyword-value.scss +76 -0
  15. package/scss/internal/_length-problem.scss +127 -0
  16. package/scss/internal/_sibling-index.scss +51 -0
  17. package/scss/internal/_words.scss +35 -0
  18. package/scss/library/_adaptive.scss +17 -0
  19. package/scss/library/_after.scss +2 -12
  20. package/scss/library/_background-dots.scss +36 -43
  21. package/scss/library/_background-image.scss +40 -52
  22. package/scss/library/_background-stripes.scss +40 -54
  23. package/scss/library/_before.scss +2 -12
  24. package/scss/library/_border-radius.scss +83 -0
  25. package/scss/library/_brand-logo.scss +10 -24
  26. package/scss/library/_breakpoint.scss +19 -26
  27. package/scss/library/_circle.scss +7 -0
  28. package/scss/library/_columnizer.scss +62 -2
  29. package/scss/library/_container-query.scss +13 -29
  30. package/scss/library/_container.scss +2 -19
  31. package/scss/library/_counter.scss +3 -14
  32. package/scss/library/_ellipsis.scss +78 -1
  33. package/scss/library/_except.scss +15 -0
  34. package/scss/library/_focus-ring.scss +29 -0
  35. package/scss/library/_font-face.scss +117 -1
  36. package/scss/library/_gradient.scss +31 -0
  37. package/scss/library/_index.scss +1 -2
  38. package/scss/library/_line-clamp.scss +2 -13
  39. package/scss/library/_only.scss +16 -0
  40. package/scss/library/_position.scss +34 -3
  41. package/scss/library/_remove.scss +1 -1
  42. package/scss/library/_resizable.scss +29 -2
  43. package/scss/library/_screen-agent.scss +8 -22
  44. package/scss/library/_sizer.scss +9 -0
  45. package/scss/library/_sprite.scss +65 -0
  46. package/scss/library/_text-gradient.scss +36 -18
  47. package/scss/library/_text-image.scss +2 -23
  48. package/scss/library/_text-stroke.scss +30 -0
  49. package/scss/library/_triangle.scss +25 -56
  50. package/scss/utilities/_gradient-value.scss +24 -0
  51. package/scss/utilities/_index.scss +1 -0
  52. package/scss/utilities/_validate-breakpoint.scss +32 -6
  53. package/scss/utilities/_validate-scissors.scss +7 -0
  54. package/scss/library/_linear-gradient.scss +0 -26
  55. package/scss/library/_radial-gradient.scss +0 -30
package/package.json CHANGED
@@ -16,7 +16,7 @@
16
16
  }
17
17
  },
18
18
  "license": "Apache-2.0",
19
- "version": "2.3.0",
19
+ "version": "3.0.0",
20
20
  "repository": {
21
21
  "type": "git",
22
22
  "url": "git+https://github.com/selfishprimate/gerillass.git"
@@ -0,0 +1,99 @@
1
+ @charset "UTF-8";
2
+
3
+ @use "sass:list";
4
+ @use "sass:meta";
5
+ @use "sass:string";
6
+
7
+ // Shared by the mixins and not public API; see _is-css-function.scss.
8
+
9
+ // What a browser keeps as a colour, measured in Chrome 152 in `color`,
10
+ // `-webkit-text-stroke-color`, `outline: 2px solid ...`, `background` and a
11
+ // gradient's colour stop:
12
+ //
13
+ // - kept: a colour Sass knows (red, #333, rgba(), hsl(), oklch(), color()),
14
+ // currentColor in any case, every system colour, deprecated ones included
15
+ // (Canvas, AccentColor, ActiveBorder), a vendor colour such as -webkit-link,
16
+ // var() and env(), and the colour functions Sass leaves as text: rgb() with
17
+ // var() in it, rgb(from ...), color-mix(), light-dark() and contrast-color();
18
+ // - dropped: a word that is none of those (huge, auto, none), a quoted colour,
19
+ // a length, a number, calc(), and two colours side by side;
20
+ // - CSS-wide keywords were kept in a declaration of their own and dropped inside
21
+ // `outline` and inside a gradient, so $css-wide says which this is.
22
+ //
23
+ // device-cmyk() is in the specification and Chrome drops it; it is let through,
24
+ // as `run-in` is for display, because refusing it would refuse valid CSS once a
25
+ // browser ships it.
26
+ //
27
+ // Deliberately looser than isColor, which stays strict because tint and shade
28
+ // pass its result to color.mix, and that needs a real Sass colour.
29
+ $-system-colors: "canvas" "canvastext" "linktext" "visitedtext" "activetext" "buttonface" "buttontext" "buttonborder" "field" "fieldtext" "highlight" "highlighttext" "selecteditem" "selecteditemtext" "mark" "marktext" "graytext" "accentcolor" "accentcolortext" "activeborder" "activecaption" "appworkspace" "background" "buttonhighlight" "buttonshadow" "captiontext" "inactiveborder" "inactivecaption" "inactivecaptiontext" "infobackground" "infotext" "menu" "menutext" "scrollbar" "threeddarkshadow" "threedface" "threedhighlight" "threedlightshadow" "threedshadow" "window" "windowframe" "windowtext";
30
+ $-functions: "var" "env" "attr" "rgb" "rgba" "hsl" "hsla" "hwb" "lab" "lch" "oklab" "oklch" "color" "color-mix" "light-dark" "contrast-color" "device-cmyk";
31
+ $-css-wide: "inherit" "initial" "unset" "revert" "revert-layer";
32
+
33
+ // The CSS named colours and transparent, for a colour that reaches the check as
34
+ // an unquoted string rather than as a Sass colour.
35
+ $-named-colors: "transparent" "aliceblue" "antiquewhite" "aqua" "aquamarine" "azure" "beige" "bisque" "black" "blanchedalmond" "blue" "blueviolet" "brown" "burlywood" "cadetblue" "chartreuse" "chocolate" "coral" "cornflowerblue" "cornsilk" "crimson" "cyan" "darkblue" "darkcyan" "darkgoldenrod" "darkgray" "darkgreen" "darkgrey" "darkkhaki" "darkmagenta" "darkolivegreen" "darkorange" "darkorchid" "darkred" "darksalmon" "darkseagreen" "darkslateblue" "darkslategray" "darkslategrey" "darkturquoise" "darkviolet" "deeppink" "deepskyblue" "dimgray" "dimgrey" "dodgerblue" "firebrick" "floralwhite" "forestgreen" "fuchsia" "gainsboro" "ghostwhite" "gold" "goldenrod" "gray" "green" "greenyellow" "grey" "honeydew" "hotpink" "indianred" "indigo" "ivory" "khaki" "lavender" "lavenderblush" "lawngreen" "lemonchiffon" "lightblue" "lightcoral" "lightcyan" "lightgoldenrodyellow" "lightgray" "lightgreen" "lightgrey" "lightpink" "lightsalmon" "lightseagreen" "lightskyblue" "lightslategray" "lightslategrey" "lightsteelblue" "lightyellow" "lime" "limegreen" "linen" "magenta" "maroon" "mediumaquamarine" "mediumblue" "mediumorchid" "mediumpurple" "mediumseagreen" "mediumslateblue" "mediumspringgreen" "mediumturquoise" "mediumvioletred" "midnightblue" "mintcream" "mistyrose" "moccasin" "navajowhite" "navy" "oldlace" "olive" "olivedrab" "orange" "orangered" "orchid" "palegoldenrod" "palegreen" "paleturquoise" "palevioletred" "papayawhip" "peachpuff" "peru" "pink" "plum" "powderblue" "purple" "rebeccapurple" "red" "rosybrown" "royalblue" "saddlebrown" "salmon" "sandybrown" "seagreen" "seashell" "sienna" "silver" "skyblue" "slateblue" "slategray" "slategrey" "snow" "springgreen" "steelblue" "tan" "teal" "thistle" "tomato" "turquoise" "violet" "wheat" "white" "whitesmoke" "yellow" "yellowgreen";
36
+
37
+ // A hex colour written as text: # and then 3, 4, 6 or 8 hex digits.
38
+ @function -is-hex($text) {
39
+ @if string.index($text, "#") != 1 or not list.index(4 5 7 9, string.length($text)) {
40
+ @return false;
41
+ }
42
+ @for $i from 2 through string.length($text) {
43
+ @if not string.index("0123456789abcdef", string.slice($text, $i, $i)) {
44
+ @return false;
45
+ }
46
+ }
47
+ @return true;
48
+ }
49
+
50
+ // Returns what is wrong with $value as one colour, or null when a browser keeps
51
+ // it. null itself is the caller's to decide, since it means a default in some
52
+ // mixins and leaving a declaration out in others.
53
+ @function colorProblem($value, $css-wide: false) {
54
+ @if meta.type-of($value) == "color" {
55
+ @return null;
56
+ }
57
+ @if meta.type-of($value) == "list" and list.length($value) > 1 {
58
+ @return "it is several values where one colour is meant";
59
+ }
60
+ @if meta.type-of($value) != "string" {
61
+ @return "it is not a colour";
62
+ }
63
+ @if string.slice(meta.inspect($value), 1, 1) == "\"" {
64
+ @return "a colour in quotes is text, not a colour";
65
+ }
66
+ $lower: string.to-lower-case($value);
67
+ $open: string.index($lower, "(");
68
+ @if $open {
69
+ @if list.index($-functions, string.slice($lower, 1, $open - 1)) {
70
+ @return null;
71
+ }
72
+ @return "`#{string.slice($value, 1, $open - 1)}()` is not a colour function";
73
+ }
74
+ @if $lower == "currentcolor" or list.index($-system-colors, $lower) {
75
+ @return null;
76
+ }
77
+ // Interpolation, such as `#{$name}` or `#{$hex}`, gives an unquoted string
78
+ // rather than a colour, and it is written into the CSS as it reads, so a
79
+ // named or hex colour in one works exactly as the colour itself does.
80
+ @if list.index($-named-colors, $lower) or -is-hex($lower) {
81
+ @return null;
82
+ }
83
+ // Vendor colours are named rather than matched by prefix. Measured in Chrome
84
+ // 152, `-webkit-link` and `-webkit-activelink` were kept as colours while
85
+ // `-webkit-fill-available`, `-webkit-box` and `-webkit-text` were dropped, and
86
+ // the prefix rule this replaced had let those through. `-webkit-focus-ring-color`
87
+ // is Safari's, and `-moz-` colours are Firefox's; neither could be measured
88
+ // here, so they are let through rather than refused unseen.
89
+ @if list.index("-webkit-link" "-webkit-activelink" "-webkit-focus-ring-color", $lower) or string.index($lower, "-moz-") == 1 {
90
+ @return null;
91
+ }
92
+ @if list.index($-css-wide, $lower) {
93
+ @if $css-wide {
94
+ @return null;
95
+ }
96
+ @return "a CSS-wide keyword only works as a whole declaration, not inside this value";
97
+ }
98
+ @return "it is not a colour";
99
+ }
@@ -0,0 +1,94 @@
1
+ @charset "UTF-8";
2
+
3
+ @use "sass:list";
4
+ @use "sass:meta";
5
+ @use "sass:string";
6
+ @use "color-problem" as *;
7
+ @use "is-condition-value" as *;
8
+
9
+ // Shared by the mixins and not public API; see _is-css-function.scss.
10
+
11
+ // Returns what is wrong with a list of gradient colour stops, or null. The items
12
+ // are read the way the mixins write them, one per comma in the gradient.
13
+ // Measured in Chrome 152 in linear-gradient():
14
+ //
15
+ // - kept: a colour on its own, even a single one (`linear-gradient(to top,
16
+ // red)`), a colour with one or two positions (`red 0 10%`), a position between
17
+ // two colours as a hint (`red, 30%, blue`), and var() as a whole stop;
18
+ // - dropped: a position first or last, two positions in a row, a colour with
19
+ // three positions, a position that is not a length or a percentage
20
+ // (`red 10deg`), a colour and another colour in one stop (`red 10% blue`), and
21
+ // anything colorProblem refuses, CSS-wide keywords included.
22
+
23
+ // True for a value a stop takes as a position: a length, a percentage, 0 or a
24
+ // calculation.
25
+ @function -is-position($value) {
26
+ @return isConditionValue($value, "length-percentage");
27
+ }
28
+
29
+ // True for var(), env() or attr(), which may stand for a whole stop or a list of
30
+ // them, so nothing around it can be judged.
31
+ @function -is-substitution($value) {
32
+ @if meta.type-of($value) != "string" or string.slice(meta.inspect($value), 1, 1) == "\"" {
33
+ @return false;
34
+ }
35
+ $lower: string.to-lower-case($value);
36
+ @return string.index($lower, "var(") == 1 or string.index($lower, "env(") == 1 or string.index($lower, "attr(") == 1;
37
+ }
38
+
39
+ @function colorStopsProblem($colors) {
40
+ @if $colors == null or list.length($colors) == 0 {
41
+ @return "a gradient needs at least one colour";
42
+ }
43
+ $count: list.length($colors);
44
+ $previous: null;
45
+ $colors-seen: 0;
46
+ @for $i from 1 through $count {
47
+ $item: list.nth($colors, $i);
48
+ $kind: null;
49
+ @if -is-substitution($item) {
50
+ $kind: "substitution";
51
+ } @else if meta.type-of($item) != "list" and -is-position($item) {
52
+ $kind: "hint";
53
+ } @else if meta.type-of($item) == "list" and list.length($item) > 1 {
54
+ $problem: colorProblem(list.nth($item, 1));
55
+ @if $problem {
56
+ @return "`#{meta.inspect($item)}` does not start with a colour: #{$problem}";
57
+ }
58
+ @if list.length($item) > 3 {
59
+ @return "`#{meta.inspect($item)}` has more than two positions, and a colour stop takes at most two";
60
+ }
61
+ @for $j from 2 through list.length($item) {
62
+ @if not -is-position(list.nth($item, $j)) and not -is-substitution(list.nth($item, $j)) {
63
+ @return "in `#{meta.inspect($item)}`, `#{meta.inspect(list.nth($item, $j))}` is not a position; a stop is a colour followed by up to two lengths or percentages";
64
+ }
65
+ }
66
+ $kind: "stop";
67
+ } @else {
68
+ $problem: colorProblem($item);
69
+ @if $problem {
70
+ @return "`#{meta.inspect($item)}` is not a colour stop: #{$problem}";
71
+ }
72
+ $kind: "stop";
73
+ }
74
+
75
+ @if $kind == "hint" {
76
+ @if $i == 1 {
77
+ @return "it starts with the position `#{meta.inspect($item)}`; a position on its own goes between two colours";
78
+ }
79
+ @if $i == $count {
80
+ @return "it ends with the position `#{meta.inspect($item)}`; a position on its own goes between two colours";
81
+ }
82
+ @if $previous == "hint" {
83
+ @return "it has two positions in a row; a position on its own goes between two colours";
84
+ }
85
+ } @else {
86
+ $colors-seen: $colors-seen + 1;
87
+ }
88
+ $previous: $kind;
89
+ }
90
+ @if $colors-seen == 0 {
91
+ @return "a gradient needs at least one colour";
92
+ }
93
+ @return null;
94
+ }
@@ -0,0 +1,22 @@
1
+ @charset "UTF-8";
2
+
3
+ @use "sass:meta";
4
+ @use "../utilities/validate-breakpoint" as *;
5
+ @use "is-condition-value" as *;
6
+
7
+ // Shared by the mixins and not public API; see _is-css-function.scss.
8
+
9
+ // Resolves a size for a width condition and refuses one no condition can match.
10
+ // validateBreakpoint turns a key into its width and refuses a word that is not a
11
+ // key; it passes a percentage or a bare number through, because a declaration
12
+ // takes them. A width condition does not: `(min-width: 50%)` and
13
+ // `(min-width: 768)` never matched in Chrome 152, so they are refused here.
14
+ // Returns the same value validateBreakpoint does, so a call that worked writes
15
+ // the same query.
16
+ @function conditionWidth($value, $mixin, $at-rule: "@media") {
17
+ $width: validateBreakpoint($value);
18
+ @if not isConditionValue($width, "length") {
19
+ @error "`#{meta.inspect($value)}` cannot be used as a size in `#{$mixin}`: a width condition takes a length, and one written with `#{meta.inspect($width)}` would never match in #{$at-rule}. Pass a length such as `600px`, `40em` or `calc(30em + 1px)`, or a key from $map-for-breakpoints.";
20
+ }
21
+ @return $width;
22
+ }
@@ -0,0 +1,25 @@
1
+ @charset "UTF-8";
2
+
3
+ @use "sass:meta";
4
+ @use "sass:string";
5
+
6
+ // Shared by the mixins and not public API; see _is-css-function.scss.
7
+
8
+ // Returns the first value in $value that holds var(), looking inside lists and
9
+ // calculations, or null. Custom properties are not evaluated in a @media or
10
+ // @container condition, so a rule built from one never applies, and nothing
11
+ // warns. Measured in Chrome 152: `(min-width: var(--wide))` did not match with
12
+ // `--wide: 200px`, while `(min-width: 200px)` did.
13
+ @function customPropertyIn($value) {
14
+ @each $item in $value {
15
+ @if meta.type-of($item) == "list" or meta.type-of($item) == "arglist" {
16
+ $found: customPropertyIn($item);
17
+ @if $found {
18
+ @return $found;
19
+ }
20
+ } @else if string.index(meta.inspect($item), "var(") {
21
+ @return $item;
22
+ }
23
+ }
24
+ @return null;
25
+ }
@@ -0,0 +1,425 @@
1
+ @charset "UTF-8";
2
+
3
+ @use "sass:list";
4
+ @use "sass:map";
5
+ @use "sass:math";
6
+ @use "sass:meta";
7
+ @use "sass:string";
8
+ @use "../maps/map-for-directions" as *;
9
+ @use "../maps/map-for-positions" as *;
10
+ @use "color-stops-problem" as *;
11
+ @use "is-condition-value" as *;
12
+ @use "is-css-function" as *;
13
+ @use "words" as *;
14
+
15
+ // Shared by the mixins and not public API; see _is-css-function.scss.
16
+ //
17
+ // Builds a linear, radial or conic gradient, plain or repeating, with an
18
+ // optional colour interpolation space, and checks every part of it. The
19
+ // gradient mixin, the gradientValue function, text-gradient, background-image,
20
+ // background-dots and background-stripes all write their gradients through it.
21
+ //
22
+ // What Chrome 152 kept and dropped, checked with CSS.supports in
23
+ // background-image:
24
+ //
25
+ // - a linear direction: an angle in deg, grad, rad or turn, a unitless 0, calc(),
26
+ // and `to` with one side or a side and a corner in either order (`to left
27
+ // top`); `to center`, `to top bottom` and a length were dropped;
28
+ // - a conic start: `from` an angle or 0, dropped with a length; a conic position
29
+ // takes `at` and a position like radial, and a size or shape dropped it;
30
+ // - a conic stop: an angle, a percentage, 0 or calc(), dropped with a length;
31
+ // - `in` a space: srgb, srgb-linear, display-p3, display-p3-linear, a98-rgb,
32
+ // prophoto-rgb, rec2020, lab, oklab, xyz, xyz-d50 and xyz-d65, or a polar
33
+ // hsl, hwb, lch or oklch with an optional `shorter`, `longer`, `increasing` or
34
+ // `decreasing hue`; a hue method on a rectangular space, `hue` without a
35
+ // method, a method without `hue`, and `rgb` or `hsv` were dropped. `in
36
+ // var(--s)` with `--s: oklab` computed to `in oklab`.
37
+ //
38
+ // The shape, size and position checks came from radial-gradient, removed in
39
+ // 3.0.0, with the measurements behind them.
40
+
41
+ $-types: "linear" "radial" "conic";
42
+ $-rectangular-spaces: "srgb" "srgb-linear" "display-p3" "display-p3-linear" "a98-rgb" "prophoto-rgb" "rec2020" "lab" "oklab" "xyz" "xyz-d50" "xyz-d65";
43
+ $-polar-spaces: "hsl" "hwb" "lch" "oklch";
44
+ $-hue-methods: "shorter" "longer" "increasing" "decreasing";
45
+ $-sides: "top" "bottom" "left" "right";
46
+
47
+ $-extents: "closest-side" "closest-corner" "farthest-side" "farthest-corner";
48
+ $-places: "left" "center" "right" "top" "bottom";
49
+
50
+ // "extent", "length" or "percentage" for a word that can size the gradient,
51
+ // or null.
52
+ @function -size-kind($word) {
53
+ $lower: string.to-lower-case("#{$word}");
54
+ @if list.index($-extents, $lower) {
55
+ @return "extent";
56
+ }
57
+ @if string.index($lower, "-") == 1 {
58
+ @return null;
59
+ }
60
+ @if isConditionValue($word, "length") {
61
+ @return "length";
62
+ }
63
+ @if isConditionValue($word, "length-percentage") {
64
+ @return "percentage";
65
+ }
66
+ @return null;
67
+ }
68
+
69
+ // Checks the size words before `at`. $shape is the shape named, or null.
70
+ @function -size-problem($words, $shape) {
71
+ $extents: 0;
72
+ $lengths: 0;
73
+ $percentages: 0;
74
+ @each $word in $words {
75
+ $kind: -size-kind($word);
76
+ @if $kind == null {
77
+ @return "`#{$word}` is not a shape, an extent keyword such as `closest-side`, or a length";
78
+ } @else if $kind == "extent" {
79
+ $extents: $extents + 1;
80
+ } @else if $kind == "length" {
81
+ $lengths: $lengths + 1;
82
+ } @else {
83
+ $percentages: $percentages + 1;
84
+ }
85
+ }
86
+ $sizes: $lengths + $percentages;
87
+ @if $extents > 1 or ($extents == 1 and $sizes > 0) {
88
+ @return "it gives the size twice";
89
+ }
90
+ @if $sizes > 2 {
91
+ @return "a size is one length or two";
92
+ }
93
+ @if $sizes == 1 and ($percentages == 1 or $shape == "ellipse") {
94
+ @return "one size is a circle's radius, which is a length and not a percentage";
95
+ }
96
+ @if $sizes == 2 and $shape == "circle" {
97
+ @return "a circle takes one length, not two";
98
+ }
99
+ @return null;
100
+ }
101
+
102
+ @function -shape-problem($shape) {
103
+ @if string.index($shape, "(") {
104
+ @return null;
105
+ }
106
+ $named: null;
107
+ $sizes: ();
108
+ @each $word in wordsOf($shape) {
109
+ $lower: string.to-lower-case($word);
110
+ @if list.index("circle" "ellipse", $lower) {
111
+ @if $named {
112
+ @return "it names the shape twice";
113
+ }
114
+ $named: $lower;
115
+ } @else {
116
+ $sizes: list.append($sizes, $word);
117
+ }
118
+ }
119
+ @return -size-problem($sizes, $named);
120
+ }
121
+
122
+ @function -position-problem($position) {
123
+ @if map.has-key($map-for-positions, $position) or string.index($position, "(") {
124
+ @return null;
125
+ }
126
+ $sizes: ();
127
+ $places: ();
128
+ $after-at: false;
129
+ @each $word in wordsOf($position) {
130
+ $lower: string.to-lower-case($word);
131
+ @if $lower == "at" {
132
+ @if $after-at {
133
+ @return "it has `at` twice";
134
+ }
135
+ $after-at: true;
136
+ } @else if $after-at {
137
+ $places: list.append($places, $word);
138
+ } @else if list.index("circle" "ellipse", $lower) {
139
+ @return "the shape goes in $shape, not $position";
140
+ } @else {
141
+ $sizes: list.append($sizes, $word);
142
+ }
143
+ }
144
+ $problem: -size-problem($sizes, null);
145
+ @if $problem {
146
+ @return "#{$problem}. Pass a position name such as `center` or `top-left`, an extent keyword such as `closest-side`, or `at` followed by a position, such as `at 30% 40%`";
147
+ }
148
+ @if $after-at and list.length($places) == 0 {
149
+ @return "`at` needs a position after it, such as `at center`";
150
+ }
151
+ @if list.length($places) > 4 {
152
+ @return "a position takes at most four values";
153
+ }
154
+ $keywords: 0;
155
+ @each $word in $places {
156
+ @if list.index($-places, string.to-lower-case($word)) {
157
+ $keywords: $keywords + 1;
158
+ } @else if not isConditionValue($word, "length-percentage") {
159
+ @return "`#{$word}` is not a position; after `at` pass left, center, right, top, bottom or a length";
160
+ }
161
+ }
162
+ @if $keywords > 2 and $keywords == list.length($places) {
163
+ @return "a position of keywords alone takes at most two, such as `at top left`";
164
+ }
165
+ @return null;
166
+ }
167
+
168
+ // True for var(), env() or attr(), which may stand for anything, so it is
169
+ // written as it is.
170
+ @function -is-substitution($value) {
171
+ @return isCssFunction($value, "var" "env" "attr");
172
+ }
173
+
174
+ // True for an angle, a unitless 0, a calculation or var().
175
+ @function -is-angle($value) {
176
+ @return $value == 0 or isConditionValue($value, "angle") or -is-substitution($value);
177
+ }
178
+
179
+ // The CSS for a linear gradient's $direction, or null when a browser would
180
+ // drop it. background-image checks its $filter-direction with this.
181
+ @function gradientDirection($direction) {
182
+ @if map.has-key($map-for-directions, $direction) {
183
+ @return map.get($map-for-directions, $direction);
184
+ }
185
+ @if -is-angle($direction) {
186
+ @return $direction;
187
+ }
188
+ $words: wordsOf($direction);
189
+ @if meta.type-of($direction) == "string" and string.to-lower-case("#{list.nth($words, 1)}") == "to" and list.length($words) >= 2 and list.length($words) <= 3 {
190
+ $vertical: 0;
191
+ $horizontal: 0;
192
+ @for $i from 2 through list.length($words) {
193
+ $word: string.to-lower-case("#{list.nth($words, $i)}");
194
+ @if $word == "top" or $word == "bottom" {
195
+ $vertical: $vertical + 1;
196
+ } @else if $word == "left" or $word == "right" {
197
+ $horizontal: $horizontal + 1;
198
+ } @else {
199
+ @return null;
200
+ }
201
+ }
202
+ @if $vertical <= 1 and $horizontal <= 1 {
203
+ @return string.unquote($direction);
204
+ }
205
+ }
206
+ @return null;
207
+ }
208
+
209
+ // What is wrong with $in, or null.
210
+ @function -in-problem($in) {
211
+ // Measured in Chrome 152: `in var(--s)` with `--s: oklab` computed to
212
+ // `in oklab`, so var() is written as it is.
213
+ @if -is-substitution($in) {
214
+ @return null;
215
+ }
216
+ @if not list.index("string" "list", meta.type-of($in)) {
217
+ @return "it is not a colour space";
218
+ }
219
+ $words: ();
220
+ @each $word in wordsOf($in) {
221
+ $words: list.append($words, string.to-lower-case("#{$word}"));
222
+ }
223
+ $space: list.nth($words, 1);
224
+ $count: list.length($words);
225
+ @if list.index($-rectangular-spaces, $space) {
226
+ @if $count > 1 {
227
+ @return "a hue method such as `longer hue` needs a polar space: hsl, hwb, lch or oklch";
228
+ }
229
+ @return null;
230
+ }
231
+ @if list.index($-polar-spaces, $space) {
232
+ @if $count == 1 {
233
+ @return null;
234
+ }
235
+ @if $count == 3 and list.index($-hue-methods, list.nth($words, 2)) and list.nth($words, 3) == "hue" {
236
+ @return null;
237
+ }
238
+ @return "after a polar space comes a hue method and the word hue, such as `oklch longer hue`";
239
+ }
240
+ @return "`#{$space}` is not a colour space; pass one such as oklab or oklch";
241
+ }
242
+
243
+ // What is wrong with a conic gradient's stops, or null. A position there is an
244
+ // angle or a percentage, so each is checked for that and then replaced with 0,
245
+ // and the list goes through colorStopsProblem for everything else.
246
+ @function -conic-stops-problem($colors) {
247
+ $checked: ();
248
+ @each $item in $colors {
249
+ @if meta.type-of($item) == "list" and list.length($item) > 1 {
250
+ $stop: (list.nth($item, 1),);
251
+ @for $i from 2 through list.length($item) {
252
+ $position: list.nth($item, $i);
253
+ @if not -is-conic-position($position) {
254
+ @return "in `#{meta.inspect($item)}`, `#{meta.inspect($position)}` is not an angle or a percentage, and a conic stop is placed by one";
255
+ }
256
+ @if -is-substitution($position) {
257
+ $stop: list.append($stop, $position, space);
258
+ } @else {
259
+ $stop: list.append($stop, 0, space);
260
+ }
261
+ }
262
+ $checked: list.append($checked, $stop, comma);
263
+ } @else if meta.type-of($item) == "number" or isConditionValue($item, "length-percentage") or isConditionValue($item, "angle") {
264
+ @if not -is-conic-position($item) {
265
+ @return "`#{meta.inspect($item)}` is not an angle or a percentage, and a conic hint is placed by one";
266
+ }
267
+ $checked: list.append($checked, 0, comma);
268
+ } @else {
269
+ $checked: list.append($checked, $item, comma);
270
+ }
271
+ }
272
+ @return colorStopsProblem($checked);
273
+ }
274
+
275
+ @function -is-conic-position($value) {
276
+ @if -is-angle($value) or meta.type-of($value) == "calculation" {
277
+ @return true;
278
+ }
279
+ @if meta.type-of($value) == "number" {
280
+ @return math.unit($value) == "%";
281
+ }
282
+ $text: string.unquote("#{$value}");
283
+ @return string.slice($text, -1) == "%" and isConditionValue($value, "length-percentage");
284
+ }
285
+
286
+
287
+ // A gradient, as a value. $member, $colors-name and $direction-name are the
288
+ // names an error gives, so a mixin that passes its own arguments through names
289
+ // them as its user wrote them. $warn is false for the fallback a mixin writes
290
+ // before a gradient with $in, so a single colour warns once.
291
+ @function gradientOf(
292
+ $colors,
293
+ $type: linear,
294
+ $direction: null,
295
+ $shape: null,
296
+ $position: null,
297
+ $from: null,
298
+ $in: null,
299
+ $repeating: false,
300
+ $member: "gradient",
301
+ $colors-name: "$colors",
302
+ $direction-name: "$direction",
303
+ $warn: true
304
+ ) {
305
+ $kind: string.to-lower-case("#{$type}");
306
+ @if meta.type-of($type) != "string" or not list.index($-types, $kind) {
307
+ @error "`#{meta.inspect($type)}` is not a valid $type for `#{$member}`. Pass linear, radial or conic.";
308
+ }
309
+ @if meta.type-of($repeating) != "bool" {
310
+ @error "`#{meta.inspect($repeating)}` is not a valid $repeating for `#{$member}`. Pass true or false.";
311
+ }
312
+
313
+ // An argument the type does not read would be dropped without a word.
314
+ @if $direction != null and $kind != "linear" {
315
+ $hint: "Use $position for where a radial gradient sits.";
316
+ @if $kind == "conic" {
317
+ $hint: "Use $from for where a conic gradient starts.";
318
+ }
319
+ @error "#{$direction-name} is for a linear gradient, and this one is #{$kind}. #{$hint}";
320
+ }
321
+ @if $shape != null and $kind != "radial" {
322
+ @error "$shape is for a radial gradient, and this one is #{$kind}.";
323
+ }
324
+ @if $position != null and $kind == "linear" {
325
+ @error "$position is for a radial or conic gradient, and this one is linear. Use #{$direction-name}.";
326
+ }
327
+ @if $from != null and $kind != "conic" {
328
+ @error "$from is for a conic gradient, and this one is #{$kind}.";
329
+ }
330
+
331
+ $prelude: ();
332
+
333
+ @if $kind == "linear" and $direction != null {
334
+ $value: gradientDirection($direction);
335
+ @if $value == null {
336
+ @error "`#{meta.inspect($direction)}` is not a valid #{$direction-name} for `#{$member}`. Pass an angle such as `45deg` or `0.25turn`, `\"to top right\"`, or one of: #{map.keys($map-for-directions)}.";
337
+ }
338
+ $prelude: list.append($prelude, $value, space);
339
+ }
340
+
341
+ // A shape or position written without quotes, such as `ellipse 70% 60%` or
342
+ // `at 50% 0%`, reaches here as a space-separated list; it is the same words.
343
+ @if meta.type-of($shape) == "list" and list.separator($shape) == "space" {
344
+ $shape: "#{$shape}";
345
+ }
346
+ @if meta.type-of($position) == "list" and list.separator($position) == "space" {
347
+ $position: "#{$position}";
348
+ }
349
+
350
+ @if $kind == "radial" and $shape != null {
351
+ @if meta.type-of($shape) != "string" {
352
+ @error "`#{meta.inspect($shape)}` is not a valid $shape for `#{$member}`. Pass circle or ellipse, optionally with a size.";
353
+ }
354
+ $shape: string.unquote($shape);
355
+ $problem: -shape-problem($shape);
356
+ @if $problem {
357
+ @error "`#{$shape}` is not a valid $shape for `#{$member}`: #{$problem}. A browser drops the whole gradient.";
358
+ }
359
+ $prelude: list.append($prelude, $shape, space);
360
+ }
361
+
362
+ @if $kind == "conic" and $from != null {
363
+ @if not -is-angle($from) {
364
+ @error "`#{meta.inspect($from)}` is not a valid $from for `#{$member}`. Pass an angle, such as `45deg` or `0.25turn`.";
365
+ }
366
+ $prelude: list.append($prelude, string.unquote("from #{$from}"), space);
367
+ }
368
+
369
+ @if $position != null {
370
+ @if meta.type-of($position) != "string" {
371
+ @error "`#{meta.inspect($position)}` is not a valid $position for `#{$member}`. Pass a position name such as `center` or `top-left`, or a CSS position as a string, such as `\"at 30% 40%\"`.";
372
+ }
373
+ $position: string.unquote($position);
374
+ @if map.has-key($map-for-positions, $position) {
375
+ $prelude: list.append($prelude, map.get($map-for-positions, $position), space);
376
+ } @else {
377
+ $problem: -position-problem($position);
378
+ @if not $problem and $kind == "conic" and not string.index($position, "(") and string.to-lower-case(list.nth(wordsOf($position), 1)) != "at" {
379
+ $problem: "a conic gradient has no size, so its position starts with `at`";
380
+ }
381
+ @if $problem {
382
+ @error "`#{$position}` is not a valid $position for `#{$member}`: #{$problem}. A browser drops the whole gradient.";
383
+ }
384
+ $prelude: list.append($prelude, $position, space);
385
+ }
386
+ }
387
+
388
+ $problem: null;
389
+ @if $kind == "conic" {
390
+ $problem: -conic-stops-problem($colors);
391
+ } @else {
392
+ $problem: colorStopsProblem($colors);
393
+ }
394
+ @if $problem {
395
+ @error "`#{meta.inspect($colors)}` is not a valid #{$colors-name} for `#{$member}`: #{$problem}. A browser drops the whole gradient.";
396
+ }
397
+ // From the compat data, not measured in an old browser: a gradient with one
398
+ // colour is kept only from Chrome 135, Firefox 136 and Safari 18.4.
399
+ @if $warn and list.length($colors) == 1 and not -is-substitution(list.nth($colors, 1)) {
400
+ @warn "`#{$member}` was given one colour, `#{meta.inspect($colors)}`. Browsers before Chrome 135, Firefox 136 and Safari 18.4 drop a gradient with a single colour stop; pass two colours, or use background-color.";
401
+ }
402
+
403
+ @if $in != null {
404
+ $problem: -in-problem($in);
405
+ @if $problem {
406
+ @error "`#{meta.inspect($in)}` is not a valid $in for `#{$member}`: #{$problem}. A browser drops the whole gradient.";
407
+ }
408
+ $prelude: list.append($prelude, string.unquote("in #{$in}"), space);
409
+ }
410
+
411
+ $stops: ();
412
+ @each $item in $colors {
413
+ $stops: list.append($stops, $item, comma);
414
+ }
415
+
416
+ $name: "#{$kind}-gradient";
417
+ @if $repeating {
418
+ $name: "repeating-#{$name}";
419
+ }
420
+ $function: meta.get-function($name, $css: true);
421
+ @if list.length($prelude) > 0 {
422
+ @return meta.call($function, $prelude, $stops...);
423
+ }
424
+ @return meta.call($function, $stops...);
425
+ }