This is an automated email from the ASF dual-hosted git repository.

greg-dove pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/royale-asjs.git

commit 84579b69e1b7dc0354f9f3e9337cb7eeef7cef14
Author: greg-dove <[email protected]>
AuthorDate: Wed Apr 22 14:14:24 2026 +1200

    Color support sweep. Lenient support for WCAG contrast generation.
    If a lighter or darker contrast is preferred then an attempt is made to 
find a contrast that meets WCAG standards, but if it cannot find a compliant 
color, it still sticks with the preference.
---
 .../org/apache/royale/style/colors/ColorSwatch.as  |  32 ++-
 .../org/apache/royale/style/colors/ColorUtils.as   | 183 +++++++++----
 .../apache/royale/style/colors/ThemeColorSet.as    | 138 ++++++++--
 .../org/apache/royale/style/util/StyleTheme.as     |   3 +-
 .../src/test/royale/FlexUnitRoyaleApplication.mxml |   5 +-
 .../ColorUtilsContrastFunctionTest.as              | 128 +++++++++
 .../royale/flexUnitTests/ColorUtilsContrastTest.as | 295 +++++++++++++++++++++
 .../test/royale/flexUnitTests/ThemeColorSetTest.as | 206 ++++++++++++++
 8 files changed, 896 insertions(+), 94 deletions(-)

diff --git 
a/frameworks/projects/Style/src/main/royale/org/apache/royale/style/colors/ColorSwatch.as
 
b/frameworks/projects/Style/src/main/royale/org/apache/royale/style/colors/ColorSwatch.as
index 111a876f28..83d2c33baf 100644
--- 
a/frameworks/projects/Style/src/main/royale/org/apache/royale/style/colors/ColorSwatch.as
+++ 
b/frameworks/projects/Style/src/main/royale/org/apache/royale/style/colors/ColorSwatch.as
@@ -91,9 +91,6 @@ package org.apache.royale.style.colors
                                        var col:uint = BASE_COLORS[key];
                                        lchLookups[key] = 
ColorUtils.rgb_ToOKLCH([(col>>16)&0xff,(col>>8)&0xff,col&0xff])
                                }
-                               //add white and black
-                               
-                               
                        }
                        return lchLookups;
                }
@@ -106,12 +103,23 @@ package org.apache.royale.style.colors
                        for (var name:String in BASE_COLORS_OKLCH) {
                                var ref:Array = BASE_COLORS_OKLCH[name];
                                var d:Number = 
ColorUtils.oklchDistance(inputAsOKLCH, ref);
+                               
+                               // Bias toward black/white if they are in the 
lookups
+                               if (name == "white" || name == "black") {
+                                       d *= 0.01;
+                                       // If the input is very extreme (L 
close to 0 or 1), force a very small distance
+                                       if (name == "white" && inputAsOKLCH[0] 
> 0.99) d = 0;
+                                       if (name == "black" && inputAsOKLCH[0] 
< 0.01) d = 0;
+                               }
+
                                if (d < bestDist) {
                                        bestDist = d;
                                        bestName = name;
                                }
                        }
                        var base:Array = BASE_COLORS_OKLCH[bestName];
+                       if (bestName == "white") return new 
ColorSwatch("white", 0);
+                       if (bestName == "black") return new 
ColorSwatch("black", 0);
                        var ramp:Object = ColorUtils.getOklchRamp(base);
                        var bestShade:uint = 500;
                        bestDist = Number.MAX_VALUE;
@@ -121,7 +129,7 @@ package org.apache.royale.style.colors
                                if (d < bestDist)
                                {
                                        bestDist = d;
-                                       bestShade = uint(shadeKey);
+                                       bestShade = Number(shadeKey);
                                }
                        }
                        return new ColorSwatch(bestName,bestShade);
@@ -142,13 +150,17 @@ package org.apache.royale.style.colors
                 */
                public function 
ColorSwatch(swatch:String,shade:Number,opacity:Number = 
100,darkMode:Boolean=false)
                {
-                       var base:Object = BASE_COLORS[swatch] || 
CSSLookup.getProperty(swatch);
-                       assert(base, "Invalid color swatch: " + swatch);
+                       if (swatch == "white") {
+                               rgb = [255,255,255];
+                       } else if (swatch == "black") {
+                               rgb = [0,0,0];
+                       } else {
+                               var base:Object = BASE_COLORS[swatch] || 
CSSLookup.getProperty(swatch);
+                               assert(base, "Invalid color swatch: " + swatch);
+                               var baseColor:uint = CSSUtils.toColor(base);
+                               rgb = 
ColorUtils.getVariation(baseColor,Math.round(shade/10),darkMode);
+                       }
                        assert(shade>=0 && shade<=1000, "Invalid shade: " + 
shade);
-                       var baseColor:uint = CSSUtils.toColor(base);
-                       // Convert from 50,100,200... to 5,10,20... for easier 
math.
-               //      shade = Math.round(shade/10);
-                       rgb = 
ColorUtils.getVariation(baseColor,Math.round(shade/10),darkMode);
                        assert(opacity >= 0 && opacity <= 100, "Opacity must be 
between 0 and 100");
                        colorBase = swatch;
                        colorShade = shade;
diff --git 
a/frameworks/projects/Style/src/main/royale/org/apache/royale/style/colors/ColorUtils.as
 
b/frameworks/projects/Style/src/main/royale/org/apache/royale/style/colors/ColorUtils.as
index 057393c547..0626a94f89 100644
--- 
a/frameworks/projects/Style/src/main/royale/org/apache/royale/style/colors/ColorUtils.as
+++ 
b/frameworks/projects/Style/src/main/royale/org/apache/royale/style/colors/ColorUtils.as
@@ -28,6 +28,8 @@ package org.apache.royale.style.colors
                        
                }
                
+               public static const WANT_LIGHT_THRESHOLD:Number= 0.675;
+               
                /**
                 * Returns an RGB array representing a perceptually-balanced 
variation of the input color.
                 * Uses OKLCH color space to maintain consistent hue and 
saturation across different lightness levels.
@@ -45,6 +47,8 @@ package org.apache.royale.style.colors
                        var r:Number = (color >> 16) & 0xFF;
                        var g:Number = (color >> 8) & 0xFF;
                        var b:Number = color & 0xFF;
+                       
+                       if (grayValue == 50) return [r, g, b];
 
                        //convert to 0 - 1000 range
                        var t:Number = pinValue(grayValue, 0, 100) * 10;
@@ -54,27 +58,6 @@ package org.apache.royale.style.colors
                        //shade it
                        lch = 
lchShade(lch,factorForShadeTableInterpolated(t,darkMode));
                        return oklch_ToRGB(lch);
-                       
-                       /*var outR:Number;
-                       var outG:Number;
-                       var outB:Number;
-
-                       if (t <= 50)
-                       {
-                               var toBase:Number = t / 50;
-                               outR = 255 + (r - 255) * toBase;
-                               outG = 255 + (g - 255) * toBase;
-                               outB = 255 + (b - 255) * toBase;
-                       }
-                       else
-                       {
-                               var toBlack:Number = (t - 50) / 50;
-                               outR = r * (1 - toBlack);
-                               outG = g * (1 - toBlack);
-                               outB = b * (1 - toBlack);
-                       }
-
-                       return [Math.round(outR), Math.round(outG), 
Math.round(outB)];*/
                }
 
                /**
@@ -86,7 +69,7 @@ package org.apache.royale.style.colors
                        var gg:uint = uint(pinValue(g, 0, 255));
                        var bb:uint = uint(pinValue(b, 0, 255));
                        var color:uint = (rr << 16) | (gg << 8) | bb;
-                       return getVariation(color, grayValue);
+                       return getVariation(color, grayValue,darkMode);
                }
                
                /**
@@ -134,7 +117,7 @@ package org.apache.royale.style.colors
                 * @param lch lch values in 3 element array
                 * @return rgb values in 3 element array
                 */
