@@ -19,10 +19,25 @@ import {
1919// tonal palettes then get used to create the different color roles (ex.
2020// on-primary) https://m3.material.io/styles/color/system/how-the-system-works
2121const HUE_TONES = [ 0 , 10 , 20 , 25 , 30 , 35 , 40 , 50 , 60 , 70 , 80 , 90 , 95 , 98 , 99 , 100 ] ;
22+ // Map of neutral hues to the previous/next hues that
23+ // can be used to estimate them, in case they're missing.
24+ const NEUTRAL_HUES = new Map < number , { prev : number ; next : number } > ( [
25+ [ 4 , { prev : 0 , next : 10 } ] ,
26+ [ 6 , { prev : 0 , next : 10 } ] ,
27+ [ 12 , { prev : 10 , next : 20 } ] ,
28+ [ 17 , { prev : 10 , next : 20 } ] ,
29+ [ 22 , { prev : 20 , next : 25 } ] ,
30+ [ 24 , { prev : 20 , next : 25 } ] ,
31+ [ 87 , { prev : 80 , next : 90 } ] ,
32+ [ 92 , { prev : 90 , next : 95 } ] ,
33+ [ 94 , { prev : 90 , next : 95 } ] ,
34+ [ 96 , { prev : 95 , next : 98 } ] ,
35+ ] ) ;
36+
2237// Note: Some of the color tokens refer to additional hue tones, but this only
2338// applies for the neutral color palette (ex. surface container is neutral
2439// palette's 94 tone). https://m3.material.io/styles/color/static/baseline
25- const NEUTRAL_HUE_TONES = HUE_TONES . concat ( [ 4 , 6 , 12 , 17 , 22 , 24 , 87 , 92 , 94 , 96 ] ) ;
40+ const NEUTRAL_HUE_TONES = [ ... HUE_TONES , ... NEUTRAL_HUES . keys ( ) ] ;
2641
2742/**
2843 * Gets color tonal palettes generated by Material from the provided color.
@@ -117,7 +132,7 @@ export function generateSCSSTheme(
117132 "@use '@angular/material' as mat;" ,
118133 '' ,
119134 '// Note: ' + colorComment ,
120- '$_palettes: ' + getColorPalettesSCSS ( colorPalettes ) ,
135+ '$_palettes: ' + getColorPalettesSCSS ( patchMissingHues ( colorPalettes ) ) ,
121136 '' ,
122137 '$_rest: (' ,
123138 ' secondary: map.get($_palettes, secondary),' ,
@@ -192,3 +207,110 @@ export default function (options: Schema): Rule {
192207 createThemeFile ( themeScss , tree , options . directory ) ;
193208 } ;
194209}
210+
211+ /**
212+ * The hue map produced by `material-color-utilities` may miss some neutral hues depending on
213+ * the provided colors. This function estimates the missing hues based on the generated ones
214+ * to ensure that we always produce a full palette. See #29157.
215+ *
216+ * This is a TypeScript port of the logic in `core/theming/_palettes.scss#_patch-missing-hues`.
217+ */
218+ function patchMissingHues (
219+ palettes : Map < string , Map < number , string > > ,
220+ ) : Map < string , Map < number , string > > {
221+ const neutral = palettes . get ( 'neutral' ) ;
222+
223+ if ( ! neutral ) {
224+ return palettes ;
225+ }
226+
227+ let newNeutral : Map < number , string > | null = null ;
228+
229+ for ( const [ hue , { prev, next} ] of NEUTRAL_HUES ) {
230+ if ( ! neutral . has ( hue ) && neutral . has ( prev ) && neutral . has ( next ) ) {
231+ const weight = ( next - hue ) / ( next - prev ) ;
232+ const result = mixColors ( neutral . get ( prev ) ! , neutral . get ( next ) ! , weight ) ;
233+
234+ if ( result !== null ) {
235+ newNeutral ??= new Map ( neutral . entries ( ) ) ;
236+ newNeutral . set ( hue , result ) ;
237+ }
238+ }
239+ }
240+
241+ if ( ! newNeutral ) {
242+ return palettes ;
243+ }
244+
245+ // Create a new map so we don't mutate the one that was passed in.
246+ const newPalettes = new Map < string , Map < number , string > > ( ) ;
247+ for ( const [ key , value ] of palettes ) {
248+ if ( key === 'neutral' ) {
249+ // Maps keep the order of their keys which can make the newly-added
250+ // ones look out of place. Re-sort the the keys in ascending order.
251+ const sortedNeutral = Array . from ( newNeutral . keys ( ) )
252+ . sort ( ( a , b ) => a - b )
253+ . reduce ( ( newHues , key ) => {
254+ newHues . set ( key , newNeutral . get ( key ) ! ) ;
255+ return newHues ;
256+ } , new Map < number , string > ( ) ) ;
257+ newPalettes . set ( key , sortedNeutral ) ;
258+ } else {
259+ newPalettes . set ( key , value ) ;
260+ }
261+ }
262+
263+ return newPalettes ;
264+ }
265+
266+ /**
267+ * TypeScript port of the `color.mix` function from Sass, simplified to only deal with hex colors.
268+ * See https://github.com/sass/dart-sass/blob/main/lib/src/functions/color.dart#L803
269+ *
270+ * @param c1 First color to use in the mixture.
271+ * @param c2 Second color to use in the mixture.
272+ * @param weight Proportion of the first color to use in the mixture.
273+ * Should be a number between 0 and 1.
274+ */
275+ function mixColors ( c1 : string , c2 : string , weight : number ) : string | null {
276+ const normalizedWeight = weight * 2 - 1 ;
277+ const weight1 = ( normalizedWeight + 1 ) / 2 ;
278+ const weight2 = 1 - weight1 ;
279+ const color1 = parseHexColor ( c1 ) ;
280+ const color2 = parseHexColor ( c2 ) ;
281+
282+ if ( color1 === null || color2 === null ) {
283+ return null ;
284+ }
285+
286+ const red = Math . round ( color1 . red * weight1 + color2 . red * weight2 ) ;
287+ const green = Math . round ( color1 . green * weight1 + color2 . green * weight2 ) ;
288+ const blue = Math . round ( color1 . blue * weight1 + color2 . blue * weight2 ) ;
289+ const intToHex = ( value : number ) => value . toString ( 16 ) . padStart ( 2 , '0' ) ;
290+
291+ return `#${ intToHex ( red ) } ${ intToHex ( green ) } ${ intToHex ( blue ) } ` ;
292+ }
293+
294+ /** Parses a hex color to its numeric red, green and blue values. */
295+ function parseHexColor ( value : string ) : { red : number ; green : number ; blue : number } | null {
296+ if ( ! / ^ # (?: [ 0 - 9 a - f A - F ] { 3 } ) { 1 , 2 } $ / . test ( value ) ) {
297+ return null ;
298+ }
299+
300+ const hexToInt = ( value : string ) => parseInt ( value , 16 ) ;
301+ let red : number ;
302+ let green : number ;
303+ let blue : number ;
304+
305+ if ( value . length === 4 ) {
306+ red = hexToInt ( value [ 1 ] + value [ 1 ] ) ;
307+ green = hexToInt ( value [ 2 ] + value [ 2 ] ) ;
308+ blue = hexToInt ( value [ 3 ] + value [ 3 ] ) ;
309+ } else {
310+ red = hexToInt ( value . slice ( 1 , 3 ) ) ;
311+ green = hexToInt ( value . slice ( 3 , 5 ) ) ;
312+ blue = hexToInt ( value . slice ( 5 , 7 ) ) ;
313+ }
314+
315+ return { red, green, blue} ;
316+ }
0 commit comments