finos / a11y-theme-builder

DesignOps toolchain theme builder for accessibility inclusion using Atomic Design.
Apache License 2.0
43 stars 69 forks source link

[SDK] update the light and dark mode generating process #501

Open lwnoble opened 1 year ago

lwnoble commented 1 year ago

Problem/Concern

Proposed Solution

Please use the following logic for building shades:

     // build darker tones of color varients //
      function buildShades(mode, theme, color, shade) {
        var i = 0;
        var prime = shade
        var rgbArray = hextoRGBArray(color);
        // calculate how many light shades need to get built //
        var lightColors = (prime/100) + 1;
        // calculate how many dark shades need to get built //
        var darkColors = ((900-prime)/100) + 1
        console.log('shade:' + shade + ' lightColor: ' + lightColors + ' darkColors: ' + darkColors)
BEGIN NEW
        if (lightColors > 1)  {
          var lightscale  = chroma.scale([( '#FFFFFF') ,color]).correctLightness(true).colors(lightColors);
        } else {
END NEW
          lightscale = [color]
        }
BEGIN NEW
        if (darkColors > 1) {
          var endColor  = mixColors('#000000',color.toString(),.9);
          var darkscale = chroma.scale([color,endColor]).correctLightness(true).colors(darkColors);
        } else {
END NEW
          darkscale = [color]
        }
        if (lightscale.length > 0) {
          lightscale.splice(-1)
        }
        var colorScale = $.merge(lightscale, darkscale);

        console.log(colorScale)
        while (i < 10) {
          //var newRGB = adjustLightness(rgbArray,shade,lightness,mode)
BEGIN NEW
          if (i == 0) {
            var f = chroma.scale([( '#FFFFFF') ,color]);
            var scale = 100/(prime * 2)
            newRGB  = (f(scale)).toString();
          } else {
            var newRGB = colorScale [i]
            // adjust saturation of each color to create triangel effect - most saturated color are 600 and 700 //
            newRGB = triangle(color,i,prime, newRGB)
          }
END NEW
          var shade = i * 100
            if (getContrast(newRGB) == '#ffffff') {
              text_color = [255,255,255]; // white
            } else {
              text_color = [18,18,18]; // black
            }
            // get the contrast ration of the color against the suggested text color //
            var contrastRation = contrast(rgbArray, text_color); // 1.0736196319018405
            // convert the color to hex //
            newRGB = rgb2hex(newRGB)

            // based on the mode light or dark - run the appropriate check to see if the color and on color meet the contrats ratio of 4.5 or if the shade needs to be lighted or darked //
            if (mode == 'dark') {
BEGIN NEW
              newRGB = mixColors('#000000',newRGB,.15);
END NEW
              checkDM(theme+'-dark-'+shade, newRGB)
            } else {
              checkContrast(theme+'-'+mode+'-'+shade, newRGB, mode)
            }
            //
            // loop through each shade //
            i++;
        }
      }

I created this new function to mix two colors - one with an opacity to create an hex value.

    const mixColors = (c1, c2, opacity) => {
      const pn = n => ('0' + n.toString(16)).slice(-2);
        const [r0, g0, b0, r1, g1, b1] = [
        parseInt(c1.slice(1, 3), 16),
        parseInt(c1.slice(3, 5), 16),
        parseInt(c1.slice(5, 7), 16),
        parseInt(c2.slice(1, 3), 16),
        parseInt(c2.slice(3, 5), 16),
        parseInt(c2.slice(5, 7), 16),
      ];
        const [r, g, b] = [
        Math.round(r0 * opacity + r1 * (1 - opacity)),
        Math.round(g0 * opacity + g1 * (1 - opacity)),
        Math.round(b0 * opacity + b1 * (1 - opacity)),
      ];
      return `#${pn(r)}${pn(g)}${pn(b)}`;
    };

      // update saturation triangle - highest saturated shade at 6 //
      function triangle(color,i,prime, newRGB) {
      var primeHcl = chroma(color).hcl();
        var hcl = chroma(newRGB).hcl();
        prime = prime/100
        var change = i/prime
        if (i <= 6) {
          var change = i/prime
        } else {
          var change = (11 - i)/prime
        }
        var s = primeHcl[1]
        var newHCL = chroma.hcl(hcl[0], s * change, hcl[2]).hex();
        var s = primeHcl[1]
        var newHCL = chroma.hcl(hcl[0], s * change, hcl[2]).hex();
        return(newHCL)
      }

