@use '../style/elevation'; @use '../style/sass-utils'; @use './m3-system'; @use 'sass:list'; @use 'sass:map'; @use 'sass:string'; $_tokens: null; $_component-prefix: null; $_system-fallbacks: m3-system.create-system-fallbacks(); // Sets the token prefix and map to use when creating token slots. @mixin use-tokens($prefix, $tokens) { $_component-prefix: $prefix !global; $_tokens: $tokens !global; @content; $_component-prefix: null !global; $_tokens: null !global; } // Combines a prefix and a string to generate a CSS variable name for a token. @function _create-var-name($prefix, $token) { @if $prefix == null or $token == null { @error 'Must specify both prefix and name when generating token'; } $string-prefix: ''; // Prefixes are lists so we need to combine them. @each $part in $prefix { $string-prefix: if($string-prefix == '', $part, '#{$string-prefix}-#{$part}'); } @return string.unquote('--#{$string-prefix}-#{$token}'); } // Creates a CSS variable, including the fallback if provided. @function _create-var($name, $fallback: null) { @if ($fallback) { @return var($name, $fallback); } @else { @return var($name); } } // Gets the value of the token given the current global context state. @function _get-token-value($token, $fallback) { $var-name: _create-var-name($_component-prefix, $token); $fallback: _get-token-fallback($token, $fallback); @return _create-var($var-name, $fallback); } // Assertion mixin that throws an error if the global state has not been set up by wrapping // calls with `use-tokens`. @function _assert-use-tokens($token) { @if $_component-prefix == null or $_tokens == null { @error 'Function was not called within a wrapping call of `use-tokens`'; } @if not map.has-key($_tokens, $token) { @error 'Token #{$token} does not exist. Configured tokens are: #{map.keys($_tokens)}'; } @return true; } // Emits a slot for the given token, provided that it has a non-null value in the token map passed // to `use-tokens`. // Accepts an optional fallback parameter to include in the CSS variable. // If $fallback is `true`, then use the tokens map to get the fallback. // TODO: Remove the use case where we accept "true" and handle any failing client screenshots @mixin create-token-slot($property, $token, $fallback: null) { $_assert: _assert-use-tokens($token); @if map.get($_tokens, $token) != null { #{$property}: #{_get-token-value($token, $fallback)}; } } // Returns the name of a token including the current prefix. Intended to be used in calculations // involving tokens. `create-token-slot` should be used when outputting tokens. @function get-token-variable-name($token) { $_assert: _assert-use-tokens($token); @return _create-var-name($_component-prefix, $token); } // Returns a `var()` reference to a specific token. Intended for declarations // where the token has to be referenced as a part of a larger expression. // Accepts an optional fallback parameter to include in the CSS variable. // If $fallback is `true`, then use the tokens map to get the fallback. // TODO: Remove the use case where we accept "true" and handle any failing client screenshots @function get-token-variable($token, $fallback: null) { $_assert: _assert-use-tokens($token); @return _get-token-value($token, $fallback); } // Gets the token's fallback value. Prefers adding a system-level fallback if one exists, otherwise // use the provided fallback. @function _get-token-fallback($token, $fallback: null) { // If the $fallback is `true`, this is the component's signal to use the current token map value @if ($fallback == true) { $fallback: map.get($_tokens, $token); } // Check whether there's a system-level fallback. If not, return the optional // provided fallback (otherwise null). $sys-fallback: map.get($_system-fallbacks, $_component-prefix, $token); @if (not $sys-fallback) { @return $fallback; } @if (sass-utils.is-css-var-name($sys-fallback)) { @return _create-var($sys-fallback, $fallback); } @return $sys-fallback; } // Outputs a map of tokens under a specific prefix. @mixin create-token-values($prefix, $tokens) { @if $tokens != null { // TODO: The `&` adds to the file size of theme, but it's necessary for compatibility // with https://sass-lang.com/documentation/breaking-changes/mixed-decls/. We should // figure out a better way to do this or move all the concrete styles out of the theme. & { @each $key, $value in $tokens { @if $value != null { #{_create-var-name($prefix, $key)}: #{$value}; } } } } } // MDC doesn't currently handle elevation tokens properly. As a temporary workaround we can combine // the elevation and shadow-color tokens into a full box-shadow and use it as the value for the // elevation token. @function resolve-elevation($tokens, $elevation-token, $shadow-color-token) { $elevation: map.get($tokens, $elevation-token); $shadow-color: map.get($tokens, $shadow-color-token); @return map.merge( $tokens, ( $elevation-token: elevation.get-box-shadow($elevation, $shadow-color), $shadow-color-token: null, ) ); } /// Checks whether a list starts wih a given prefix /// @param {List} $list The list value to check the prefix of. /// @param {List} $prefix The prefix to check. /// @return {Boolean} Whether the list starts with the prefix. @function _is-prefix($list, $prefix) { @for $i from 1 through list.length($prefix) { @if list.nth($list, $i) != list.nth($prefix, $i) { @return false; } } @return true; } /// Gets the supported color variants in the given token set for the given prefix. /// @param {Map} $tokens The full token map. /// @param {List} $prefix The component prefix to get color variants for. /// @return {List} The supported color variants. @function _supported-color-variants($tokens, $prefix) { $result: (); @each $namespace in map.keys($tokens) { @if list.length($prefix) == list.length($namespace) - 1 and _is-prefix($namespace, $prefix) { $result: list.append($result, list.nth($namespace, list.length($namespace)), comma); } } @return $result; } /// Gets the token values for the given components prefix with the given options. /// @param {Map} $tokens The full token map. /// @param {List} $prefix The component prefix to get the token values for. /// @param {ArgList} Any additional options /// Currently the additional supported options are: // - $color-variant - The color variant to use for the component // - $emit-overrides-only - Whether to emit *only* the overrides for the // specific color variant, or all color styles. Defaults to false. /// @throws If given options are invalid /// @return {Map} The token values for the requested component. @function get-tokens-for($tokens, $prefix, $options...) { $options: sass-utils.validate-keyword-args($options, (color-variant, emit-overrides-only)); @if $tokens == () { @return (); } $values: map.get($tokens, $prefix); $color-variant: map.get($options, color-variant); $emit-overrides-only: map.get($options, emit-overrides-only); @if $color-variant == null { @return $values; } $overrides: map.get($tokens, list.append($prefix, $color-variant)); @if $overrides == null { $variants: _supported-color-variants($tokens, $prefix); $secondary-message: if( $variants == (), 'Mixin does not support color variants', 'Supported color variants are: #{$variants}' ); @error 'Invalid color variant: #{$color-variant}. #{$secondary-message}.'; } @return if($emit-overrides-only, $overrides, map.merge($values, $overrides)); } /// Emits new token values for the given token overrides. /// Verifies that the overrides passed in are valid tokens. /// New token values are emitted under the current selector or root. @mixin batch-create-token-values($overrides: (), $namespace-configs...) { @include sass-utils.current-selector-or-root() { $prefixed-name-data: (); $unprefixed-name-data: (); $all-names: (); @each $config in $namespace-configs { $namespace: map.get($config, namespace); $prefix: if(map.has-key($config, prefix), map.get($config, prefix), ''); $tokens: _filter-nulls(map.get($config, tokens)); @each $name, $value in $tokens { $prefixed-name: $prefix + $name; $all-names: list.append($all-names, $prefixed-name, $separator: comma); @if map.has-key($prefixed-name-data, $prefixed-name) { @error #{ 'Error overriding token: Ambiguous token name `' }#{ $prefixed-name }#{ '` exists in multiple namespaces: `(' }#{ list.nth(map.get($prefixed-name-data, $prefixed-name), 1) }#{ ')` and `(' }#{ $namespace }#{ ')`' }; } $prefixed-name-data: map.set($prefixed-name-data, $prefixed-name, ($namespace, $name)); $unprefixed-data: map.has-key($unprefixed-name-data, $name) and map.get($unprefixed-name-data, $name) or (); $unprefixed-data: list.append($unprefixed-data, ($namespace, $prefixed-name)); $unprefixed-name-data: map.set($unprefixed-name-data, $name, $unprefixed-data); } } @each $name, $value in $overrides { @if map.has-key($prefixed-name-data, $name) { $data: map.get($prefixed-name-data, $name); $namespace: list.nth($data, 1); $name: list.nth($data, 2); @include create-token-values( $namespace, ( $name: $value, ) ); } @else if (map.has-key($unprefixed-name-data, $name)) { $datalist: map.get($unprefixed-name-data, $name); $prefixed-names: (); @each $data in $datalist { $namespace: list.nth($data, 1); $prefixed-names: list.append($prefixed-names, list.nth($data, 2), $separator: comma); @include create-token-values( $namespace, ( $name: $value, ) ); } @warn #{ 'Token `' }#{ $name }#{ '` is deprecated. Please use one of the following alternatives: ' }#{ $prefixed-names }; } @else { @error #{'Invalid token name `'}#{$name}#{'`. '}#{'Valid tokens are: '}#{$all-names}; } } } } /// Filters keys with a null value out of the map. /// @param {Map} $map The map to filter. /// @return {Map} The given map with all of the null keys filtered out. @function _filter-nulls($map) { $result: (); @each $key, $val in $map { @if $val != null { $result: map.set($result, $key, $val); } } @return $result; }