|
| 1 | +@use 'sass:list'; |
| 2 | +@use 'sass:map'; |
| 3 | +@use 'sass:meta'; |
| 4 | +@use '../theming/theming'; |
| 5 | +@use './palette'; |
| 6 | + |
| 7 | +/// Extracts a color from a palette or throws an error if it doesn't exist. |
| 8 | +/// @param {Map} $palette The palette from which to extract a color. |
| 9 | +/// @param {String | Number} $hue The hue for which to get the color. |
| 10 | +@function _get-color-from-palette($palette, $hue) { |
| 11 | + @if map.has-key($palette, $hue) { |
| 12 | + @return map.get($palette, $hue); |
| 13 | + } |
| 14 | + |
| 15 | + @error 'Hue "' + $hue + '" does not exist in palette. Available hues are: ' + map.keys($palette); |
| 16 | +} |
| 17 | + |
| 18 | +/// For a given hue in a palette, return the contrast color from the map of contrast palettes. |
| 19 | +/// @param {Map} $palette The palette from which to extract a color. |
| 20 | +/// @param {String | Number} $hue The hue for which to get a contrast color. |
| 21 | +/// @returns {Color} The contrast color for the given palette and hue. |
| 22 | +@function get-contrast-color-from-palette($palette, $hue) { |
| 23 | + @return map.get(map.get($palette, contrast), $hue); |
| 24 | +} |
| 25 | + |
| 26 | + |
| 27 | +/// Creates a map of hues to colors for a theme. This is used to define a theme palette in terms |
| 28 | +/// of the Material Design hues. |
| 29 | +/// @param {Map} $base-palette Map of hue keys to color values for the basis for this palette. |
| 30 | +/// @param {String | Number} $default Default hue for this palette. |
| 31 | +/// @param {String | Number} $lighter "lighter" hue for this palette. |
| 32 | +/// @param {String | Number} $darker "darker" hue for this palette. |
| 33 | +/// @param {String | Number} $text "text" hue for this palette. |
| 34 | +/// @returns {Map} A complete Angular Material theming palette. |
| 35 | +@function define-palette($base-palette, $default: 500, $lighter: 100, $darker: 700, |
| 36 | + $text: $default) { |
| 37 | + $result: map.merge($base-palette, ( |
| 38 | + default: _get-color-from-palette($base-palette, $default), |
| 39 | + lighter: _get-color-from-palette($base-palette, $lighter), |
| 40 | + darker: _get-color-from-palette($base-palette, $darker), |
| 41 | + text: _get-color-from-palette($base-palette, $text), |
| 42 | + default-contrast: get-contrast-color-from-palette($base-palette, $default), |
| 43 | + lighter-contrast: get-contrast-color-from-palette($base-palette, $lighter), |
| 44 | + darker-contrast: get-contrast-color-from-palette($base-palette, $darker) |
| 45 | + )); |
| 46 | + |
| 47 | + // For each hue in the palette, add a "-contrast" color to the map. |
| 48 | + @each $hue, $color in $base-palette { |
| 49 | + $result: map.merge($result, ( |
| 50 | + '#{$hue}-contrast': get-contrast-color-from-palette($base-palette, $hue) |
| 51 | + )); |
| 52 | + } |
| 53 | + |
| 54 | + @return $result; |
| 55 | +} |
| 56 | + |
| 57 | + |
| 58 | +/// Gets a color from a theme palette (the output of mat-palette). |
| 59 | +/// The hue can be one of the standard values (500, A400, etc.), one of the three preconfigured |
| 60 | +/// hues (default, lighter, darker), or any of the aforementioned suffixed with "-contrast". |
| 61 | +/// |
| 62 | +/// @param {Map} $palette The palette from which to extract a color. |
| 63 | +/// @param {String | Number} $hue The hue from the palette to use. If this is a value between 0 |
| 64 | +// and 1, it will be treated as opacity. |
| 65 | +/// @param {Number} $opacity The alpha channel value for the color. |
| 66 | +/// @returns {Color} The color for the given palette, hue, and opacity. |
| 67 | +@function get-color-from-palette($palette, $hue: default, $opacity: null) { |
| 68 | + // If hueKey is a number between zero and one, then it actually contains an |
| 69 | + // opacity value, so recall this function with the default hue and that given opacity. |
| 70 | + @if meta.type-of($hue) == number and $hue >= 0 and $hue <= 1 { |
| 71 | + @return get-color-from-palette($palette, default, $hue); |
| 72 | + } |
| 73 | + |
| 74 | + // We cast the $hue to a string, because some hues starting with a number, like `700-contrast`, |
| 75 | + // might be inferred as numbers by Sass. Casting them to string fixes the map lookup. |
| 76 | + $color: if(map.has-key($palette, $hue), map.get($palette, $hue), map.get($palette, $hue + '')); |
| 77 | + |
| 78 | + @if (meta.type-of($color) != color) { |
| 79 | + // If the $color resolved to something different from a color (e.g. a CSS variable), |
| 80 | + // we can't apply the opacity anyway so we return the value as is, otherwise Sass can |
| 81 | + // throw an error or output something invalid. |
| 82 | + @return $color; |
| 83 | + } |
| 84 | + |
| 85 | + @return rgba($color, if($opacity == null, opacity($color), $opacity)); |
| 86 | +} |
| 87 | + |
| 88 | +// Validates the specified theme by ensuring that the optional color config defines |
| 89 | +// a primary, accent and warn palette. Returns the theme if no failures were found. |
| 90 | +@function _mat-validate-theme($theme) { |
| 91 | + @if map.get($theme, color) { |
| 92 | + $color: map.get($theme, color); |
| 93 | + @if not map.get($color, primary) { |
| 94 | + @error 'Theme does not define a valid "primary" palette.'; |
| 95 | + } |
| 96 | + @else if not map.get($color, accent) { |
| 97 | + @error 'Theme does not define a valid "accent" palette.'; |
| 98 | + } |
| 99 | + @else if not map.get($color, warn) { |
| 100 | + @error 'Theme does not define a valid "warn" palette.'; |
| 101 | + } |
| 102 | + } |
| 103 | + @return $theme; |
| 104 | +} |
| 105 | + |
| 106 | +// Creates a light-themed color configuration from the specified |
| 107 | +// primary, accent and warn palettes. |
| 108 | +@function _mat-create-light-color-config($primary, $accent, $warn: null) { |
| 109 | + @return ( |
| 110 | + primary: $primary, |
| 111 | + accent: $accent, |
| 112 | + warn: if($warn != null, $warn, define-palette(palette.$red-palette)), |
| 113 | + is-dark: false, |
| 114 | + foreground: palette.$light-theme-foreground-palette, |
| 115 | + background: palette.$light-theme-background-palette, |
| 116 | + ); |
| 117 | +} |
| 118 | + |
| 119 | +// Creates a dark-themed color configuration from the specified |
| 120 | +// primary, accent and warn palettes. |
| 121 | +@function _mat-create-dark-color-config($primary, $accent, $warn: null) { |
| 122 | + @return ( |
| 123 | + primary: $primary, |
| 124 | + accent: $accent, |
| 125 | + warn: if($warn != null, $warn, define-palette(palette.$red-palette)), |
| 126 | + is-dark: true, |
| 127 | + foreground: palette.$dark-theme-foreground-palette, |
| 128 | + background: palette.$dark-theme-background-palette, |
| 129 | + ); |
| 130 | +} |
| 131 | + |
| 132 | +// TODO: Remove legacy API and rename `$primary` below to `$config`. Currently it cannot be renamed |
| 133 | +// as it would break existing apps that set the parameter by name. |
| 134 | + |
| 135 | +/// Creates a container object for a light theme to be given to individual component theme mixins. |
| 136 | +/// @param {Map} $primary The theme configuration object. |
| 137 | +/// @returns {Map} A complete Angular Material theme map. |
| 138 | +@function define-light-theme($primary, $accent: null, $warn: define-palette(palette.$red-palette)) { |
| 139 | + // This function creates a container object for the individual component theme mixins. Consumers |
| 140 | + // can construct such an object by calling this function, or by building the object manually. |
| 141 | + // There are two possible ways to invoke this function in order to create such an object: |
| 142 | + // |
| 143 | + // (1) Passing in a map that holds optional configurations for individual parts of the |
| 144 | + // theming system. For `color` configurations, the function only expects the palettes |
| 145 | + // for `primary` and `accent` (and optionally `warn`). The function will expand the |
| 146 | + // shorthand into an actual configuration that can be consumed in `-color` mixins. |
| 147 | + // (2) Legacy pattern: Passing in the palettes as parameters. This is not as flexible |
| 148 | + // as passing in a configuration map because only the `color` system can be configured. |
| 149 | + // |
| 150 | + // If the legacy pattern is used, we generate a container object only with a light-themed |
| 151 | + // configuration for the `color` theming part. |
| 152 | + @if $accent != null { |
| 153 | + @warn theming.$private-legacy-theme-warning; |
| 154 | + $theme: _mat-validate-theme(( |
| 155 | + _is-legacy-theme: true, |
| 156 | + color: _mat-create-light-color-config($primary, $accent, $warn), |
| 157 | + )); |
| 158 | + |
| 159 | + @return _internalize-theme(theming.private-create-backwards-compatibility-theme($theme)); |
| 160 | + } |
| 161 | + // If the map pattern is used (1), we just pass-through the configurations for individual |
| 162 | + // parts of the theming system, but update the `color` configuration if set. As explained |
| 163 | + // above, the color shorthand will be expanded to an actual light-themed color configuration. |
| 164 | + $result: $primary; |
| 165 | + @if map.get($primary, color) { |
| 166 | + $color-settings: map.get($primary, color); |
| 167 | + $primary: map.get($color-settings, primary); |
| 168 | + $accent: map.get($color-settings, accent); |
| 169 | + $warn: map.get($color-settings, warn); |
| 170 | + $result: map.merge($result, (color: _mat-create-light-color-config($primary, $accent, $warn))); |
| 171 | + } |
| 172 | + @return _internalize-theme( |
| 173 | + theming.private-create-backwards-compatibility-theme(_mat-validate-theme($result))); |
| 174 | +} |
| 175 | + |
| 176 | +// TODO: Remove legacy API and rename below `$primary` to `$config`. Currently it cannot be renamed |
| 177 | +// as it would break existing apps that set the parameter by name. |
| 178 | + |
| 179 | +/// Creates a container object for a dark theme to be given to individual component theme mixins. |
| 180 | +/// @param {Map} $primary The theme configuration object. |
| 181 | +/// @returns {Map} A complete Angular Material theme map. |
| 182 | +@function define-dark-theme($primary, $accent: null, $warn: define-palette(palette.$red-palette)) { |
| 183 | + // This function creates a container object for the individual component theme mixins. Consumers |
| 184 | + // can construct such an object by calling this function, or by building the object manually. |
| 185 | + // There are two possible ways to invoke this function in order to create such an object: |
| 186 | + // |
| 187 | + // (1) Passing in a map that holds optional configurations for individual parts of the |
| 188 | + // theming system. For `color` configurations, the function only expects the palettes |
| 189 | + // for `primary` and `accent` (and optionally `warn`). The function will expand the |
| 190 | + // shorthand into an actual configuration that can be consumed in `-color` mixins. |
| 191 | + // (2) Legacy pattern: Passing in the palettes as parameters. This is not as flexible |
| 192 | + // as passing in a configuration map because only the `color` system can be configured. |
| 193 | + // |
| 194 | + // If the legacy pattern is used, we generate a container object only with a dark-themed |
| 195 | + // configuration for the `color` theming part. |
| 196 | + @if $accent != null { |
| 197 | + @warn theming.$private-legacy-theme-warning; |
| 198 | + $theme: _mat-validate-theme(( |
| 199 | + _is-legacy-theme: true, |
| 200 | + color: _mat-create-dark-color-config($primary, $accent, $warn), |
| 201 | + )); |
| 202 | + @return _internalize-theme(theming.private-create-backwards-compatibility-theme($theme)); |
| 203 | + } |
| 204 | + // If the map pattern is used (1), we just pass-through the configurations for individual |
| 205 | + // parts of the theming system, but update the `color` configuration if set. As explained |
| 206 | + // above, the color shorthand will be expanded to an actual dark-themed color configuration. |
| 207 | + $result: $primary; |
| 208 | + @if map.get($primary, color) { |
| 209 | + $color-settings: map.get($primary, color); |
| 210 | + $primary: map.get($color-settings, primary); |
| 211 | + $accent: map.get($color-settings, accent); |
| 212 | + $warn: map.get($color-settings, warn); |
| 213 | + $result: map.merge($result, (color: _mat-create-dark-color-config($primary, $accent, $warn))); |
| 214 | + } |
| 215 | + @return _internalize-theme( |
| 216 | + theming.private-create-backwards-compatibility-theme(_mat-validate-theme($result))); |
| 217 | +} |
| 218 | + |
| 219 | +/// Gets the color configuration from the given theme or configuration. |
| 220 | +/// @param {Map} $theme The theme map returned from `define-light-theme` or `define-dark-theme`. |
| 221 | +/// @param {Map} $default The default value returned if the given `$theme` does not include a |
| 222 | +/// `color` configuration. |
| 223 | +/// @returns {Map} Color configuration for a theme. |
| 224 | +@function get-color-config($theme, $default: null) { |
| 225 | + @return theming.private-get-color-config($theme, $default); |
| 226 | +} |
| 227 | + |
| 228 | +/// Gets the density configuration from the given theme or configuration. |
| 229 | +/// @param {Map} $theme-or-config The theme map returned from `define-light-theme` or |
| 230 | +/// `define-dark-theme`. |
| 231 | +/// @param {Map} $default The default value returned if the given `$theme` does not include a |
| 232 | +/// `density` configuration. |
| 233 | +/// @returns {Map} Density configuration for a theme. |
| 234 | +@function get-density-config($theme-or-config, $default: 0) { |
| 235 | + @return theming.private-get-density-config($theme-or-config, $default); |
| 236 | +} |
| 237 | + |
| 238 | +/// Gets the typography configuration from the given theme or configuration. |
| 239 | +/// For backwards compatibility, typography is not included by default. |
| 240 | +/// @param {Map} $theme-or-config The theme map returned from `define-light-theme` or |
| 241 | +/// `define-dark-theme`. |
| 242 | +/// @param {Map} $default The default value returned if the given `$theme` does not include a |
| 243 | +/// `typography` configuration. |
| 244 | +/// @returns {Map} Typography configuration for a theme. |
| 245 | +@function get-typography-config($theme-or-config, $default: null) { |
| 246 | + @return theming.private-get-typography-config($theme-or-config, $default); |
| 247 | +} |
| 248 | + |
| 249 | +/// Copies the given theme object and nests it within itself under a secret key and replaces the |
| 250 | +/// original map keys with error values. This allows the inspection API which is aware of the secret |
| 251 | +/// key to access the real values, but attempts to directly access the map will result in errors. |
| 252 | +/// @param {Map} $theme The theme map. |
| 253 | +@function _internalize-theme($theme) { |
| 254 | + @if map.has-key($theme, theming.$private-internal-name) { |
| 255 | + @return $theme; |
| 256 | + } |
| 257 | + $internalized-theme: ( |
| 258 | + theming.$private-internal-name: ( |
| 259 | + theme-version: 0, |
| 260 | + m2-config: $theme |
| 261 | + ) |
| 262 | + ); |
| 263 | + @if (theming.$theme-legacy-inspection-api-compatibility) { |
| 264 | + @return map.merge($theme, $internalized-theme); |
| 265 | + } |
| 266 | + $error-theme: |
| 267 | + _replace-values-with-errors($theme, 'Theme may only be accessed via theme inspection API'); |
| 268 | + @return map.merge($error-theme, $internalized-theme); |
| 269 | +} |
| 270 | + |
| 271 | +/// Replaces concrete CSS values with errors in a theme object. |
| 272 | +/// Errors are represented as a map `(ERROR: <message>)`. Because maps are not valid CSS values, |
| 273 | +/// the Sass will not compile if the user tries to use any of the error theme values in their CSS. |
| 274 | +/// Users will see a message about `(ERROR: <message>)` not being a valid CSS value. Using the |
| 275 | +/// message, that winds up getting shown, we can help explain to users why they're getting the |
| 276 | +/// error. |
| 277 | +/// @param {*} $value The theme value to replace with errors. |
| 278 | +/// @param {String} $message The error message to sow users. |
| 279 | +/// @return {Map} A version of $value where concrete CSS values have been replaced with errors |
| 280 | +@function _replace-values-with-errors($value, $message) { |
| 281 | + $value-type: meta.type-of($value); |
| 282 | + @if $value-type == 'map' { |
| 283 | + @each $k, $v in $value { |
| 284 | + $value: map.set($value, $k, _replace-values-with-errors($v, $message)); |
| 285 | + } |
| 286 | + @return $value; |
| 287 | + } |
| 288 | + @else if $value-type == 'list' and list.length($value) > 0 { |
| 289 | + @for $i from 1 through list.length() { |
| 290 | + $value: list.set-nth($value, $i, _replace-values-with-errors(list.nth($value, $i), $message)); |
| 291 | + } |
| 292 | + @return $value; |
| 293 | + } |
| 294 | + @return (ERROR: $message); |
| 295 | +} |
0 commit comments