This creates a consistent scale of colors in both light and dark mode.

aaronreed708 commented 1 year ago

Keith is working this with #440

smithbk commented 1 year ago

@lwnoble Lise, we need to walk through this together. This is a complete change it seems to the current logic for generating dark mode shades when adding a new color. I might be able to figure out #1, but I need clarity on the others. For example, on #2, "Check the saturations levels of each color and adjust", exactly what check and how to adjust.
I'll try to set up a time to discuss. See https://github.com/finos/a11y-theme-builder-sdk/blob/main/src/common/shade.ts#L416 for how the shades are currently being built.

aaronreed708 commented 1 year ago

@lwnoble and @smithbk to collaborate

lwnoble commented 11 months ago

Keith I will schedule time for us after thanks giving.

lwnoble commented 10 months ago

I have updated the following:

1.) The BuildShades function

smithbk commented 8 months ago

@lwnoble Lise, I spent several hours trying to translate your code and I have lots of questions.

  1. You said that you got rid of mixColors, but there is still a call to it in your latest buildShades.
  2. You are passing "theme" as the "colorName" argument to rescale. Is that correct.
  3. I don't know where the checkContrast function is that you are calling or exactly what it does. The comment seems to imply that it may adjust it.
  4. There are some undefined variables and other things in rescale that I'm not sure how to handle.

I think we need a working session.

In general, I think making changes to your old prototype is not the way to go. My $.02 woth.

lwnoble commented 6 months ago

Keith this is the Build Shades function

      // build darker tones of color varients //
      function buildShades(mode, theme, color, shade) {
        var i = 0;
        var prime = shade
        var rgbArray = hextoRGBArray(color);
        // calculate how many light shades need to get built //
        var lightColors = (prime/100) + 1;
        // calculate how many dark shades need to get built //
        var darkColors = ((900-prime)/100) + 1
        if (lightColors > 1)  {
          var lightscale  = chroma.scale([( '#FFFFFF') ,color]).correctLightness(true).colors(lightColors);
        } else {
          lightscale = [color]
        }
        if (darkColors > 1) {
          if (mode == 'dark') {
            var endColor  = mixColors('#000000',color.toString(),.98);
          } else {
            var endColor  = mixColors('#000000',color.toString(),.95);
          }
          var darkscale = chroma.scale([color,endColor]).correctLightness(true).colors(darkColors);
        } else {
          darkscale = [color]
        }
        if (lightscale.length > 0) {
          lightscale.splice(-1)
        }
        var colorScale = $.merge(lightscale, darkscale);
        while (i < 10) {
          //var newRGB = adjustLightness(rgbArray,shade,lightness,mode)
          if (i == 0) {
            var f = chroma.scale([( '#FFFFFF') ,color]);
            if (mode == 'light') {
              var scale = 100/(prime * 2)
            } else {
              var scale = (100/ (prime * 4)) * 3
            }
            newRGB  = (f(scale)).toString();
          } else {
            var newRGB = colorScale [i]
            // adjust saturation of each color to create triangel effect - most saturated color are 600 and 700 //
          }
          newRGB = triangle(color,i,prime, newRGB, mode)
          var shade = i * 100
            if (getContrast(newRGB) == '#ffffff') {
              text_color = [255,255,255]; // white
            } else {
              text_color = darkTextArray; // black
            }
            // get the contrast ration of the color against the suggested text color //
            var contrastRation = contrast(rgbArray, text_color); // 1.0736196319018405
            // convert the color to hex //
            newRGB = rgb2hex(newRGB)
            // based on the mode light or dark - run the appropriate check to see if the color and on color meet the contrats ratio of wcagContrast or if the shade needs to be lighted or darked //
            checkContrast(theme+'-'+mode+'-'+shade, newRGB, mode)
            //
            // loop through each shade //
            i++;
        }
      if (mode == 'dark') {
            $(document).find('#' + theme + '-' + mode).removeClass('rescaled')
            rescale(theme,'dark')
        }
      }