-               public static function oklch_ToRGB(lch:Array):Array {
+               /*public static function oklch_ToRGB(lch:Array):Array {
                        
                        // --- 1. OKLCH → OKLab ---
                        const L:Number = lch[0];
@@ -176,8 +159,90 @@ package org.apache.royale.style.colors
                        var b:uint = uint(pinValue(linearToSrgb(bLin),0,1) * 
255);
                        
                        return [r ,g ,b];
+               }*/
+               
+               public static function oklch_ToRGB(lch:Array):Array
+               {
+                       const L:Number = lch[0];
+                       const C:Number = lch[1];
+                       const hRad:Number = lch[2] * Math.PI / 180.0;
+                       
+                       // Convert OKLCH → OKLab
+                       function labFrom(L:Number, C:Number, hRad:Number):Array 
{
+                               const a_:Number = C * Math.cos(hRad);
+                               const b_:Number = C * Math.sin(hRad);
+                               
+                               const l_:Number = L + 0.3963377774 * a_ + 
0.2158037573 * b_;
+                               const m_:Number = L - 0.1055613458 * a_ - 
0.0638541728 * b_;
+                               const s_:Number = L - 0.0894841775 * a_ - 
1.2914855480 * b_;
+                               
+                               const l:Number = (l_ < 0) ? 0 : l_ * l_ * l_;
+                               const m:Number = (m_ < 0) ? 0 : m_ * m_ * m_;
+                               const s:Number = (s_ < 0) ? 0 : s_ * s_ * s_;
+                               
+                               return [l, m, s];
+                       }
+                       
+                       // Convert LMS → sRGB (linear → gamma)
+                       function rgbFrom(l:Number, m:Number, s:Number):Array {
+                               var rLin:Number =  4.0767416621 * l - 
3.3077115913 * m + 0.2309699292 * s;
+                               var gLin:Number = -1.2684380046 * l + 
2.6097574011 * m - 0.3413193965 * s;
+                               var bLin:Number = -0.0041960863 * l - 
0.7034186147 * m + 1.7076147010 * s;
+                               
+                               var r:Number = pinValue(linearToSrgb(rLin), 0, 
1);
+                               var g:Number = pinValue(linearToSrgb(gLin), 0, 
1);
+                               var b:Number = pinValue(linearToSrgb(bLin), 0, 
1);
+                               
+                               return [r, g, b];
+                       }
+                       
+                       // Try full chroma first
+                       var lms:Array = labFrom(L, C, hRad);
+                       var rgb:Array = rgbFrom(lms[0], lms[1], lms[2]);
+                       
+                       var inGamut:Boolean =
+                                       rgb[0] >= 0 && rgb[0] <= 1 &&
+                                       rgb[1] >= 0 && rgb[1] <= 1 &&
+                                       rgb[2] >= 0 && rgb[2] <= 1;
+                       
+                       if (inGamut) {
+                               return [
+                                       uint(rgb[0] * 255),
+                                       uint(rgb[1] * 255),
+                                       uint(rgb[2] * 255)
+                               ];
+                       }
+                       
+                       // Otherwise binary-search chroma down
+                       var low:Number = 0;
+                       var high:Number = C;
+                       
+                       for (var i:int = 0; i < 20; i++) {
+                               var mid:Number = (low + high) * 0.5;
+                               
+                               lms = labFrom(L, mid, hRad);
+                               rgb = rgbFrom(lms[0], lms[1], lms[2]);
+                               
+                               inGamut =
+                                               rgb[0] >= 0 && rgb[0] <= 1 &&
+                                               rgb[1] >= 0 && rgb[1] <= 1 &&
+                                               rgb[2] >= 0 && rgb[2] <= 1;
+                               
+                               if (inGamut)
+                                       low = mid;
+                               else
+                                       high = mid;
+                       }
+                       
+                       return [
+                               uint(rgb[0] * 255),
+                               uint(rgb[1] * 255),
+                               uint(rgb[2] * 255)
+                       ];
                }
                
+               
+               
                private static function srgbToLinear(x:Number):Number {
                        return (x <= 0.04045) ? x / 12.92 : Math.pow((x + 
0.055) / 1.055, 2.4);
                }
@@ -262,17 +327,7 @@ package org.apache.royale.style.colors
                        for (var shade:String in shading_factors)
                        {
                                var m:Number = shading_factors[shade];
-                               
-                               // Lightness scaling
-                               var L:Number = base[0] * m;
-                               
-                               // Chroma scaling (slightly reduced for darker 
shades)
-                               var C:Number = base[1] * (m < 1 ? m : 1);
-                               
-                               // Hue stays constant
-                               var H:Number = base[2];
-                               
-                               ramp[shade] = [L,C,H];
+                               ramp[shade] = lchShade(base, m);
                        }
                        return ramp;
                }
@@ -283,10 +338,18 @@ package org.apache.royale.style.colors
                        var dh:Number = Math.abs(a[2] - b[2]);
                        if (dh > 180) dh = 360 - dh;
                        
+                       // If both colors are very achromatic, hue doesn't 
matter
+                       if (a[1] < 0.01 && b[1] < 0.01) dh = 0;
+                       
+                       // Normalize dh to a similar scale as L and C (0..1)
+                       var dH_normalized:Number = dh / 180.0;
+                       
                        var dL:Number = a[0] - b[0];
                        var dC:Number = a[1] - b[1];
                        
-                       return Math.sqrt(dL*dL + dC*dC + dh*dh);
+                       // Weighted distance: prioritize Hue strongly, then 
Lightness, then Chroma
+                       // This ensures we snap to the correct color family 
(hue) first.
+                       return Math.sqrt(dL*dL + (dC*dC * 0.25) + 
(dH_normalized * dH_normalized * 16.0));
                }
                
                public static function flattenRGBAOverBackground(fg_rgb:Array, 
alpha:Number, bg_rgb:Array = null):Array
@@ -462,7 +525,7 @@ package org.apache.royale.style.colors
                /**
                 * Compute WCAG relative luminance from sRGB (0–255)
                 */
