_ag-theme-params.scss 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. // Utilities to parse params supplied as a map. Values can be defined in terms of
  2. // other values, with modifications. For example:
  3. //
  4. // @include ag-register-params((
  5. // a: ag-derived(b, $times: c, $plus: 2),
  6. // b: 4,
  7. // c: 10
  8. // ));
  9. // @debug ag-param(a); // outputs 42
  10. // Define a derived parameter. Derived values are lazily evaluated. This function is
  11. // sugar for defining a data structure to record the derived value's parameters.
  12. @function ag-derived(
  13. $reference-name,
  14. $times: null,
  15. $divide: null,
  16. $plus: null,
  17. $minus: null,
  18. $opacity: null,
  19. $lighten: null,
  20. $darken: null,
  21. $mix: null,
  22. $self-overlay: null
  23. ) {
  24. $derived: (
  25. "--ag-is-derived-value": true,
  26. "reference-name": $reference-name
  27. );
  28. @if $times != null {
  29. $derived: map-merge($derived, ("times": $times));
  30. }
  31. @if $divide != null {
  32. $derived: map-merge($derived, ("divide": $divide));
  33. }
  34. @if $plus != null {
  35. $derived: map-merge($derived, ("plus": $plus));
  36. }
  37. @if $minus != null {
  38. $derived: map-merge($derived, ("minus": $minus));
  39. }
  40. @if $opacity != null {
  41. $derived: map-merge($derived, ("opacity": $opacity));
  42. }
  43. @if $lighten != null {
  44. $derived: map-merge($derived, ("lighten": $lighten));
  45. }
  46. @if $darken != null {
  47. $derived: map-merge($derived, ("darken": $darken));
  48. }
  49. @if $mix != null {
  50. $derived: map-merge($derived, ("mix": $mix));
  51. }
  52. @if $self-overlay != null {
  53. $derived: map-merge($derived, ("self-overlay": $self-overlay));
  54. }
  55. @return $derived;
  56. }
  57. // Use a parameter in SCSS, e.g. `color: ag-param(foreground-color)`
  58. // Note, it is not possible to use this for color params, use the ag-color-property mixin instead
  59. @function ag-param($name, $caller: null) {
  60. @if $-ag-allow-color-param-access-with-ag-param != true and str-index($name, "-color") and $caller != "permitted internal _ag-theme-params.scss access" {
  61. @error "Illegal call to ag-param(#{$name}) - all colour params must be accessed through the ag-color-property mixin.";
  62. }
  63. $resolved: -ag-param-unchecked($name);
  64. @if str-index(inspect($resolved), "ag-derived(") != null {
  65. @error "#{$name} param contains a ag-derived() as a CSS function call expression. This means that you have tried to use ag-derived() before the function is defined - you need to include the file that defines it.";
  66. }
  67. @if type-of($resolved) == map {
  68. @error "ag-param(#{$name}) resolved to a map, which is not valid CSS: #{inspect($resolved)}";
  69. }
  70. @each $part in $resolved {
  71. @if type-of($part) == map {
  72. @error "ag-param(#{$name}) resolved to a list containing a map, which is not valid CSS: #{str-slice(inspect($resolved), 0, 1000)}";
  73. }
  74. }
  75. @return $resolved;
  76. }
  77. // Return true if a param has a value other than null or false
  78. @function ag-param-is-set($name) {
  79. $value: -ag-param-unchecked($name);
  80. @return $value != null and $value != false;
  81. }
  82. // Return true if two params have different values
  83. @function ag-params-are-different($name-a, $name-b) {
  84. @return -ag-param-unchecked($name-a) != -ag-param-unchecked($name-b);
  85. }
  86. // A mixin to apply a color to an element. This sets the value of a CSS property using a
  87. // theme parameter, and also emits CSS that allows the value to be overridden at runtime
  88. // using CSS variables. If the mixin is called like this:
  89. //
  90. // @include ag-color-property(background-color, header-background-color)
  91. //
  92. // ... and the header-background-color parameter is set to `red` then the emitted CSS will
  93. // be something like:
  94. //
  95. // background-color: red;
  96. // background-color: var(--ag-header-background-color, red);
  97. //
  98. // The optional $important argument can be used to add a CSS !important directive
  99. @mixin ag-color-property($property, $param, $important: false) {
  100. $value: ag-param($param, $caller: "permitted internal _ag-theme-params.scss access");
  101. $important: if($important, !important, null);
  102. @if $value != null {
  103. #{$property}: $value $important;
  104. }
  105. @if not ag-param-is-set(suppress-css-var-overrides) {
  106. $value-as-css-var: -ag-param-as-css-var($param);
  107. @if $value != $value-as-css-var {
  108. #{$property}: $value-as-css-var $important;
  109. }
  110. }
  111. }
  112. $-ag-allow-color-param-access-with-ag-param: true;
  113. @mixin ag-allow-color-param-access-with-ag-param($allow) {
  114. $-ag-allow-color-param-access-with-ag-param: $allow !global;
  115. }
  116. // Merge params supplied to a theme with the defaults, optionally validate, and register
  117. // the resulting map globally for use with ag-param()
  118. //
  119. // $params: params supplied by the derived theme
  120. // $defaults: values for params not in $params
  121. @function ag-process-theme-variables($params, $defaults) {
  122. $params: -ag-require-type($params, map, "$params argument to ag-process-theme-variables");
  123. // Derived themes can add params, and those new params would trigger validation errors when
  124. // passed to the base theme, so don't re-validate params that have already been validated
  125. @if not map-has-key($params, "--ag-already-validated") {
  126. @each $key in map-keys($params) {
  127. @if not map-has-key($defaults, $key) and str-index($key, "--internal-") != 1 {
  128. @warn "Unrecognised param \"#{$key}\"";
  129. }
  130. }
  131. }
  132. @if map-get($params, "icons-font-codes") and map-get($defaults, "icons-font-codes") {
  133. $merged-codes: map-merge(map-get($defaults, "icons-font-codes"), map-get($params, "icons-font-codes"));
  134. $params: map-merge($params, ("icons-font-codes": $merged-codes));
  135. }
  136. $params: map-merge($defaults, $params);
  137. $params: map-merge($params, ("--ag-already-validated": true));
  138. $-ag-params: $params !global;
  139. @return $params;
  140. }
  141. // global map of params used by ag-param()
  142. $-ag-params: null !default;
  143. // Register a params map globally so that it can be used by ag-param($name)
  144. // NOTE: Custom themes should NOT use this, use ag-process-theme-variables() instead
  145. @mixin ag-register-params($params) {
  146. $params: -ag-require-type($params, "map", "$params argument");
  147. $-ag-params: $params !global;
  148. }
  149. //
  150. // PRIVATE IMPLEMENTATION FUNCTIONS
  151. //
  152. // Return a parameter value as a CSS variable declaration
  153. @function -ag-param-as-css-var($name) {
  154. $value: map-get($-ag-params, $name);
  155. @if -is-ag-derived($value) {
  156. $has-modificatons: length($value) > 2;
  157. @if $has-modificatons {
  158. $value: ag-param($name, $caller: "permitted internal _ag-theme-params.scss access");
  159. }
  160. @else {
  161. $reference-name: map-get($value, "reference-name");
  162. $value: -ag-param-as-css-var($reference-name);
  163. }
  164. }
  165. @if $value == null {
  166. @return var(--ag-#{$name});
  167. }
  168. @return var(--ag-#{$name}, #{$value});
  169. }
  170. // Get a parameter, with no checks other than that the parameter exists
  171. @function -ag-param-unchecked($name) {
  172. @if $-ag-params == null {
  173. @error "ag-param() called before ag-register-params";
  174. }
  175. @if str-index($name, "--internal-") == 1 {
  176. // internal vars are returned without ag-derived resolution or validation that the var exists
  177. @return map-get($-ag-params, $name);
  178. }
  179. @if not map-has-key($-ag-params, $name) {
  180. @error "ag-param(#{$name}): no such parameter";
  181. }
  182. @return -ag-resolve-param-name($-ag-params, $name);
  183. }
  184. // Return true if a value is a record returned by ag-derived()
  185. @function -is-ag-derived($value) {
  186. @return type-of($value) == map and map-get($value, "--ag-is-derived-value") == true;
  187. }
  188. @function -ag-resolve-param-name($params, $name) {
  189. $value: map-get($params, $name);
  190. @return -ag-resolve-param-value($params, $value, $name);
  191. }
  192. @function -ag-resolve-param-value($params, $input-value, $context-name) {
  193. @if type-of($input-value) == list {
  194. $resolved: $input-value;
  195. @for $i from 1 through length($input-value) {
  196. $resolved: set-nth($resolved, $i, -ag-resolve-param-value($params, nth($resolved, $i), $context-name));
  197. }
  198. @return $resolved;
  199. }
  200. @if not -is-ag-derived($input-value) {
  201. @return $input-value;
  202. }
  203. $derived: $input-value;
  204. $reference-name: map-get($derived, "reference-name");
  205. @if not map-has-key($params, $reference-name) {
  206. @error "ag-derived: no such parameter \"#{$reference-name}\"";
  207. }
  208. $resolved: map-get($params, $reference-name);
  209. $resolved: -ag-resolve-param-value($params, $resolved, $reference-name);
  210. $resolved: -ag-apply-derived-operator($params, $resolved, $derived, "times", $context-name);
  211. $resolved: -ag-apply-derived-operator($params, $resolved, $derived, "divide", $context-name);
  212. $resolved: -ag-apply-derived-operator($params, $resolved, $derived, "plus", $context-name);
  213. $resolved: -ag-apply-derived-operator($params, $resolved, $derived, "minus", $context-name);
  214. $resolved: -ag-apply-derived-operator($params, $resolved, $derived, "opacity", $context-name);
  215. $resolved: -ag-apply-derived-operator($params, $resolved, $derived, "mix", $context-name);
  216. $resolved: -ag-apply-derived-operator($params, $resolved, $derived, "lighten", $context-name);
  217. $resolved: -ag-apply-derived-operator($params, $resolved, $derived, "darken", $context-name);
  218. $resolved: -ag-apply-derived-operator($params, $resolved, $derived, "self-overlay", $context-name);
  219. @return -ag-resolve-param-value($params, $resolved, $reference-name);
  220. }
  221. @function -ag-apply-derived-operator($params, $lhs, $derived, $operator, $context-name) {
  222. @if $lhs == null {
  223. @return $lhs;
  224. }
  225. $rhs: map-get($derived, $operator);
  226. @if $rhs == null {
  227. @return $lhs;
  228. }
  229. @if -ag-is-css-var-token($lhs) {
  230. $reference-name: map-get($derived, "reference-name");
  231. @warn "Problem while calculating theme parameter `#{$context-name}: #{-ag-inspect-derived-value($derived)}`. This rule attempts to modify the color of `#{$reference-name}` using $#{$operator}, but (#{$reference-name}: #{$lhs}) is a CSS variable and can't be modified at compile time. Either set `#{$reference-name}` to a CSS color value (e.g. #ffffff) or provide a value for `#{$context-name}` that does not use $#{$operator}";
  232. @return null;
  233. }
  234. @if type-of($rhs) == string {
  235. $rhs: -ag-resolve-param-name($params, $rhs);
  236. }
  237. $operator-function: "-ag-operator-function-#{$operator}";
  238. @if not function-exists($operator-function) {
  239. @error "No such function #{$operator-function}";
  240. }
  241. @return call(get-function($operator-function), $params, $lhs, $rhs);
  242. }
  243. // return a string representation of an ag-derived value for debugging
  244. @function -ag-inspect-derived-value($derived) {
  245. @return "ag-derived("
  246. + map-get($derived, "reference-name")
  247. + if(map-get($derived, "times"), ", $times: #{map-get($derived, "times")}", "")
  248. + if(map-get($derived, "divide"), ", $divide: #{map-get($derived, "divide")}", "")
  249. + if(map-get($derived, "plus"), ", $plus: #{map-get($derived, "plus")}", "")
  250. + if(map-get($derived, "minus"), ", $minus: #{map-get($derived, "minus")}", "")
  251. + if(map-get($derived, "opacity"), ", $opacity: #{map-get($derived, "opacity")}", "")
  252. + if(map-get($derived, "mix"), ", $mix: #{map-get($derived, "mix")}", "")
  253. + if(map-get($derived, "lighten"), ", $lighten: #{map-get($derived, "lighten")}", "")
  254. + if(map-get($derived, "darken"), ", $darken: #{map-get($derived, "darken")}", "")
  255. + if(map-get($derived, "self-overlay"), ", $self-overlay: #{map-get($derived, "self-overlay")}", "")
  256. + ")";
  257. }
  258. @function -ag-is-css-var-token($value) {
  259. @return type-of($value) == string and str-index($value, "var(") != null
  260. }
  261. @function -ag-require-type($value, $expected, $context) {
  262. @if type-of($value) == $expected or ($expected == "map" and $value == ()) {
  263. @return $value;
  264. }
  265. @error "Expected #{$context} to be a #{$expected} but got a #{type-of($value)} instead (#{inspect($value)})";
  266. }
  267. @function -ag-operator-function-times($params, $lhs, $rhs) {
  268. $lhs: -ag-require-type($lhs, "number", "value before $times");
  269. $rhs: -ag-require-type($rhs, "number", "argument to $times");
  270. @return $lhs * $rhs;
  271. }
  272. @function -ag-operator-function-divide($params, $lhs, $rhs) {
  273. $lhs: -ag-require-type($lhs, "number", "value before $divide");
  274. $rhs: -ag-require-type($rhs, "number", "argument to $divide");
  275. @return $lhs / $rhs;
  276. }
  277. @function -ag-operator-function-plus($params, $lhs, $rhs) {
  278. $lhs: -ag-require-type($lhs, "number", "value before $plus");
  279. $rhs: -ag-require-type($rhs, "number", "argument to $plus");
  280. @return $lhs + $rhs;
  281. }
  282. @function -ag-operator-function-minus($params, $lhs, $rhs) {
  283. $lhs: -ag-require-type($lhs, "number", "value before $minus");
  284. $rhs: -ag-require-type($rhs, "number", "argument to $minus");
  285. @return $lhs - $rhs;
  286. }
  287. @function -ag-operator-function-opacity($params, $lhs, $rhs) {
  288. $lhs: -ag-require-type($lhs, "color", "value before $opacity");
  289. $rhs: -ag-require-type($rhs, "number", "argument to $opacity");
  290. @if $rhs < 0 or $rhs > 1 {
  291. @error "Expected argument to $opacity to be between 0 and 1, got #{inspect($rhs)} instead.";
  292. }
  293. @return rgba($lhs, $rhs);
  294. }
  295. @function -ag-operator-function-mix($params, $lhs, $rhs) {
  296. $lhs: -ag-require-type($lhs, "color", "value before $mix");
  297. @if length($rhs) != 2 {
  298. @error "Expected argument to $mix to be a 2-item array [color, percentage] but got #{inspect($rhs)}";
  299. }
  300. $color: nth($rhs, 1);
  301. @if type-of($color) == string {
  302. $color: -ag-resolve-param-name($params, $color);
  303. }
  304. $percentage: nth($rhs, 2);
  305. @if type-of($color) != color or type-of($percentage) != number {
  306. @error "Expected argument to $mix to be a 2-item array [color, number] but got [#{type-of($color)}, #{type-of($percentage)}]: #{inspect($rhs)}";
  307. }
  308. @return mix($color, $lhs, $percentage);
  309. }
  310. @function -ag-operator-function-lighten($params, $lhs, $rhs) {
  311. $lhs: -ag-require-type($lhs, "color", "value before $lighten");
  312. $rhs: -ag-require-type($rhs, "number", "argument to $lighten");
  313. @if $rhs < 0 or $rhs > 100 {
  314. @error "Expected argument to $lighten to be between 0 and 100, got #{inspect($rhs)} instead.";
  315. }
  316. @return lighten($lhs, $rhs);
  317. }
  318. @function -ag-operator-function-darken($params, $lhs, $rhs) {
  319. $lhs: -ag-require-type($lhs, "color", "value before $darken");
  320. $rhs: -ag-require-type($rhs, "number", "argument to $darken");
  321. @if $rhs < 0 or $rhs > 100 {
  322. @error "Expected argument to $darken to be between 0 and 100, got #{inspect($rhs)} instead.";
  323. }
  324. @return darken($lhs, $rhs);
  325. }
  326. @function -ag-operator-function-self-overlay($params, $color, $times) {
  327. $color: -ag-require-type($color, "color", "value before $self-overlay");
  328. $times: -ag-require-type($times, "number", "argument to $self-overlay");
  329. @if $times < 0 or $times > 100 {
  330. @error "Expected argument to $self-overlay to be between 0 and 100, got #{inspect($times)} instead.";
  331. }
  332. $solidity: 1 - opacity($color);
  333. $output-solidity: 1;
  334. @if $times > 0 {
  335. @for $i from 1 through $times {
  336. $output-solidity: $output-solidity * $solidity;
  337. }
  338. }
  339. @return rgba($color, 1 - $output-solidity);
  340. }