@use 'sass:list'; @use 'sass:map'; @use 'sass:math'; @use 'sass:meta'; @use 'palette'; @use '../density/private/compatibility'; // Whether duplication warnings should be disabled. Warnings enabled by default. $theme-ignore-duplication-warnings: false !default; // Whether density should be generated by default. $_generate-default-density: true !default; // Warning that will be printed if duplicated styles are generated by a theme. $_duplicate-warning: 'Read more about how style duplication can be avoided in a dedicated ' + 'guide. https://github.com/angular/components/blob/main/guides/duplicate-theming-styles.md'; // Warning that will be printed if the legacy theming API is used. $_legacy-theme-warning: 'Angular Material themes should be created from a map containing the ' + 'keys "color", "typography", and "density". The color value should be a map containing the ' + 'palette values for "primary", "accent", and "warn". ' + 'See https://material.angular.io/guide/theming for more information.'; // Flag whether theme config getter functions should warn if a key is expected to exist but not // present in the config. This can be transformed internally in Google to ensure all clients have // comprehensive theme configurations. $_enable-strict-theme-config: false; // Flag whether to disable theme definitions copying color values to the top-level theme config. // This copy is to preserve backwards compatibility. $_disable-color-backwards-compatibility: false; // These variable are not intended to be overridden externally. They use `!default` to // avoid being reset every time this file is imported. $_emitted-color: () !default; $_emitted-typography: () !default; $_emitted-density: () !default; /// Extracts a color from a palette or throws an error if it doesn't exist. /// @param {Map} $palette The palette from which to extract a color. /// @param {String | Number} $hue The hue for which to get the color. @function _get-color-from-palette($palette, $hue) { @if map.has-key($palette, $hue) { @return map.get($palette, $hue); } @error 'Hue "' + $hue + '" does not exist in palette. Available hues are: ' + map.keys($palette); } /// For a given hue in a palette, return the contrast color from the map of contrast palettes. /// @param {Map} $palette The palette from which to extract a color. /// @param {String | Number} $hue The hue for which to get a contrast color. /// @returns {Color} The contrast color for the given palette and hue. @function get-contrast-color-from-palette($palette, $hue) { @return map.get(map.get($palette, contrast), $hue); } /// Creates a map of hues to colors for a theme. This is used to define a theme palette in terms /// of the Material Design hues. /// @param {Map} $base-palette Map of hue keys to color values for the basis for this palette. /// @param {String | Number} $default Default hue for this palette. /// @param {String | Number} $lighter "lighter" hue for this palette. /// @param {String | Number} $darker "darker" hue for this palette. /// @param {String | Number} $text "text" hue for this palette. /// @returns {Map} A complete Angular Material theming palette. @function define-palette($base-palette, $default: 500, $lighter: 100, $darker: 700, $text: $default) { $result: map.merge($base-palette, ( default: _get-color-from-palette($base-palette, $default), lighter: _get-color-from-palette($base-palette, $lighter), darker: _get-color-from-palette($base-palette, $darker), text: _get-color-from-palette($base-palette, $text), default-contrast: get-contrast-color-from-palette($base-palette, $default), lighter-contrast: get-contrast-color-from-palette($base-palette, $lighter), darker-contrast: get-contrast-color-from-palette($base-palette, $darker) )); // For each hue in the palette, add a "-contrast" color to the map. @each $hue, $color in $base-palette { $result: map.merge($result, ( '#{$hue}-contrast': get-contrast-color-from-palette($base-palette, $hue) )); } @return $result; } /// Gets a color from a theme palette (the output of mat-palette). /// The hue can be one of the standard values (500, A400, etc.), one of the three preconfigured /// hues (default, lighter, darker), or any of the aforementioned suffixed with "-contrast". /// /// @param {Map} $palette The palette from which to extract a color. /// @param {String | Number} $hue The hue from the palette to use. If this is a value between 0 // and 1, it will be treated as opacity. /// @param {Number} $opacity The alpha channel value for the color. /// @returns {Color} The color for the given palette, hue, and opacity. @function get-color-from-palette($palette, $hue: default, $opacity: null) { // If hueKey is a number between zero and one, then it actually contains an // opacity value, so recall this function with the default hue and that given opacity. @if meta.type-of($hue) == number and $hue >= 0 and $hue <= 1 { @return get-color-from-palette($palette, default, $hue); } // We cast the $hue to a string, because some hues starting with a number, like `700-contrast`, // might be inferred as numbers by Sass. Casting them to string fixes the map lookup. $color: if(map.has-key($palette, $hue), map.get($palette, $hue), map.get($palette, $hue + '')); @if (meta.type-of($color) != color) { // If the $color resolved to something different from a color (e.g. a CSS variable), // we can't apply the opacity anyway so we return the value as is, otherwise Sass can // throw an error or output something invalid. @return $color; } @return rgba($color, if($opacity == null, opacity($color), $opacity)); } // Validates the specified theme by ensuring that the optional color config defines // a primary, accent and warn palette. Returns the theme if no failures were found. @function _mat-validate-theme($theme) { @if map.get($theme, color) { $color: map.get($theme, color); @if not map.get($color, primary) { @error 'Theme does not define a valid "primary" palette.'; } @else if not map.get($color, accent) { @error 'Theme does not define a valid "accent" palette.'; } @else if not map.get($color, warn) { @error 'Theme does not define a valid "warn" palette.'; } } @return $theme; } // Creates a light-themed color configuration from the specified // primary, accent and warn palettes. @function _mat-create-light-color-config($primary, $accent, $warn: null) { @return ( primary: $primary, accent: $accent, warn: if($warn != null, $warn, define-palette(palette.$red-palette)), is-dark: false, foreground: palette.$light-theme-foreground-palette, background: palette.$light-theme-background-palette, ); } // Creates a dark-themed color configuration from the specified // primary, accent and warn palettes. @function _mat-create-dark-color-config($primary, $accent, $warn: null) { @return ( primary: $primary, accent: $accent, warn: if($warn != null, $warn, define-palette(palette.$red-palette)), is-dark: true, foreground: palette.$dark-theme-foreground-palette, background: palette.$dark-theme-background-palette, ); } // TODO: Remove legacy API and rename `$primary` below to `$config`. Currently it cannot be renamed // as it would break existing apps that set the parameter by name. /// Creates a container object for a light theme to be given to individual component theme mixins. /// @param {Map} $primary The theme configuration object. /// @returns {Map} A complete Angular Material theme map. @function define-light-theme($primary, $accent: null, $warn: define-palette(palette.$red-palette)) { // This function creates a container object for the individual component theme mixins. Consumers // can construct such an object by calling this function, or by building the object manually. // There are two possible ways to invoke this function in order to create such an object: // // (1) Passing in a map that holds optional configurations for individual parts of the // theming system. For `color` configurations, the function only expects the palettes // for `primary` and `accent` (and optionally `warn`). The function will expand the // shorthand into an actual configuration that can be consumed in `-color` mixins. // (2) Legacy pattern: Passing in the palettes as parameters. This is not as flexible // as passing in a configuration map because only the `color` system can be configured. // // If the legacy pattern is used, we generate a container object only with a light-themed // configuration for the `color` theming part. @if $accent != null { @warn $_legacy-theme-warning; @return private-create-backwards-compatibility-theme(_mat-validate-theme(( _is-legacy-theme: true, color: _mat-create-light-color-config($primary, $accent, $warn), ))); } // If the map pattern is used (1), we just pass-through the configurations for individual // parts of the theming system, but update the `color` configuration if set. As explained // above, the color shorthand will be expanded to an actual light-themed color configuration. $result: $primary; @if map.get($primary, color) { $color-settings: map.get($primary, color); $primary: map.get($color-settings, primary); $accent: map.get($color-settings, accent); $warn: map.get($color-settings, warn); $result: map.merge($result, (color: _mat-create-light-color-config($primary, $accent, $warn))); } @return private-create-backwards-compatibility-theme(_mat-validate-theme($result)); } // TODO: Remove legacy API and rename below `$primary` to `$config`. Currently it cannot be renamed // as it would break existing apps that set the parameter by name. /// Creates a container object for a dark theme to be given to individual component theme mixins. /// @param {Map} $primary The theme configuration object. /// @returns {Map} A complete Angular Material theme map. @function define-dark-theme($primary, $accent: null, $warn: define-palette(palette.$red-palette)) { // This function creates a container object for the individual component theme mixins. Consumers // can construct such an object by calling this function, or by building the object manually. // There are two possible ways to invoke this function in order to create such an object: // // (1) Passing in a map that holds optional configurations for individual parts of the // theming system. For `color` configurations, the function only expects the palettes // for `primary` and `accent` (and optionally `warn`). The function will expand the // shorthand into an actual configuration that can be consumed in `-color` mixins. // (2) Legacy pattern: Passing in the palettes as parameters. This is not as flexible // as passing in a configuration map because only the `color` system can be configured. // // If the legacy pattern is used, we generate a container object only with a dark-themed // configuration for the `color` theming part. @if $accent != null { @warn $_legacy-theme-warning; @return private-create-backwards-compatibility-theme(_mat-validate-theme(( _is-legacy-theme: true, color: _mat-create-dark-color-config($primary, $accent, $warn), ))); } // If the map pattern is used (1), we just pass-through the configurations for individual // parts of the theming system, but update the `color` configuration if set. As explained // above, the color shorthand will be expanded to an actual dark-themed color configuration. $result: $primary; @if map.get($primary, color) { $color-settings: map.get($primary, color); $primary: map.get($color-settings, primary); $accent: map.get($color-settings, accent); $warn: map.get($color-settings, warn); $result: map.merge($result, (color: _mat-create-dark-color-config($primary, $accent, $warn))); } @return private-create-backwards-compatibility-theme(_mat-validate-theme($result)); } /// Gets the color configuration from the given theme or configuration. /// @param {Map} $theme The theme map returned from `define-light-theme` or `define-dark-theme`. /// @param {Map} $default The default value returned if the given `$theme` does not include a /// `color` configuration. /// @returns {Map} Color configuration for a theme. @function get-color-config($theme, $default: null) { // If a configuration has been passed, return the config directly. @if not private-is-theme-object($theme) { @return $theme; } // If the theme has been constructed through the legacy theming API, we use the theme object // as color configuration instead of the dedicated `color` property. We do this because for // backwards compatibility, we copied the color configuration from `$theme.color` to `$theme`. // Hence developers could customize the colors at top-level and want to respect these changes // TODO: Remove when legacy theming API is removed. @if private-is-legacy-constructed-theme($theme) { @return $theme; } @if map.has-key($theme, color) { @return map.get($theme, color); } @else if ($_enable-strict-theme-config) { @error 'Angular Material theme configuration is missing a "color" value'; } @return $default; } /// Gets the density configuration from the given theme or configuration. /// @param {Map} $theme-or-config The theme map returned from `define-light-theme` or /// `define-dark-theme`. /// @param {Map} $default The default value returned if the given `$theme` does not include a /// `density` configuration. /// @returns {Map} Density configuration for a theme. @function get-density-config($theme-or-config, $default: 0) { // If a configuration has been passed, return the config directly. @if not private-is-theme-object($theme-or-config) { @return $theme-or-config; } // In case a theme has been passed, extract the configuration if present, // or fall back to the default density config. @if map.has-key($theme-or-config, density) { @return map.get($theme-or-config, density); } @else if ($_enable-strict-theme-config) { @error 'Angular Material theme configuration is missing a "density" value'; } @return $default; } /// Gets the typography configuration from the given theme or configuration. /// For backwards compatibility, typography is not included by default. /// @param {Map} $theme-or-config The theme map returned from `define-light-theme` or /// `define-dark-theme`. /// @param {Map} $default The default value returned if the given `$theme` does not include a /// `typography` configuration. /// @returns {Map} Typography configuration for a theme. @function get-typography-config($theme-or-config, $default: null) { // If a configuration has been passed, return the config directly. @if not private-is-theme-object($theme-or-config) { @return $theme-or-config; } // In case a theme has been passed, extract the configuration if present, // or fall back to the default typography config. @if (map.has-key($theme-or-config, typography)) { @return map.get($theme-or-config, typography); } @else if ($_enable-strict-theme-config) { @error 'Angular Material theme configuration is missing a "typography" value'; } @return $default; } // // Private APIs // // Checks if configurations that have been declared in the given theme have been generated // before. If so, warnings will be reported. This should notify developers in case duplicate // styles are accidentally generated due to wrong usage of the all-theme mixins. // // Additionally, this mixin controls the default value for the density configuration. By // default, density styles are generated at scale zero. If the same density styles would be // generated a second time though, the default value will change to avoid duplicate styles. // // The mixin keeps track of all configurations in a list that is scoped to the specified // id. This is necessary because a given theme can be passed to multiple disjoint theme mixins // (e.g. `all-component-themes` and `all-legacy-component-themes`) without causing any // style duplication. @mixin private-check-duplicate-theme-styles($theme-or-color-config, $id) { $theme: private-legacy-get-theme($theme-or-color-config); $color-config: get-color-config($theme); $density-config: get-density-config($theme); $typography-config: get-typography-config($theme); // Lists of previous `color`, `density` and `typography` configurations. $previous-color: map.get($_emitted-color, $id) or (); $previous-typography: map.get($_emitted-typography, $id) or (); $previous-density: map.get($_emitted-density, $id) or (); // Whether duplicate legacy density styles would be generated. $duplicate-legacy-density: false; // Check if the color configuration has been generated before. @if $color-config != null { @if list.index($previous-color, $color-config) != null and not $theme-ignore-duplication-warnings { @warn 'The same color styles are generated multiple times. ' + $_duplicate-warning; } $previous-color: list.append($previous-color, $color-config); } // Check if the typography configuration has been generated before. @if $typography-config != null { @if list.index($previous-typography, $typography-config) != null and not $theme-ignore-duplication-warnings { @warn 'The same typography styles are generated multiple times. ' + $_duplicate-warning; } $previous-typography: list.append($previous-typography, $typography-config); } // Check if the density configuration has been generated before. @if $density-config != null { @if list.index($previous-density, $density-config) != null { // Only report a warning if density styles would be duplicated for non-legacy theme // definitions. For legacy themes, we have compatibility logic that avoids duplication // of default density styles. We don't want to report a warning in those cases. @if private-is-legacy-constructed-theme($theme) { $duplicate-legacy-density: true; } @else if not $theme-ignore-duplication-warnings { @warn 'The same density styles are generated multiple times. ' + $_duplicate-warning; } } $previous-density: list.append($previous-density, $density-config); } $_emitted-color: map.merge($_emitted-color, ($id: $previous-color)) !global; $_emitted-density: map.merge($_emitted-density, ($id: $previous-density)) !global; $_emitted-typography: map.merge($_emitted-typography, ($id: $previous-typography)) !global; // Optionally, consumers of this mixin can wrap contents inside so that nested // duplicate style checks do not report another warning. e.g. if developers include // the `all-component-themes` mixin twice, only the top-level duplicate styles check // should report a warning. Not all individual components should report a warning too. $orig-mat-theme-ignore-duplication-warnings: $theme-ignore-duplication-warnings; $theme-ignore-duplication-warnings: true !global; // If duplicate default density styles would be generated for a legacy constructed theme, // we adjust the density generation so that no density styles are generated by default. // If no default density styles have been generated yet, we ensure that the styles // are generated at root. For legacy themes our goal is to generate default density // styles **once** and at root. This matches the old behavior where density styles were // part of the base component styles (that did not use view encapsulation). // TODO: Remove this compatibility logic when the legacy theming API is removed. compatibility.$private-density-generate-at-root: private-is-legacy-constructed-theme($theme); compatibility.$private-density-generate-styles: not $duplicate-legacy-density; @content; $theme-ignore-duplication-warnings: $orig-mat-theme-ignore-duplication-warnings !global; compatibility.$private-density-generate-at-root: false; compatibility.$private-density-generate-styles: true; } // Checks whether the given value resolves to a theme object. Theme objects are always // of type `map` and can optionally only specify `color`, `density` or `typography`. @function private-is-theme-object($value) { @return meta.type-of($value) == 'map' and ( map.has-key($value, color) or map.has-key($value, density) or map.has-key($value, typography) or list.length($value) == 0 ); } // Checks whether a given value corresponds to a legacy constructed theme. @function private-is-legacy-constructed-theme($value) { @return meta.type-of($value) == 'map' and map.get($value, '_is-legacy-theme'); } // Creates a backwards compatible theme. Previously in Angular Material, theme objects // contained the color configuration directly. With the recent refactoring of the theming // system to allow for density and typography configurations, this is no longer the case. // To ensure that constructed themes which will be passed to custom theme mixins do not break, // we copy the color configuration and put its properties at the top-level of the theme object. // Here is an example of a pattern that should still work until it's officially marked as a // breaking change: // // @mixin my-custom-component-theme($theme) { // .my-comp { // background-color: mat.get-color-from-palette(map.get($theme, primary)); // } // } // // Note that the `$theme.primary` key does usually not exist since the color configuration // is stored in `$theme.color` which contains a property for `primary`. This method copies // the map from `$theme.color` to `$theme` for backwards compatibility. @function private-create-backwards-compatibility-theme($theme) { @if ($_disable-color-backwards-compatibility or not map.get($theme, color)) { @return $theme; } $color: map.get($theme, color); @return map.merge($theme, $color); } // Gets the theme from the given value that is either already a theme, or a color configuration. // This handles the legacy case where developers pass a color configuration directly to the // theme mixin. Before we introduced the new pattern for constructing a theme, developers passed // the color configuration directly to the theme mixins. This can be still the case if developers // construct a theme manually and pass it to a theme. We support this for backwards compatibility. // TODO(devversion): remove this in the future. Constructing themes manually is rare, // and the code can be easily updated to the new API. @function private-legacy-get-theme($theme-or-color-config) { @if private-is-theme-object($theme-or-color-config) { @return $theme-or-color-config; } @warn $_legacy-theme-warning; @return private-create-backwards-compatibility-theme(( _is-legacy-theme: true, color: $theme-or-color-config )); } // Approximates an rgba color into a solid hex color, given a background color. @function private-rgba-to-hex($color, $background-color) { // We convert the rgba color into a solid one by taking the opacity from the rgba // value and using it to determine the percentage of the background to put // into foreground when mixing the colors together. @return mix($background-color, rgba($color, 1), (1 - opacity($color)) * 100%); } // Clamps the density scale to a number between the given min and max. // 'minimum' and 'maximum' are converted to the given min or max number respectively. @function clamp-density($density-scale, $min, $max: 0) { @if $density-scale == minimum { @return $min; } @if $density-scale == maximum { @return $max; } @if meta.type-of($density-scale) != 'number' or not math.is-unitless($density-scale) { @return 0; } @if $density-scale < $min { @return $min; } @if $density-scale > $max { @return $max; } @return $density-scale; }