-               private static function relativeLuminance(rgb:Array):Number {
+               public static function relativeLuminance(rgb:Array):Number {
                        function chan(v:Number):Number {
                                v /= 255.0;
                                return (v <= 0.04045) ? (v / 12.92) : 
Math.pow((v + 0.055) / 1.055, 2.4);
@@ -515,33 +578,45 @@ package org.apache.royale.style.colors
                        }
                        
                        var mid:Number;
-                       var safeC:Number;
                        var test:Array;
                        var rgb:Array;
+                       var bestL:Number = wantLight ? 1.0 : 0.0;
                        
                        // 20 iterations = sub‑pixel precision
                        for (var i:int = 0; i < 20; i++) {
                                mid = (low + high) * 0.5;
-                               
-                               // Stability chroma curve (prevents invalid 
OKLCH at extremes)
-                               safeC = C * (1 - Math.abs(mid - 0.5) * 2);
-                               safeC = pinValue(safeC, 0, C);
-                               
-                               test = [mid, safeC, h];
+                               // Be conservative: solve L for achromatic 
foreground contrast: C=0
+                               test = [mid, 0, h];
                                rgb = oklch_ToRGB(test);
                                
                                var cr:Number = contrast(rgb, bgRgb);
                                
-                               if (cr >= 4.5) {
-                                       if (wantLight) high = mid;
-                                       else low = mid;
-                               } else {
-                                       if (wantLight) low = mid;
-                                       else high = mid;
-                               }
+                       if (cr >= 4.5) {
+                               bestL = mid;
+                               if (wantLight) high = mid;
+                               else low = mid;
+                       } else {
+                               if (wantLight) low = mid;
+                               else high = mid;
                        }
-                       
-                       var L_final:Number = mid;
+               }
+               
+               // If we wanted light and didn't reach 4.5, check if pure white 
(L=1) is better than bestL.
+               // If we wanted dark and didn't reach 4.5, check if pure black 
(L=0) is better than bestL.
+               // In JS, sometimes the binary search might stay slightly away 
from the extreme.
+               if (wantLight) {
+                       test = [1.0, 0, h];
+                       if (contrast(oklch_ToRGB(test), bgRgb) > 
contrast(oklch_ToRGB([bestL, 0, h]), bgRgb)) {
+                               bestL = 1.0;
+                       }
+               } else {
+                       test = [0.0, 0, h];
+                       if (contrast(oklch_ToRGB(test), bgRgb) > 
contrast(oklch_ToRGB([bestL, 0, h]), bgRgb)) {
+                               bestL = 0.0;
+                       }
+               }
+               
+               var L_final:Number = bestL;
                        
                        // -----------------------------
                        // 2. BINARY SEARCH FOR CHROMA
@@ -601,8 +676,8 @@ package org.apache.royale.style.colors
                        var bgLch:Array = rgb_ToOKLCH(bgRgb);
                        var L:Number = bgLch[0];
                        
-                       // If background is light, generate dark text; if dark, 
generate light text
-                       var wantLight:Boolean = (L < 0.6);
+                       // If background is light, generate dark contrast; if 
dark, generate light contrast
+                       var wantLight:Boolean = (L < WANT_LIGHT_THRESHOLD);
                        
                        var correctedLch:Array = generateContrastLCH(bgLch, 
wantLight);
                        return oklch_ToRGB(correctedLch);
diff --git 
a/frameworks/projects/Style/src/main/royale/org/apache/royale/style/colors/ThemeColorSet.as
 
b/frameworks/projects/Style/src/main/royale/org/apache/royale/style/colors/ThemeColorSet.as
index 1a7740e8d3..8b691e6d11 100644
--- 
a/frameworks/projects/Style/src/main/royale/org/apache/royale/style/colors/ThemeColorSet.as
+++ 
b/frameworks/projects/Style/src/main/royale/org/apache/royale/style/colors/ThemeColorSet.as
@@ -56,18 +56,38 @@ package org.apache.royale.style.colors
                COMPILE::JS
                private const storage:Map = new Map();
                
-               private const lchLookups:Object = {init:false};
+               private var _includeBlackAndWhite:Boolean = false;
+               public function get includeBlackAndWhite():Boolean{
+                       return _includeBlackAndWhite;
+               }
+               public function set includeBlackAndWhite(value:Boolean):void{
+                       if (_includeBlackAndWhite != value) {
+                               _includeBlackAndWhite = value;
+                               // Reset lookups if they were already 
initialized
+                               if (lchLookups.init !== false) {
+                                       // We need to re-initialize to reflect 
the change in _includeBlackAndWhite
+                                       lchLookups = {init:false};
+                               }
+                       }
+               }
+
+               private var lchLookups:Object = {init:false};
                private function getLCHLookups():Object{
                        if (lchLookups.init === false) {
                                delete lchLookups.init;
                                var key:String;
-                               for each(key in _fieldNames) {
+                               var fieldNames:Array = _fieldNames;
+                               for each(key in fieldNames) {
                                        var baseColor:String = 
getThemeBaseColor(key);
-                                       if (baseColor) {
-                                               var col:uint 
=ColorSwatch.getColorValue(baseColor);
+                                       if (baseColor && 
!lchLookups.hasOwnProperty(baseColor)) {
+                                               var col:uint = 
ColorSwatch.getColorValue(baseColor);
                                                lchLookups[baseColor] = 
ColorUtils.rgb_ToOKLCH([(col>>16)&0xff,(col>>8)&0xff,col&0xff])
                                        }
                                }
+                               if (_includeBlackAndWhite) {
+                                       lchLookups['black'] = [0,0,0]; // Black 
L=0, C=0, H=0
+                                       lchLookups['white'] = [1,0,0]; // White 
L=1, C=0, H=0
+                               }
                        }
                        return lchLookups;
                }
@@ -89,6 +109,10 @@ package org.apache.royale.style.colors
                                COMPILE::JS {
                                        storage.set(key,value);
                                }
+                               // Clear lookups if theme colors change
+                               if (lchLookups.init !== false) {
+                                       lchLookups = {init:false};
+                               }
                        } else {
                                COMPILE::JS {
                                        if (storage.has(key)) 
storage.delete(key);
@@ -187,39 +211,93 @@ package org.apache.royale.style.colors
          * @param limitRange if true then limit lookups to this color set only
          * @return
          */
-               public function findContrastVariant(swatch:String, 
shade:Number,dark:Boolean, weak:Boolean, limitRange:Boolean=false):ColorSwatch{
-                       const lookupKey:String = 
swatch+'$$'+shade+'$$'+dark+'$$'+weak+'$$';
-
+               public function 
findContrastVariant(swatch:String,shade:Number,dark:Boolean,weak:Boolean,limitRange:Boolean
 = false):ColorSwatch
+               {
+                       const lookupKey:String = swatch + "$$" + shade + "$$" + 
dark + "$$" + weak + "$$" + limitRange;
+                       
+                       // Cached?
                        var existing:String = 
contrastLookupSpecifiers[lookupKey];
-                       if (existing) {
-                               
+                       if (existing)
                                return ColorSwatch.fromSpecifier(existing);
-                       }
+                       
+                       // Resolve base color
                        var base:Object = ColorSwatch.getColorValue(swatch) || 
CSSLookup.getProperty(swatch);
                        var baseColor:uint = CSSUtils.toColor(base);
-                       shade = Math.round(shade/10);
-                       var colorVals:Array = 
ColorUtils.getVariation(baseColor,shade,dark);
-                       var bgLch:Array = ColorUtils.rgb_ToOKLCH(colorVals);
-                       var L:Number = bgLch[0];
-
-               //      var wantLight:Boolean = 
(ColorUtils.contrast([255,255,255], colorVals) < ColorUtils.contrast([0,0,0], 
colorVals));
-                       var wantLight:Boolean = (L < .62);
                        
-                       // Strong or weak contrast?
-                       var targetLch:Array = 
ColorUtils.generateContrastLCH(bgLch, wantLight);
-                       if (weak) {
-                               // Weak contrast = reduce chroma + move L 
slightly toward bg
-                               targetLch[1] *= 0.4;     // reduce chroma
-                               targetLch[0] = (targetLch[0] + L) * 0.5; // 
blend toward background
+                       // Tailwind-like shade rounding
+                       shade = Math.round(shade / 10);
+                       
+                       // Background RGB for contrast measurement
+                       var bgRgb:Array = ColorUtils.getVariation(baseColor, 
shade, dark);
+                       
+                       // Background OKLCH
+                       var bgLch:Array = ColorUtils.rgb_ToOKLCH(bgRgb);
+                       var bgL:Number = bgLch[0];
+                       
+                       // Decide whether we want a light or dark foreground
+                       // (L threshold is more stable than RGB contrast 
heuristic)
+                       var wantLight:Boolean = (bgL < 
ColorUtils.WANT_LIGHT_THRESHOLD);
+                       
+                       // --- STEP 1: Try hue‑preserving OKLCH contrast first 
---
+                       var fgLch:Array = ColorUtils.generateContrastLCH(bgLch, 
wantLight);
+                       var fgRgb:Array = ColorUtils.oklch_ToRGB(fgLch);
+                       
+                       // --- STEP 2: If weak contrast requested, soften the 
LCH result ---
+                       if (weak)
+                       {
+                               // Reduce chroma
+                               fgLch[1] *= 0.40;
+                               
+                               // Blend L halfway back toward background
+                               fgLch[0] = (fgLch[0] + bgL) * 0.50;
+                               
+                               // Recompute RGB after weak adjustment
+                               fgRgb = ColorUtils.oklch_ToRGB(fgLch);
                        }
                        
-                       var fg:Array = targetLch;
-                       colorVals = ColorUtils.oklch_ToRGB(fg);
+                       // --- STEP 3: If contrast < 4.5, fallback to 
guaranteed RGB contrast ---
+                       if (!weak) // weak contrast is allowed to be < 4.5
+                       {
+                               if (ColorUtils.contrast(fgRgb, bgRgb) < 4.5)
+                               {
+                                       // WCAG contrast is not guaranteed (may 
be black or white)
+                                       if (wantLight)
+                                               fgRgb = [255,255,255];
+                                       else
+                                               fgRgb = [0,0,0];
+                                       
+                               }
+                       }
+                       
+                       // --- STEP 4: Snap to theme palette if requested ---
                        var lookups:Object = limitRange ? getLCHLookups() : 
null;
-                       var ret:ColorSwatch = 
ColorSwatch.estimateFromRGB(colorVals,lookups);
-                       contrastLookupSpecifiers[lookupKey] = ret.toString()
+                       
+                       // If we are NOT limiting range, we should still use a 
global lookup 
+                       // that EXCLUDES black and white unless explicitly 
asked? 
+                       // Actually, ColorSwatch.estimateFromRGB uses global 
lookups that DO NOT include black/white now.
+                       
+                       var result:ColorSwatch = 
ColorSwatch.estimateFromRGB(fgRgb, lookups);
+                       
+                       // If includeBlackAndWhite is enabled and we wanted 
light/dark, check if black/white would be a better fit
+                       // than what estimateFromRGB returned, especially if it 
didn't snap to black/white but should have.
+                       if (_includeBlackAndWhite && limitRange) {
+                               if (wantLight && result.colorBase != "white") {
+                                       // Check if white is actually better
+                                       if (ColorUtils.contrast([255,255,255], 
bgRgb) > ColorUtils.contrast(result.getRGB(), bgRgb)) {
+                                               result = new 
ColorSwatch("white", 0);
+                                       }
+                               } else if (!wantLight && result.colorBase != 
"black") {
+                                       // Check if black is actually better
+                                       if (ColorUtils.contrast([0,0,0], bgRgb) 
> ColorUtils.contrast(result.getRGB(), bgRgb)) {
+                                               result = new 
ColorSwatch("black", 0);
+                                       }
+                               }
+                       }
 
-                       return ret;
+                       // Cache
+                       contrastLookupSpecifiers[lookupKey] = result.toString();
+                       
+                       return result;
                }
                
                public function fromJSON(obj:Object):void{
@@ -232,6 +310,9 @@ package org.apache.royale.style.colors
                                        case '_baseContentWeak':
                                                _baseContentWeak = obj[key];
                                                break;
+                                       case 'includeBlackAndWhite':
+                                               includeBlackAndWhite = obj[key];
+                                               break;
                                        default:
                                                setThemeColor(key,obj[key]);
                                }
@@ -249,6 +330,7 @@ package org.apache.royale.style.colors
                                }
                                obj['_baseContent'] = _baseContent;
                                obj['_baseContentWeak'] = _baseContentWeak;
+                               obj['includeBlackAndWhite'] = 
_includeBlackAndWhite;
                        }
                        return obj;
                }
diff --git 
a/frameworks/projects/Style/src/main/royale/org/apache/royale/style/util/StyleTheme.as
 
b/frameworks/projects/Style/src/main/royale/org/apache/royale/style/util/StyleTheme.as
index 93021c0bb3..43e5c7bdd9 100644
--- 
a/frameworks/projects/Style/src/main/royale/org/apache/royale/style/util/StyleTheme.as
+++ 
b/frameworks/projects/Style/src/main/royale/org/apache/royale/style/util/StyleTheme.as
@@ -44,7 +44,8 @@ package org.apache.royale.style.util
                        'error': 'red',
                        "base": "slate",
                        "_baseContent" : 'slate-500',
-                       "_baseContentWeak" : 'neutral-400/80'
+                       "_baseContentWeak" : 'neutral-400/80',
+                       'includeBlackAndWhite' : true
                });
 
                public var spacing:Number = 4;