It uses the MixColors function:

    const mixColors = (c1, c2, opacity) => {
      const pn = n => ('0' + n.toString(16)).slice(-2);
        const [r0, g0, b0, r1, g1, b1] = [
        parseInt(c1.slice(1, 3), 16),
        parseInt(c1.slice(3, 5), 16),
        parseInt(c1.slice(5, 7), 16),
        parseInt(c2.slice(1, 3), 16),
        parseInt(c2.slice(3, 5), 16),
        parseInt(c2.slice(5, 7), 16),
      ];
        const [r, g, b] = [
        Math.round(r0 * opacity + r1 * (1 - opacity)),
        Math.round(g0 * opacity + g1 * (1 - opacity)),
        Math.round(b0 * opacity + b1 * (1 - opacity)),
      ];
      return `#${pn(r)}${pn(g)}${pn(b)}`;
    };

Using the MixColors function I created this small lighten function:

    function lighten(color,amount) {
      return((mixColors(color,'#ffffff',amount )).toString())
    }

Here is the checkContrast function (which now correctly checks the background color in dark mode against text with an opacity over the background using the above mixColor function)

function checkContrast(theme, color, mode) {
      var lightTextArray = hextoRGBArray(lightText);
      var rgbArray          = hextoRGBArray(rgb2hex(color));
      var shade = theme.split('-')[2];
      var newRGB = "rgb(" + rgbArray +")"
      var lightArray = lightTextArray
      var light = contrast(lightArray, rgbArray);
      var dark  = contrast(darkTextArray, rgbArray);
      var text_color, textTint, contrastRatio
      var contrastRatio = contrast(lightArray, rgbArray);
      var elevationHex;
      if ( light > dark ) {
        text_color = lightArray; // white
        var textTint = 'light';
        if (mode == 'dark') {
          var colorHex = rgb2hex(color)
          /// for dark mode - lighten color light text ///
          var newText = lighten(colorHex,mixer)
          var newArray = hextoRGBArray(colorHex);
          var lightArray = hextoRGBArray(newText)
          var textHex
          contrastRatio = contrast(lightArray, newArray);
          var i = .00
          while (contrastRatio < wcagContrast) {
            var hex = (chroma(color).darken(i)).toString()
            var textHex =  (mixColors(hex,'#ffffff',mixer )).toString();
            var textArray = hextoRGBArray(textHex);
            var newArray = hextoRGBArray(hex);
            var contrastRatio = contrast(newArray, textArray);
            i = i + .01
          }
          var newHex   = (chroma(rgb2hex(color)).darken(i)).toString()
          console.log('original color: ' + color + ', i:' + i + ', elevationHex: ' + elevationHex + ', textHex: ' + textHex + ', conttrast: ' + contrastRatio  + ' new-color: ' + newHex)
          var rgbArray = hextoRGBArray(newHex);
          var textTint = 'light';
          buildColor(theme,mode,rgbArray,text_color,contrastRatio)
          return false;
        }
      } else {
        text_color = darkTextArray; // dark
        var textTint = 'dark';
        contrastRatio = contrast(text_color, rgbArray);
      }
      if (textTint == 'light') {
        var buildText = lightTextArray
      } else {
        var buildText =  darkTextArray
      }
      contrastRatio  = contrastRatio.toFixed(2)
      if (contrastRatio < wcagContrast) {
        var darkCount      = adjustDarkerCount(theme, newRGB, lightArray, contrastRatio, mode)
        var lightCount     = adjustLighterCount(theme, newRGB, darkTextArray, contrastRatio, mode)
        if (darkCount < lightCount || shade >= 600) {
          adjustColorDarker(theme, newRGB, lightArray, contrastRatio, mode)
        } else {
          adjustColorLighter(theme, newRGB, darkTextArray, contrastRatio, mode)
        }
      } else {
        console.log('theme: ' + theme + ' ,text color:' + text_color + ', rgbArray' +  rgbArray  + ', contrastRatio: ' + contrastRatio)
        buildColor(theme,mode,rgbArray,buildText,contrastRatio)
      }
    }