diff --git 
a/frameworks/projects/Style/src/test/royale/FlexUnitRoyaleApplication.mxml 
b/frameworks/projects/Style/src/test/royale/FlexUnitRoyaleApplication.mxml
index b334c0e7ab..d71dfd52a8 100644
--- a/frameworks/projects/Style/src/test/royale/FlexUnitRoyaleApplication.mxml
+++ b/frameworks/projects/Style/src/test/royale/FlexUnitRoyaleApplication.mxml
@@ -35,6 +35,9 @@ limitations under the License.
             }
 
             import flexUnitTests.StyleManagerTest;
+            import flexUnitTests.ColorUtilsContrastTest;
+            import flexUnitTests.ColorUtilsContrastFunctionTest;
+            import flexUnitTests.ThemeColorSetTest;
             
             import org.apache.royale.events.Event;
             import org.apache.royale.test.listeners.CIListener;
@@ -91,7 +94,7 @@ limitations under the License.
                 {
                     core.addEventListener(Event.COMPLETE, 
core_completeHandler);
                     // Insert test classes here. For example:
-                    core.runClasses(StyleManagerTest);
+                    
core.runClasses(StyleManagerTest,ColorUtilsContrastFunctionTest,ColorUtilsContrastTest,ThemeColorSetTest);
                 }
             }
 
diff --git 
a/frameworks/projects/Style/src/test/royale/flexUnitTests/ColorUtilsContrastFunctionTest.as
 
b/frameworks/projects/Style/src/test/royale/flexUnitTests/ColorUtilsContrastFunctionTest.as
new file mode 100644
index 0000000000..5f92639bcc
--- /dev/null
+++ 
b/frameworks/projects/Style/src/test/royale/flexUnitTests/ColorUtilsContrastFunctionTest.as
@@ -0,0 +1,128 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+//  Licensed to the Apache Software Foundation (ASF) under one or more
+//  contributor license agreements.  See the NOTICE file distributed with
+//  this work for additional information regarding copyright ownership.
+//  The ASF licenses this file to You under the Apache License, Version 2.0
+//  (the "License"); you may not use this file except in compliance with
+//  the License.  You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+//  Unless required by applicable law or agreed to in writing, software
+//  distributed under the License is distributed on an "AS IS" BASIS,
+//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//  See the License for the specific language governing permissions and
+//  limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+package flexUnitTests
+{
+       import org.apache.royale.style.colors.ColorUtils;
+       import org.apache.royale.test.asserts.*;
+       
+       public class ColorUtilsContrastFunctionTest
+       {
+               [Before] public function setUp():void {}
+               [After] public function tearDown():void {}
+               [BeforeClass] public static function setUpBeforeClass():void {}
+               [AfterClass] public static function tearDownAfterClass():void {}
+               
+               
+               
+               // -------------------------------------------------------------
+               // 1. Relative luminance correctness
+               // -------------------------------------------------------------
+               [Test]
+               public function testRelativeLuminance_PureColors():void
+               {
+                       // Expected WCAG luminances
+                       // White: 1.0
+                       // Black: 0.0
+                       // Red:   0.2126
+                       // Green: 0.7152
+                       // Blue:  0.0722
+                       
+                       
assertCloseTo(ColorUtils.relativeLuminance([255,255,255]), 1.0, 0.0001, "White 
luminance");
+                       assertCloseTo(ColorUtils.relativeLuminance([0,0,0]),    
   0.0, 0.0001, "Black luminance");
+                       
+                       assertCloseTo(ColorUtils.relativeLuminance([255,0,0]),  
 0.2126, 0.0001, "Red luminance");
+                       assertCloseTo(ColorUtils.relativeLuminance([0,255,0]),  
 0.7152, 0.0001, "Green luminance");
+                       assertCloseTo(ColorUtils.relativeLuminance([0,0,255]),  
 0.0722, 0.0001, "Blue luminance");
+               }
+               
+               // -------------------------------------------------------------
+               // 2. Known WCAG contrast ratios
+               // -------------------------------------------------------------
+               [Test]
+               public function testContrast_KnownPairs():void
+               {
+                       // White vs Black = 21.0
+                       assertCloseTo(
+                                       ColorUtils.contrast([255,255,255], 
[0,0,0]),
+                                       21.0, 0.001,
+                                       "White/Black contrast"
+                       );
+                       
+                       // White vs mid-gray (#777777)
+                       // Expected ≈ 4.48
+                       assertCloseTo(
+                                       ColorUtils.contrast([255,255,255], 
[119,119,119]),
+                                       4.49, 0.05,
+                                       "White vs #777777"
+                       );
+                       
+                       // Black vs mid-gray (#777777)
+                       // Expected ≈ 5.32
+                       assertCloseTo(
+                                       ColorUtils.contrast([0,0,0], 
[119,119,119]),
+                                       4.69, 0.05,
+                                       "Black vs #777777"
+                       );
+               }
+               
+               // -------------------------------------------------------------
+               // 3. Symmetry: contrast(a,b) == contrast(b,a)
+               // -------------------------------------------------------------
+               [Test]
+               public function testContrast_Symmetry():void
+               {
+                       var a:Array = [200, 50, 100];
+                       var b:Array = [20, 180, 220];
+                       
+                       var ab:Number = ColorUtils.contrast(a, b);
+                       var ba:Number = ColorUtils.contrast(b, a);
+                       
+                       assertCloseTo(ab, ba, 0.000001, "Contrast must be 
symmetric");
+               }
+               
+               // -------------------------------------------------------------
+               // 4. Monotonicity: lighter colors produce higher contrast
+               // -------------------------------------------------------------
+               [Test]
+               public function testContrast_Monotonicity():void
+               {
+                       var black:Array = [0,0,0];
+                       
+                       var c1:Number = ColorUtils.contrast([50,50,50], black);
+                       var c2:Number = ColorUtils.contrast([120,120,120], 
black);
+                       var c3:Number = ColorUtils.contrast([200,200,200], 
black);
+                       
+                       assertTrue(c1 < c2, "Contrast should increase with 
luminance");
+                       assertTrue(c2 < c3, "Contrast should increase with 
luminance");
+               }
+               
+               // -------------------------------------------------------------
+               // 5. Edge cases: identical colors → contrast = 1.0
+               // -------------------------------------------------------------
+               [Test]
+               public function testContrast_IdenticalColors():void
+               {
+                       assertCloseTo(
+                                       ColorUtils.contrast([100,100,100], 
[100,100,100]),
+                                       1.0, 0.0001,
+                                       "Identical colors must have contrast 
1.0"
+                       );
+               }
+       }
+}
diff --git 
a/frameworks/projects/Style/src/test/royale/flexUnitTests/ColorUtilsContrastTest.as
 
b/frameworks/projects/Style/src/test/royale/flexUnitTests/ColorUtilsContrastTest.as
new file mode 100644
index 0000000000..d28187e500
--- /dev/null
+++ 
b/frameworks/projects/Style/src/test/royale/flexUnitTests/ColorUtilsContrastTest.as
@@ -0,0 +1,295 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+//  Licensed to the Apache Software Foundation (ASF) under one or more
+//  contributor license agreements.  See the NOTICE file distributed with
+//  this work for additional information regarding copyright ownership.
+//  The ASF licenses this file to You under the Apache License, Version 2.0
+//  (the "License"); you may not use this file except in compliance with
+//  the License.  You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+//  Unless required by applicable law or agreed to in writing, software
+//  distributed under the License is distributed on an "AS IS" BASIS,
+//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//  See the License for the specific language governing permissions and
+//  limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+package flexUnitTests
+{
+       import org.apache.royale.style.colors.ColorUtils;
+       import org.apache.royale.test.asserts.*;
+       
+       public class ColorUtilsContrastTest
+       {
+               [Before] public function setUp():void {}
+               [After] public function tearDown():void {}
+               [BeforeClass] public static function setUpBeforeClass():void {}
+               [AfterClass] public static function tearDownAfterClass():void {}
+               
+               private function contrast(rgb1:Array, rgb2:Array):Number
+               {
+                       return ColorUtils.contrast(rgb1, rgb2);
+               }
+               
+               private function toRGB(lch:Array):Array
+               {
+                       return ColorUtils.oklch_ToRGB(lch);
+               }
+               
+               private function toLCH(rgb:Array):Array
+               {
+                       return ColorUtils.rgb_ToOKLCH(rgb);
+               }
+               
+               // -------------------------------------------------------------
+               // 1. Very Dark Background (navy)
+               // -------------------------------------------------------------
+               [Test]
+               public function testVeryDarkBackground():void
+               {
+                       var bg:Array = [0.08, 0.12, 260]; // navy blue
+                       var wantLight:Boolean = true;
+                       
+                       var fg:Array = ColorUtils.generateContrastLCH(bg, 
wantLight);
+                       
+                       assertTrue(fg[0] > bg[0], "Foreground should be 
lighter");
+                       
+                       if (fg[1] > 0.02)
+                       {
+                               var dh:Number = Math.abs(fg[2] - bg[2]);
+                               dh = Math.min(dh, 360 - dh);
+                               assertTrue(dh < 5.0, "Hue should be preserved");
+                       }
+                       
+                       assertTrue(contrast(toRGB(fg), toRGB(bg)) >= 4.5);
+               }
+               
+               // -------------------------------------------------------------
+               // 2. Very Light Background (pastel warm green)
+               // -------------------------------------------------------------
+               [Test]
+               public function testVeryLightBackground():void
+               {
+                       var bg:Array = [0.96, 0.03, 100];
+                       var wantLight:Boolean = false;
+                       
+                       var fg:Array = ColorUtils.generateContrastLCH(bg, 
wantLight);
+                       
+                       assertTrue(fg[0] < bg[0], "Foreground should be 
darker");
+                       
+                       if (fg[1] > 0.02)
+                       {
+                               var dh:Number = Math.abs(fg[2] - bg[2]);
+                               dh = Math.min(dh, 360 - dh);
+                               assertTrue(dh < 5.0);
+                       }
+                       
+                       assertTrue(contrast(toRGB(fg), toRGB(bg)) >= 4.5);
+               }
+               
+               // -------------------------------------------------------------
+               // 3. Mid‑tone Saturated Blue (blue‑500‑like)
+               // -------------------------------------------------------------
+               [Test]
+               public function testMidToneBlue():void
+               {
+                       var bg:Array = [0.55, 0.20, 250];
+                       var wantLight:Boolean = false;
+                       
+                       var fg:Array = ColorUtils.generateContrastLCH(bg, 
wantLight);
+                       
+                       assertTrue(fg[0] < bg[0], "Foreground should be 
darker");
+                       
+                       if (fg[1] > 0.02)
+                       {
+                               var dh:Number = Math.abs(fg[2] - bg[2]);
+                               dh = Math.min(dh, 360 - dh);
+                               assertTrue(dh < 5.0);
+                       }
+                       // Soft sanity check only – not a hard 4.5 requirement
+                       assertTrue(contrast(toRGB(fg), toRGB(bg)) >= 3.0, 
"Contrast should be reasonably high");
+               }
+               
+               //rgb version api
+               [Test]
+               public function testChooseContrastRGB_MidToneBlue():void
+               {
+                       var bgLch:Array = [0.55, 0.20, 250];
+                       var bgRgb:Array = toRGB(bgLch);
+                       
+                       var fgRgb:Array = ColorUtils.chooseContrastRGB(bgRgb);
+                       
+                       assertTrue(contrast(fgRgb, bgRgb) >= 4.5, "UI contrast 
must be >= 4.5");
+               }
+               
+               [Test]
+               public function testMidToneBlue2():void
+               {
+                       // There is no light foreground that can reach 4.5:1 on 
this bg.
+                       var bg:Array = [0.663, 0.266, 255.3];
+                       var wantLight:Boolean = true;
+                       
+                       var fg:Array = ColorUtils.generateContrastLCH(bg, 
wantLight);
+                       
+                       // Foreground must be lighter
+                       assertTrue(fg[0] > bg[0], "Foreground should be 
lighter");
+                       
+                       // If chroma is significant, hue must be preserved
+                       if (fg[1] > 0.02)
+                       {
+                               var dh:Number = Math.abs(fg[2] - bg[2]);
+                               dh = Math.min(dh, 360 - dh);
+                               assertTrue(dh < 5.0, "Hue should be preserved");
+                       }
+                       
+                       // We expect the solver to return the lightest possible 
(white),
+                       // even though contrast will be < 4.5.
+                       var cr:Number = ColorUtils.contrast(
+                                       ColorUtils.oklch_ToRGB(fg),
+                                       ColorUtils.oklch_ToRGB(bg)
+                       );
+                       assertTrue(cr < 4.5);
+                       assertEquals(1, fg[0]);   // L = 1
+                       assertEquals(0, fg[1]);   // C = 0
+                       
+               }
+               
+               // -------------------------------------------------------------
+               // 4. Mid‑tone Neutral Gray
+               // -------------------------------------------------------------
+               [Test]
+               public function testNeutralGray():void
+               {
+                       var bg:Array = [0.55, 0.005, 0]; // mid‑tone neutral 
gray
+                       var wantLight:Boolean = false;
+                       
+                       var fg:Array = ColorUtils.generateContrastLCH(bg, 
wantLight);
+                       
+                       assertTrue(fg[0] < bg[0], "Foreground should be 
darker");
+                       assertTrue(fg[1] <= bg[1] + 0.03, "Chroma should remain 
low");
+                       // Soft sanity check only
+                       assertTrue(contrast(toRGB(fg), toRGB(bg)) >= 3.0, 
"Contrast should be reasonably high");
+               }
+               
+               //rgb version api
+               [Test]
+               public function testChooseContrastRGB_NeutralGray():void
+               {
+                       var bgLch:Array = [0.55, 0.005, 0]; // mid‑tone neutral 
gray
+                       var bgRgb:Array = toRGB(bgLch);
+                       
+                       var fgRgb:Array = ColorUtils.chooseContrastRGB(bgRgb);
+                       
+                       assertTrue(contrast(fgRgb, bgRgb) >= 4.5, "UI contrast 
must be >= 4.5");
+               }
+               
+               // -------------------------------------------------------------
+               // 5. Highly Saturated Red
+               // -------------------------------------------------------------
+               [Test]
+               public function testSaturatedRed():void
+               {
+                       var bg:Array = [0.60, 0.28, 25];
+                       var wantLight:Boolean = false;
+                       
+                       var fg:Array = ColorUtils.generateContrastLCH(bg, 
wantLight);
+                       
+                       assertTrue(fg[0] < bg[0], "Foreground should be 
darker");
+                       assertTrue(fg[1] < bg[1], "Chroma should be reduced");
+                       
+                       assertTrue(contrast(toRGB(fg), toRGB(bg)) >= 4.5);
+               }
+               
+               // -------------------------------------------------------------
+               // 6. Low‑Chroma Pastel Green
+               // -------------------------------------------------------------
+               [Test]
+               public function testPastelGreen():void
+               {
+                       var bg:Array = [0.88, 0.04, 130];
+                       var wantLight:Boolean = false;
+                       
+                       var fg:Array = ColorUtils.generateContrastLCH(bg, 
wantLight);
+                       
+                       assertTrue(fg[0] < bg[0], "Foreground should be 
darker");
+                       assertTrue(fg[1] <= 0.05, "Chroma should remain low");
+                       
+                       assertTrue(contrast(toRGB(fg), toRGB(bg)) >= 4.5);
+               }
+               
+               // -------------------------------------------------------------
+               // 7. Black Background
+               // -------------------------------------------------------------
+               [Test]
+               public function testBlackBackground():void
+               {
+                       var bg:Array = [0.00, 0.00, 0];
+                       var wantLight:Boolean = true;
+                       
+                       var fg:Array = ColorUtils.generateContrastLCH(bg, 
wantLight);
+                       
+                       assertTrue(fg[0] > 0.50, "Foreground should be 
lighter");
+                       assertTrue(fg[1] <= 0.02, "Chroma should remain near 
zero");
+                       
+                       assertTrue(contrast(toRGB(fg), toRGB(bg)) >= 4.5);
+               }
+               
+               // -------------------------------------------------------------
+               // 8. White Background
+               // -------------------------------------------------------------
+               [Test]
+               public function testWhiteBackground():void
+               {
+                       var bg:Array = [0.97, 0.00, 0]; // corrected white
+                       var wantLight:Boolean = false;
+                       
+                       var fg:Array = ColorUtils.generateContrastLCH(bg, 
wantLight);
+                       
+                       assertTrue(fg[0] < bg[0], "Foreground should be 
darker");
+                       assertTrue(fg[1] <= 0.02, "Chroma should remain near 
zero");
+                       
+                       assertTrue(contrast(toRGB(fg), toRGB(bg)) >= 4.5);
+               }
+               
+               // -------------------------------------------------------------
+               // 9. Hue Preservation Across Multiple Hues
+               // -------------------------------------------------------------
+               [Test]
+               public function testHuePreservation():void
+               {
+                       var hues:Array = [0, 30, 60, 120, 180, 240, 300];
+                       
+                       for each (var h:Number in hues)
+                       {
+                               var bg:Array = [0.50, 0.12, h]; // gamut‑safe 
chroma
+                               var wantLight:Boolean = false;
+                               
+                               var fg:Array = 
ColorUtils.generateContrastLCH(bg, wantLight);
+                               
+                               if (fg[1] > 0.02)
+                               {
+                                       var dh:Number = Math.abs(fg[2] - h);
+                                       dh = Math.min(dh, 360 - dh);
+                                       assertTrue(dh < 5.0, "Hue should be 
preserved for h=" + h);
+                               }
+                       }
+               }
+               
+               // -------------------------------------------------------------
+               // 10. Round‑Trip OKLCH Validity
+               // -------------------------------------------------------------
+               [Test]
+               public function testRoundTripStability():void
+               {
+                       var bg:Array = [0.40, 0.15, 200]; // gamut‑safe chroma
+                       var wantLight:Boolean = true;
+                       
+                       var fg:Array = ColorUtils.generateContrastLCH(bg, 
wantLight);
+                       var rt:Array = toLCH(toRGB(fg));
+                       
+                       assertTrue(Math.abs(rt[0] - fg[0]) < 0.02);
+               }
+       }
+}
diff --git 
a/frameworks/projects/Style/src/test/royale/flexUnitTests/ThemeColorSetTest.as 
b/frameworks/projects/Style/src/test/royale/flexUnitTests/ThemeColorSetTest.as
new file mode 100644
index 0000000000..781ad93937
--- /dev/null
+++ 
b/frameworks/projects/Style/src/test/royale/flexUnitTests/ThemeColorSetTest.as
@@ -0,0 +1,206 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+//  Licensed to the Apache Software Foundation (ASF) under one or more
+//  contributor license agreements.  See the NOTICE file distributed with
+//  this work for additional information regarding copyright ownership.
+//  The ASF licenses this file to You under the Apache License, Version 2.0
+//  (the "License"); you may not use this file except in compliance with
+//  the License.  You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+//  Unless required by applicable law or agreed to in writing, software
+//  distributed under the License is distributed on an "AS IS" BASIS,
+//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//  See the License for the specific language governing permissions and
+//  limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+package flexUnitTests
+{
+       import org.apache.royale.style.colors.ThemeColorSet;
+       import org.apache.royale.style.colors.ColorSwatch;
+       import org.apache.royale.style.colors.ColorUtils;
+       import org.apache.royale.test.asserts.*;
+       import org.apache.royale.utils.CSSUtils;
+
+       /**
+        * Tests for ThemeColorSet contrast and variant resolution logic.
+        */
+       public class ThemeColorSetTest
+       {               
+               private var themeSet:ThemeColorSet;
+
+               [Before]
+               public function setUp():void
+               {
+                       // Default theme-like configuration
+                       themeSet = new ThemeColorSet({
+                               'primary': 'blue',
+                               'secondary': 'rose',
+                               'neutral': 'slate'
+                       });
+               }
+
+               [After]
+               public function tearDown():void
+               {
+                       themeSet = null;
+               }
+               
+               
+               public function getRGBColor(swatch:ColorSwatch):uint
+               {
+                       var rgb:Array = swatch.getRGB();
+                       if (!rgb || rgb.length < 3) return 0;
+                       var rr:uint = rgb[0];
+                       var gg:uint = rgb[1];
+                       var bb:uint = rgb[2];
+                       return (rr << 16) | (gg << 8) | bb;
+               }
+
+               // 
---------------------------------------------------------------------
+               // 1. Basic Contrast Resolution
+               // 
---------------------------------------------------------------------
+
+               [Test]
+               public function 
testFindContrastVariant_Strong_DarkBackground():void
+               {
+                       // Slate-900 is very dark
+                       var result:ColorSwatch = 
themeSet.findContrastVariant("slate", 900, false, false);
+                       
+                       // Should result in a light color
+                       var bgRgb:Array = 
ColorUtils.getVariation(CSSUtils.toColor(ColorSwatch.getColorValue("slate")), 
90, false);
+                       var fgRgb:Array = result.getRGB();
+                       
+                       var cr:Number = ColorUtils.contrast(fgRgb, bgRgb);
+                       assertTrue(cr >= 4.5, "Contrast ratio must be at least 
4.5 for strong variant. Got: " + cr);
+                       
+                       // On dark background, it should be significantly 
lighter
+                       assertTrue(ColorUtils.relativeLuminance(fgRgb) > 
ColorUtils.relativeLuminance(bgRgb), "Foreground should be lighter than 
background");
+               }
+
+               [Test]
+               public function 
testFindContrastVariant_Strong_LightBackground():void
+               {
+                       // Slate-50 is very light
+                       var result:ColorSwatch = 
themeSet.findContrastVariant("slate", 50, false, false);
+                       
+                       var bgRgb:Array = 
ColorUtils.getVariation(CSSUtils.toColor(ColorSwatch.getColorValue("slate")), 
5, false);
+                       var fgRgb:Array = result.getRGB();
+                       
+                       var cr:Number = ColorUtils.contrast(fgRgb, bgRgb);
+                       assertTrue(cr >= 4.5, "Contrast ratio must be at least 
4.5. Got: " + cr);
+                       
+                       // On light background, it should be significantly 
darker
+                       assertTrue(ColorUtils.relativeLuminance(fgRgb) < 
ColorUtils.relativeLuminance(bgRgb), "Foreground should be darker than 
background");
+               }
+
+               // 
---------------------------------------------------------------------
+               // 2. Weak Contrast Resolution
+               // 
---------------------------------------------------------------------
+
+               [Test]
+               public function testFindContrastVariant_Weak():void
+               {
+                       var strong:ColorSwatch = 
themeSet.findContrastVariant("blue", 500, false, false);
+                       var weak:ColorSwatch = 
themeSet.findContrastVariant("blue", 500, false, true);
+                       
+                       var bgRgb:Array = 
ColorUtils.getVariation(CSSUtils.toColor(ColorSwatch.getColorValue("blue")), 
50, false);
+                       var strongRgb:Array = strong.getRGB();
+                       var weakRgb:Array = weak.getRGB();
+                       
+                       var strongCr:Number = ColorUtils.contrast(strongRgb, 
bgRgb);
+                       var weakCr:Number = ColorUtils.contrast(weakRgb, bgRgb);
+                       
+                       // Weak contrast should be lower than strong contrast, 
but still provide some separation
+                       assertTrue(weakCr < strongCr, "Weak contrast should be 
less than strong contrast");
+                       assertTrue(weakCr > 1.2, "Weak contrast should still be 
visible (> 1.2)");
+               }
+
+               // 
---------------------------------------------------------------------
+               // 3. Palette Limitation (limitRange)
+               // 
---------------------------------------------------------------------
+
+               [Test]
+               public function testFindContrastVariant_LimitRange():void
+               {
+                       // If we limit range, the result MUST be one of the 
base colors in the ThemeColorSet (primary, secondary, neutral, etc.)
+                       // or their variations.
+                       var result:ColorSwatch = 
themeSet.findContrastVariant("primary", 500, false, false, true);
+                       
+                       // The result swatch's base name should be found in our 
theme set keys
+                       var validBases:Array = ["blue", "rose", "slate"]; // 
derived from setUp
+                       // Also allow "neutral" if it was used as default
+                       validBases.push("neutral");
+                       var resultBase:String = result.colorBase;
+                       
+                       assertTrue(validBases.indexOf(resultBase) != -1, 
"Result base '" + resultBase + "' should be one of the theme bases: " + 
validBases.join(", "));
+               }
+
+               [Test]
+               public function 
testFindContrastVariant_IncludeBlackAndWhite():void
+               {
+                       themeSet.includeBlackAndWhite = true;
+                       
+                       // 1. Test Light Background -> Should snap to BLACK
+                       // Slate-50 is very light (L ~ 0.95)
+                       var resultBlack:ColorSwatch = 
themeSet.findContrastVariant("slate", 50, false, false, true);
+                       assertEquals("black", resultBlack.colorBase, "Should 
snap to 'black' for light background when enabled");
+                       assertEquals(0x000000, getRGBColor(resultBlack), "Black 
swatch should be 0x000000");
+
+                       // 2. Test Dark Background -> Should snap to WHITE
+                       // Slate-900 is very dark (L ~ 0.1)
+                       var resultWhite:ColorSwatch = 
themeSet.findContrastVariant("slate", 900, false, false, true);
+                       assertEquals("white", resultWhite.colorBase, "Should 
snap to 'white' for dark background when enabled");
+                       assertEquals(0xFFFFFF, getRGBColor(resultWhite), "White 
swatch should be 0xFFFFFF");
+               }
+
+               [Test]
+               public function 
testFindContrastVariant_ExcludeBlackAndWhite():void
+               {
+                       themeSet.includeBlackAndWhite = false; // Default
+                       
+                       // Even on very light background, it should NOT snap to 
black if limited and not included
+                       var result:ColorSwatch = 
themeSet.findContrastVariant("slate", 50, false, false, true);
+                       
+                       assertFalse(result.colorBase == "black", "Should NOT 
snap to 'black' when includeBlackAndWhite is false");
+                       assertFalse(result.colorBase == "white", "Should NOT 
snap to 'white' when includeBlackAndWhite is false");
+                       
+                       var validBases:Array = ["blue", "rose", "slate", 
"neutral"];
+                       assertTrue(validBases.indexOf(result.colorBase) != -1, 
"Result base '" + result.colorBase + "' should be one of the theme bases");
+               }
+
+               // 
---------------------------------------------------------------------
+               // 4. Hue Preservation Check
+               // 
---------------------------------------------------------------------
+
+               [Test]
+               public function testContrast_HuePreservation():void
+               {
+                       // Use a shade where hue preservation is actually 
possible
+                       var result:ColorSwatch = 
themeSet.findContrastVariant("blue", 300, false, false);
+                       
+                       var bgRgb:Array = ColorUtils.getVariation(
+                                       
CSSUtils.toColor(ColorSwatch.getColorValue("blue")),
+                                       30,   // 300 → 30 after /10 rounding
+                                       false
+                       );
+                       
+                       var bgLch:Array = ColorUtils.rgb_ToOKLCH(bgRgb);
+                       var fgLch:Array = 
ColorUtils.rgb_ToOKLCH(result.getRGB());
+                       
+                       // If chroma is tiny, hue is meaningless → skip
+                       if (fgLch[1] <= 0.01)
+                               return; // this is the case for the current test
+                       
+                       // Compute hue difference with wrap-around
+                       var hueDiff:Number = Math.abs(bgLch[2] - fgLch[2]);
+                       if (hueDiff > 180) hueDiff = 360 - hueDiff;
+                       
+                       // Now hue preservation is expected
+                       assertTrue(hueDiff < 30,
+                                       "Contrast color should preserve 
background hue within 30 degrees. Diff: " + hueDiff);
+               }
+       }
+}


Reply via email to