And here is the triangle function:

      // updade sautation //
      function triangle(color,i,prime, newRGB, mode) {
        var maxChroma, dmmaxChroma
        if ($('#setchromaMax').is(':checked')) {
          maxChroma   = $('#chromaMax').val();
          dmmaxChroma = $('#dmchromaMax').val();
        } else {
          maxChroma = 100
          dmmaxChroma = 40
        }
        prime = prime/100
        var primeHcl = chroma(color).hcl();
        if (primeHcl[1] > maxChroma) {
          maxChroma = primeHcl[1]
        }
        if (mode == 'dark') {
          if (maxChroma >= dmmaxChroma) {
            maxChroma = dmmaxChroma
          }
        }
        var primeChroma = primeHcl[1]
        var ihcl = chroma(newRGB).hcl()
        var change
        if (i == prime) {
          change = 1
          var newChroma = primeChroma * change
        } else if (prime < 7) {
          if (i == 0) {
            if (mode == 'dark') {
                change =.75/prime
            } else {
                change =.5/prime
            }
          }
          else if (i <= 7) {
            change = i/prime
          } else {
            change = (7 - (i - 7) - 1)/7
          }
          var newChroma = primeChroma * change
        } else {
          var seven = (7/(7 - (prime - 7) - 1)) * primeChroma
          if (seven > maxChroma) {
            seven = maxChroma
          }
          console.log('7: '  + seven)
          if (i <= 7) {
            if (i == 0) {
               var change = .75/7
            } else {
               var change = i/7
            }
            console.log('change:'  + change + ', seven: ' + seven)
            newChroma = seven * change
            console.log('i: ' + i + ', newChroma: ' + newChroma)
          } else {
            var change = (7 - (i - 7) - 1)/7
            newChroma = seven * change
            console.log('i: ' + i + ', newChroma: ' + newChroma)
          }
        }
        /// don't let the chroma be over the max of less than 4 ///
        if (newChroma > maxChroma) {
          newChroma = maxChroma
        } else if (newChroma < 4){
          newChroma = 4
        }
        newChroma = newChroma
        console.log('prime:'  + prime + ', i: ' + i + ', change: ' + change + ' Chroma:' + newChroma)
        console.log('h: ' + ihcl[0]  +  ', c: ' + newChroma + ' , l: ' + ihcl[2])
        var newHCL = chroma.hcl(ihcl[0], newChroma , ihcl[2]).hex();
        console.log('i:'  + i + ', hex: ' + newHCL + ' , chroma:'  + chroma(newHCL).hcl()[1]);
        return(newHCL)
      }

Note in the code above I have introduced the following variables:

WCAGContrast (for WCAG AA which is 4.5 or WCAG AAA is 7.1 for text) - by default this is 4.5
MaxChoma (Light Mode Max Chroma) by default this is 100
dmmaxChroma (Dark Mode Max Chroma) by default this is 100
darkTextArray (which is the rgb of the dark text the user enters  for discover's #23233f this value = 35,35,63); by default this is 18,18,18

I have the light text set to pure white 255,255,255

I have removed the rescale function for now

lwnoble commented 6 months ago

For dark mode I have created two new functions to improve the color scales.

adjusttoMaxContrast

    function adjusttoMaxContrast(color,text,mode) {
    /// get shades as close to contrast requiement as possible ///
    var i = 0
    var hex = (chroma(color).darken(i)).toString()
    var startHex = hex
    var newText, textArray, rbgArray, contrastRatio
    // get the dark mode text color //
    if (text == '#ffffff') {
       if (mode == 'dark') {
         newText = lighten(color,mixer).toString()
         textArray = hextoRGBArray(newText);
       } else {
         textArray = [255,255,255]
       }
    } else {
       textArray = darkTextArray
    }

    rgbArray = hextoRGBArray(hex);
    // get the contrast ration of the color against the suggested text color //
    contrastRatio = contrast(rgbArray, textArray);
    //alert('start i: ' + i + ', color: ' + hex + ', contrast: ' + contrastRatio)
    while (contrastRatio > wcagContrast) {
      i = i + .01
      if (text == '#ffffff') {
        hex = lighten(color,1 - i)
        if (mode == 'dark') {
          newText = lighten(color,mixer).toString()
          textArray = hextoRGBArray(newText);
        } else {
          textArray = [255,255,255]
        }
      } else {
        hex = (chroma(color).darken(i)).toString()
        textArray = darkTextArray
      }
      rgbArray = hextoRGBArray(hex);
      contrastRatio = contrast(rgbArray, textArray);
      //alert('i: ' + i + ', color: ' + hex + ', contrast: ' + contrastRatio)
    }
    i = i - .01
    if (text == '#ffffff') {
      var hex = lighten(color,1 - i)
      if (mode == 'dark') {
        newText = lighten(color,mixer).toString()
        textArray = hextoRGBArray(newText);
      } else {
        textArray = [255,255,255]
      }
    } else {
      hex = (chroma(color).darken(i)).toString()
      textArray = darkTextArray
    }
    rgbArray = hextoRGBArray(hex);
    contrastRatio = contrast(rgbArray, textArray);
    if (contrastRatio < wcagContrast || i == 0) {
      hex = startHex
      rgbArray = hextoRGBArray(hex);
      contrastRatio = contrast(rgbArray, textArray);
    }
    return(hex)
  }

adjust


  function adjust(colorName, mode) {
        var lastChar = dmOpacity[dmOpacity.length - 1];
        if (lastChar == "0") {
          dmOpacity = dmOpacity.slice(0, -1);
        }
        i = 100
        var firstLightText;
        while (i <= 900){
          var text = $(document).find('#' + colorName + '-'+mode+'-' + i + ' .Hex').css('color');
          var darkTextRGB = hex2rgb(darkText)
          text = text.replace(/ /g, '');
          if (text == 'rgba(255,255,255,'+dmOpacity+')' || text == 'rgb(255,255,255)') {
            firstLightText = i
            lastDarkText = i - 100;
            var startLightShade = rgb2hex($(document).find('#' + colorName + '-'+mode+'-0 .Hex').css('backgroundColor'));
            var endLightShade          = rgb2hex($(document).find('#' + colorName + '-'+mode+'-' + lastDarkText + ' .Hex').css('backgroundColor'));
            var nexttoLast = lastDarkText - 100
            var nexttoLastLightShade   = rgb2hex($(document).find('#' + colorName + '-'+mode+'-' + nexttoLast + ' .Hex').css('backgroundColor'));
            // check to see if the last and 2nd to last colors are close ///
            var difference = chroma.deltaE(endLightShade , nexttoLastLightShade);
            // if the color
            if ($('#' + colorName + '-'+mode+'-' + lastDarkText + ' .Hex').hasClass('lightened') || $('#' + colorName + '-'+mode+'-' + lastDarkText + ' .Hex').hasClass('darkened') || difference < 1.5) {
              var endLightShade   = rgb2hex($(document).find('#' + colorName + '-'+mode+'-' + nexttoLast  + ' .Hex').css('backgroundColor'));
            } else {
              var endLightShade   = rgb2hex($(document).find('#' + colorName + '-'+mode+'-' + lastDarkText + ' .Hex').css('backgroundColor'));
            }
            // adjust the color to have the max possible contrast //
            endLightShade = adjusttoMaxContrast(endLightShade, darkTextArray, mode);
            //alert(colorName+'-'+mode+': ' + endLightShade)
            $(document).find('#' + colorName + '-'+mode+'-' + lastDarkText + ' .Hex').addClass('lastDarkText');
            checkContrast(colorName+'-'+mode+'-'+lastDarkText, hex2rgb(endLightShade), mode);
            var d = firstLightText
            var startDarkShade = rgb2hex($(document).find('#' + colorName + '-'+mode+'-' + firstLightText + ' .Hex').css('backgroundColor'));
            startDarkShade = adjusttoMaxContrast(startDarkShade,'#ffffff',mode)
            $(document).find('#' + colorName + '-'+mode+'-' + firstLightText + ' .Hex').addClass('firstLightText')
            checkContrast(colorName+'-'+mode+'-'+d, hex2rgb(startDarkShade), mode);
            rescale(colorName,mode,lastDarkText );
            return false;
          }
          i = i + 100
        }
      }

rescale

  function rescale(colorName, mode, lastDarkText ) {
        // get the lights shade //
        var startLightShade = rgb2hex($(document).find('#' + colorName + '-'+ mode +'-0 .Hex').css('backgroundColor'));
        // get the last shade with dark text //
        var endLightShade  = rgb2hex($(document).find('#' + colorName + '-'+ mode +'-'+ lastDarkText + ' .Hex').css('backgroundColor'));
        var colorCount = lastDarkText/100 + 1
        var newLightShades = chroma.scale([startLightShade,endLightShade]).colors(colorCount);
        // cycle through the new chroma scale and assign to the shades //
        var firstLightText = lastDarkText + 100
        var n = 0
        while (n < firstLightText  ) {
          var shadeIndex = n/100
          var newColor = newLightShades[shadeIndex]
          var newRGB   = hex2rgb(newColor);
          checkContrast(colorName+'-'+mode+'-'+n, newRGB, mode)
          n = n + 100
        }
        // get the darkest shade //
        var endDarkShade   = rgb2hex($(document).find('#' + colorName + '-'+mode+'-900 .Hex').css('backgroundColor'));
        // get the first shade with light text //
        var startDarkShade = rgb2hex($(document).find('#' + colorName + '-'+ mode +'-'+ firstLightText+' .Hex').css('backgroundColor'));
        var d = firstLightText
        while (d <= 900) {
          if (d == 900) {
            var endDarkShade   = rgb2hex($(document).find('#' + colorName + '-'+mode+'-900 .Hex').css('backgroundColor'));
            var newRGB = hex2rgb(endDarkShade)
            checkContrast(colorName+'-'+mode+'-'+d, newRGB, mode)
          } else {
            // cycle through the new chroma scale and assign to the shades //
            var colorCount = (900 - lastDarkText)/100
            var newDarkShades = chroma.scale([startDarkShade,endDarkShade]).colors(colorCount);
            var shadeIndex = (d - firstLightText)/100
            var newColor = newDarkShades[shadeIndex]
            var newRGB   = hex2rgb(newColor);
            checkContrast(colorName+'-'+mode+'-'+d, newRGB, mode)
          }
        d = d + 100
       }
      }
smithbk commented 2 months ago

Changes are in the issue-501-update-light-and-dark-mode branch. It compiles but need to test & debug as needed with @aaronreed708

smithbk commented 1 month ago

AFAICT, the changes in the issue-501-update-light-and-dark-mode work. The UI needs to be changed to support AAA given these changes.

For the color palette atom, see src/atoms/colorPalette.ts, the Color class, note the following variables:

    /** Color specific Shade builder config */
    public wcagLevel: PropertyWCAGSelectable;
    public lightModeMaxChroma: PropertyNumberRange;
    public darkModeMaxChroma: PropertyNumberRange;
    /** Shades */
    public shades: ShadeBuilderView;
    // "light" and "dark" are temporary until UI changes to use "shades" above instead
    public light: { shades: Shade[] };
    public dark: { shades: Shade[] };

The UI is current referencing the "light.shades" and "dark.shades" variables to get the list of shades to display in the color palette. The UI should be changed to instead call "shades.getShades(WCAGLevel.AA)" to get light and dark mode shades for a AA tab and "shades.getShades(WCAGLevel.AAA)" to get light and dark mode shades for a AAA tab. You can also call "shades.addListener" to be notified each time a property is updated which should cause you to call this function again to get an updated list of shades.

Regarding the properties which are to be updated, you can see the color-specific properties above (wcagLevel, lightModeMaxChroma, and darkModeMaxChroma. There are also the same property names on the state settings atom because we have the 4 colors there also. There are also several properties on the DesignSystem in designSystem.ts as shown below which need to be exposed in the UI.

    /** Config for shade building */
    public wcagLevel: PropertyWCAGSelectable;
    public lightText: PropertyString;
    public darkText: PropertyString;
    public lightModeLightTextOpacity: PropertyNumber;
    public darkModeLightTextOpacity: PropertyNumber;
    public lightModeMaxChroma: PropertyNumber;
    public darkModeMaxChroma: PropertyNumber;

Here are some design highlight notes ...

The new file src/common/shadeBuilder.ts contains the bulk of the new code. It contains a ShadeBuilder class which consumes the ShadeBuilderCfg class as input to generate 10 shades for a given color. It also contains a ShadeBuilderView class which takes a ShadeBuilderViewCfg class as input, where ShadeBuilderViewCfg extends ShadeBuilderCfg, which overrides most of the configuration input by extracting values from properties associated with various nodes in the design system tree. The design system tree is the tree formed by a design system as it's root (see src/designSystem.ts) and various nodes (see src/common/node.ts) and properties (see src/common/props.ts) as the nodes of the tree. The getPropsByName and getPropValueByName functions in src/common/node.ts allows properties and values to be associated with and looked up given any node in the tree; it starts at the given node and searches up to the design system root, checking each node for a property field by the given name. If a property is found with a value, then we search no further up the tree; otherwise, we always return a value because the ShadeBuilderCfg class in src/common/shadeBuilder.ts always returns a default value.

The ShadeBuilderView class contains 4 shade builders which together comprise the view of shades which we concurrently want to build: 1) light mode AA 2) dark mode AA 3) light mode AAA 4) dark mode AAA