@afixt/test-utils 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/CLAUDE.md +1 -1
  3. package/README.md +40 -1
  4. package/docs/arrayUtils.js.html +8 -13
  5. package/docs/colorConversions.js.html +238 -0
  6. package/docs/constants.js.html +671 -0
  7. package/docs/cssUtils.js.html +80 -0
  8. package/docs/data/search.json +1 -1
  9. package/docs/domUtils.js.html +252 -32
  10. package/docs/formUtils.js.html +161 -0
  11. package/docs/getAccessibleName.js.html +215 -120
  12. package/docs/getAccessibleText.js.html +103 -48
  13. package/docs/getAriaAttributesByElement.js.html +4 -4
  14. package/docs/getCSSGeneratedContent.js.html +50 -41
  15. package/docs/getComputedRole.js.html +8 -3
  16. package/docs/getFocusableElements.js.html +25 -21
  17. package/docs/getGeneratedContent.js.html +24 -13
  18. package/docs/getImageText.js.html +31 -9
  19. package/docs/getStyleObject.js.html +7 -3
  20. package/docs/global.html +1 -1
  21. package/docs/hasAccessibleName.js.html +2 -10
  22. package/docs/hasAttribute.js.html +7 -3
  23. package/docs/hasCSSGeneratedContent.js.html +18 -14
  24. package/docs/hasHiddenParent.js.html +4 -4
  25. package/docs/hasParent.js.html +7 -3
  26. package/docs/hasValidAriaAttributes.js.html +7 -3
  27. package/docs/index.html +1 -1
  28. package/docs/index.js.html +98 -32
  29. package/docs/isA11yVisible.js.html +98 -0
  30. package/docs/isAriaAttributesValid.js.html +10 -64
  31. package/docs/isComplexTable.js.html +13 -6
  32. package/docs/isDataTable.js.html +11 -6
  33. package/docs/isFocusable.js.html +36 -12
  34. package/docs/isHidden.js.html +47 -11
  35. package/docs/isOffScreen.js.html +7 -3
  36. package/docs/isValidUrl.js.html +7 -3
  37. package/docs/listEventListeners.js.html +203 -0
  38. package/docs/module-QueryCache.html +3 -0
  39. package/docs/module-afixt-test-utils.html +1 -1
  40. package/docs/module-colorConversions.html +3 -0
  41. package/docs/module-constants.html +3 -0
  42. package/docs/module-cssUtils.html +3 -0
  43. package/docs/module-formUtils.html +3 -0
  44. package/docs/module-suggestContrast.html +3 -0
  45. package/docs/module-tableUtils.html +3 -0
  46. package/docs/queryCache.js.html +360 -0
  47. package/docs/scripts/core.js +726 -726
  48. package/docs/scripts/core.min.js +22 -22
  49. package/docs/scripts/resize.js +90 -90
  50. package/docs/scripts/search.js +265 -265
  51. package/docs/scripts/third-party/Apache-License-2.0.txt +202 -202
  52. package/docs/scripts/third-party/fuse.js +8 -8
  53. package/docs/scripts/third-party/hljs-line-num-original.js +369 -369
  54. package/docs/scripts/third-party/hljs-original.js +5171 -5171
  55. package/docs/scripts/third-party/popper.js +5 -5
  56. package/docs/scripts/third-party/tippy.js +1 -1
  57. package/docs/scripts/third-party/tocbot.js +671 -671
  58. package/docs/styles/clean-jsdoc-theme-base.css +1159 -1159
  59. package/docs/styles/clean-jsdoc-theme-dark.css +412 -412
  60. package/docs/styles/clean-jsdoc-theme-light.css +482 -482
  61. package/docs/styles/clean-jsdoc-theme-scrollbar.css +29 -29
  62. package/docs/suggestContrast.js.html +389 -0
  63. package/docs/tableUtils.js.html +151 -0
  64. package/docs/testContrast.js.html +201 -24
  65. package/docs/testLang.js.html +533 -451
  66. package/docs/testOrder.js.html +9 -4
  67. package/package.json +1 -1
  68. package/src/colorConversions.js +235 -0
  69. package/src/index.js +6 -0
  70. package/src/stringUtils.js +35 -0
  71. package/src/suggestContrast.js +386 -0
  72. package/test/colorConversions.test.js +223 -0
  73. package/test/stringUtils.test.js +60 -0
  74. package/test/suggestContrast.test.js +394 -0
@@ -1,30 +1,30 @@
1
- ::-webkit-scrollbar {
2
- height: 0.3125rem;
3
- width: 0.3125rem;
4
-
5
- }
6
-
7
- ::-webkit-scrollbar-thumb,
8
- ::-webkit-scrollbar-track {
9
- border-radius: 1rem;
10
- }
11
-
12
- ::-webkit-scrollbar-track {
13
- background: #333;
14
- }
15
-
16
- ::-webkit-scrollbar-thumb {
17
- background: #555;
18
- outline: 0.06125rem solid #555;
19
- }
20
-
21
-
22
- .light ::-webkit-scrollbar-track {
23
- background: #ddd;
24
-
25
- }
26
-
27
- .light ::-webkit-scrollbar-thumb {
28
- background: #aaa;
29
- outline: 0.06125rem solid #aaa;
1
+ ::-webkit-scrollbar {
2
+ height: 0.3125rem;
3
+ width: 0.3125rem;
4
+
5
+ }
6
+
7
+ ::-webkit-scrollbar-thumb,
8
+ ::-webkit-scrollbar-track {
9
+ border-radius: 1rem;
10
+ }
11
+
12
+ ::-webkit-scrollbar-track {
13
+ background: #333;
14
+ }
15
+
16
+ ::-webkit-scrollbar-thumb {
17
+ background: #555;
18
+ outline: 0.06125rem solid #555;
19
+ }
20
+
21
+
22
+ .light ::-webkit-scrollbar-track {
23
+ background: #ddd;
24
+
25
+ }
26
+
27
+ .light ::-webkit-scrollbar-thumb {
28
+ background: #aaa;
29
+ outline: 0.06125rem solid #aaa;
30
30
  }
@@ -0,0 +1,389 @@
1
+ <!DOCTYPE html><html lang="en" style="font-size:16px"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Source: suggestContrast.js</title><!--[if lt IE 9]>
2
+ <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
3
+ <![endif]--><script src="scripts/third-party/hljs.js" defer="defer"></script><script src="scripts/third-party/hljs-line-num.js" defer="defer"></script><script src="scripts/third-party/popper.js" defer="defer"></script><script src="scripts/third-party/tippy.js" defer="defer"></script><script src="scripts/third-party/tocbot.min.js"></script><script>var baseURL="/",locationPathname="";baseURL=(locationPathname=document.location.pathname).substr(0,locationPathname.lastIndexOf("/")+1)</script><link rel="stylesheet" href="styles/clean-jsdoc-theme.min.css"><svg aria-hidden="true" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="display:none"><defs><symbol id="copy-icon" viewbox="0 0 488.3 488.3"><g><path d="M314.25,85.4h-227c-21.3,0-38.6,17.3-38.6,38.6v325.7c0,21.3,17.3,38.6,38.6,38.6h227c21.3,0,38.6-17.3,38.6-38.6V124 C352.75,102.7,335.45,85.4,314.25,85.4z M325.75,449.6c0,6.4-5.2,11.6-11.6,11.6h-227c-6.4,0-11.6-5.2-11.6-11.6V124 c0-6.4,5.2-11.6,11.6-11.6h227c6.4,0,11.6,5.2,11.6,11.6V449.6z"/><path d="M401.05,0h-227c-21.3,0-38.6,17.3-38.6,38.6c0,7.5,6,13.5,13.5,13.5s13.5-6,13.5-13.5c0-6.4,5.2-11.6,11.6-11.6h227 c6.4,0,11.6,5.2,11.6,11.6v325.7c0,6.4-5.2,11.6-11.6,11.6c-7.5,0-13.5,6-13.5,13.5s6,13.5,13.5,13.5c21.3,0,38.6-17.3,38.6-38.6 V38.6C439.65,17.3,422.35,0,401.05,0z"/></g></symbol><symbol id="search-icon" viewBox="0 0 512 512"><g><g><path d="M225.474,0C101.151,0,0,101.151,0,225.474c0,124.33,101.151,225.474,225.474,225.474 c124.33,0,225.474-101.144,225.474-225.474C450.948,101.151,349.804,0,225.474,0z M225.474,409.323 c-101.373,0-183.848-82.475-183.848-183.848S124.101,41.626,225.474,41.626s183.848,82.475,183.848,183.848 S326.847,409.323,225.474,409.323z"/></g></g><g><g><path d="M505.902,476.472L386.574,357.144c-8.131-8.131-21.299-8.131-29.43,0c-8.131,8.124-8.131,21.306,0,29.43l119.328,119.328 c4.065,4.065,9.387,6.098,14.715,6.098c5.321,0,10.649-2.033,14.715-6.098C514.033,497.778,514.033,484.596,505.902,476.472z"/></g></g></symbol><symbol id="font-size-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M11.246 15H4.754l-2 5H.6L7 4h2l6.4 16h-2.154l-2-5zm-.8-2L8 6.885 5.554 13h4.892zM21 12.535V12h2v8h-2v-.535a4 4 0 1 1 0-6.93zM19 18a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></symbol><symbol id="add-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M11 11V5h2v6h6v2h-6v6h-2v-6H5v-2z"/></symbol><symbol id="minus-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M5 11h14v2H5z"/></symbol><symbol id="dark-theme-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M10 7a7 7 0 0 0 12 4.9v.1c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2h.1A6.979 6.979 0 0 0 10 7zm-6 5a8 8 0 0 0 15.062 3.762A9 9 0 0 1 8.238 4.938 7.999 7.999 0 0 0 4 12z"/></symbol><symbol id="light-theme-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 18a6 6 0 1 1 0-12 6 6 0 0 1 0 12zm0-2a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM11 1h2v3h-2V1zm0 19h2v3h-2v-3zM3.515 4.929l1.414-1.414L7.05 5.636 5.636 7.05 3.515 4.93zM16.95 18.364l1.414-1.414 2.121 2.121-1.414 1.414-2.121-2.121zm2.121-14.85l1.414 1.415-2.121 2.121-1.414-1.414 2.121-2.121zM5.636 16.95l1.414 1.414-2.121 2.121-1.414-1.414 2.121-2.121zM23 11v2h-3v-2h3zM4 11v2H1v-2h3z"/></symbol><symbol id="reset-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M18.537 19.567A9.961 9.961 0 0 1 12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10c0 2.136-.67 4.116-1.81 5.74L17 12h3a8 8 0 1 0-2.46 5.772l.997 1.795z"/></symbol><symbol id="down-icon" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M12.7803 6.21967C13.0732 6.51256 13.0732 6.98744 12.7803 7.28033L8.53033 11.5303C8.23744 11.8232 7.76256 11.8232 7.46967 11.5303L3.21967 7.28033C2.92678 6.98744 2.92678 6.51256 3.21967 6.21967C3.51256 5.92678 3.98744 5.92678 4.28033 6.21967L8 9.93934L11.7197 6.21967C12.0126 5.92678 12.4874 5.92678 12.7803 6.21967Z"></path></symbol><symbol id="codepen-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M16.5 13.202L13 15.535v3.596L19.197 15 16.5 13.202zM14.697 12L12 10.202 9.303 12 12 13.798 14.697 12zM20 10.869L18.303 12 20 13.131V10.87zM19.197 9L13 4.869v3.596l3.5 2.333L19.197 9zM7.5 10.798L11 8.465V4.869L4.803 9 7.5 10.798zM4.803 15L11 19.131v-3.596l-3.5-2.333L4.803 15zM4 13.131L5.697 12 4 10.869v2.262zM2 9a1 1 0 0 1 .445-.832l9-6a1 1 0 0 1 1.11 0l9 6A1 1 0 0 1 22 9v6a1 1 0 0 1-.445.832l-9 6a1 1 0 0 1-1.11 0l-9-6A1 1 0 0 1 2 15V9z"/></symbol><symbol id="close-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z"/></symbol><symbol id="menu-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M3 4h18v2H3V4zm0 7h18v2H3v-2zm0 7h18v2H3v-2z"/></symbol></defs></svg></head><body data-theme="dark"><div class="sidebar-container"><div class="sidebar" id="sidebar"><div class="sidebar-items-container"><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-modules"><div>Modules</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="module-QueryCache.html">QueryCache</a></div><div class="sidebar-section-children"><a href="module-afixt-test-utils.html">afixt-test-utils</a></div><div class="sidebar-section-children"><a href="module-colorConversions.html">colorConversions</a></div><div class="sidebar-section-children"><a href="module-constants.html">constants</a></div><div class="sidebar-section-children"><a href="module-cssUtils.html">cssUtils</a></div><div class="sidebar-section-children"><a href="module-formUtils.html">formUtils</a></div><div class="sidebar-section-children"><a href="module-suggestContrast.html">suggestContrast</a></div><div class="sidebar-section-children"><a href="module-tableUtils.html">tableUtils</a></div></div><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-global"><div>Global</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="global.html#RTL_LANGUAGES">RTL_LANGUAGES</a></div><div class="sidebar-section-children"><a href="global.html#_internal">_internal</a></div><div class="sidebar-section-children"><a href="global.html#arrayCount">arrayCount</a></div><div class="sidebar-section-children"><a href="global.html#arrayRemoveByValue">arrayRemoveByValue</a></div><div class="sidebar-section-children"><a href="global.html#arrayUnique">arrayUnique</a></div><div class="sidebar-section-children"><a href="global.html#cellColorDiffs">cellColorDiffs</a></div><div class="sidebar-section-children"><a href="global.html#cellCount">cellCount</a></div><div class="sidebar-section-children"><a href="global.html#checkInconsistent">checkInconsistent</a></div><div class="sidebar-section-children"><a href="global.html#checkMultiRowsInHeader">checkMultiRowsInHeader</a></div><div class="sidebar-section-children"><a href="global.html#checkMultiRowsWithColspan">checkMultiRowsWithColspan</a></div><div class="sidebar-section-children"><a href="global.html#cleanBlank">cleanBlank</a></div><div class="sidebar-section-children"><a href="global.html#colCount">colCount</a></div><div class="sidebar-section-children"><a href="global.html#collectSubtreeText">collectSubtreeText</a></div><div class="sidebar-section-children"><a href="global.html#countBordersPct">countBordersPct</a></div><div class="sidebar-section-children"><a href="global.html#extractMeaningfulContent">extractMeaningfulContent</a></div><div class="sidebar-section-children"><a href="global.html#getAccessibleName">getAccessibleName</a></div><div class="sidebar-section-children"><a href="global.html#getAccessibleText">getAccessibleText</a></div><div class="sidebar-section-children"><a href="global.html#getAriaAttributesByElement">getAriaAttributesByElement</a></div><div class="sidebar-section-children"><a href="global.html#getAttributeHandlers">getAttributeHandlers</a></div><div class="sidebar-section-children"><a href="global.html#getCSSGeneratedContent">getCSSGeneratedContent</a></div><div class="sidebar-section-children"><a href="global.html#getColorContrast">getColorContrast</a></div><div class="sidebar-section-children"><a href="global.html#getComputedBackgroundColor">getComputedBackgroundColor</a></div><div class="sidebar-section-children"><a href="global.html#getComputedRole">getComputedRole</a></div><div class="sidebar-section-children"><a href="global.html#getContrastRatio">getContrastRatio</a></div><div class="sidebar-section-children"><a href="global.html#getEffectiveDir">getEffectiveDir</a></div><div class="sidebar-section-children"><a href="global.html#getEventListeners">getEventListeners</a></div><div class="sidebar-section-children"><a href="global.html#getFocusableElements">getFocusableElements</a></div><div class="sidebar-section-children"><a href="global.html#getGeneratedContent">getGeneratedContent</a></div><div class="sidebar-section-children"><a href="global.html#getImageText">getImageText</a></div><div class="sidebar-section-children"><a href="global.html#getPropertyHandlers">getPropertyHandlers</a></div><div class="sidebar-section-children"><a href="global.html#getRelativeLuminance">getRelativeLuminance</a></div><div class="sidebar-section-children"><a href="global.html#getStyleObject">getStyleObject</a></div><div class="sidebar-section-children"><a href="global.html#getTwoLetterCode">getTwoLetterCode</a></div><div class="sidebar-section-children"><a href="global.html#hasAccessibleName">hasAccessibleName</a></div><div class="sidebar-section-children"><a href="global.html#hasAttribute">hasAttribute</a></div><div class="sidebar-section-children"><a href="global.html#hasBackgroundImage">hasBackgroundImage</a></div><div class="sidebar-section-children"><a href="global.html#hasCSSGeneratedContent">hasCSSGeneratedContent</a></div><div class="sidebar-section-children"><a href="global.html#hasHiddenParent">hasHiddenParent</a></div><div class="sidebar-section-children"><a href="global.html#hasParent">hasParent</a></div><div class="sidebar-section-children"><a href="global.html#hasValidAriaAttributes">hasValidAriaAttributes</a></div><div class="sidebar-section-children"><a href="global.html#isA11yVisible">isA11yVisible</a></div><div class="sidebar-section-children"><a href="global.html#isAriaAttributeValid">isAriaAttributeValid</a></div><div class="sidebar-section-children"><a href="global.html#isComplexTable">isComplexTable</a></div><div class="sidebar-section-children"><a href="global.html#isDataTable">isDataTable</a></div><div class="sidebar-section-children"><a href="global.html#isFocusable">isFocusable</a></div><div class="sidebar-section-children"><a href="global.html#isHidden">isHidden</a></div><div class="sidebar-section-children"><a href="global.html#isNotVisible">isNotVisible</a></div><div class="sidebar-section-children"><a href="global.html#isOffScreen">isOffScreen</a></div><div class="sidebar-section-children"><a href="global.html#isRTLLanguage">isRTLLanguage</a></div><div class="sidebar-section-children"><a href="global.html#isValidLanguageCode">isValidLanguageCode</a></div><div class="sidebar-section-children"><a href="global.html#isValidUrl">isValidUrl</a></div><div class="sidebar-section-children"><a href="global.html#langCodes">langCodes</a></div><div class="sidebar-section-children"><a href="global.html#listEventListeners">listEventListeners</a></div><div class="sidebar-section-children"><a href="global.html#luminance">luminance</a></div><div class="sidebar-section-children"><a href="global.html#matchesSelector">matchesSelector</a></div><div class="sidebar-section-children"><a href="global.html#parseColor">parseColor</a></div><div class="sidebar-section-children"><a href="global.html#parseRGB">parseRGB</a></div><div class="sidebar-section-children"><a href="global.html#rowCount">rowCount</a></div><div class="sidebar-section-children"><a href="global.html#rtls">rtls</a></div><div class="sidebar-section-children"><a href="global.html#sortByVisualOrder">sortByVisualOrder</a></div><div class="sidebar-section-children"><a href="global.html#strlen">strlen</a></div><div class="sidebar-section-children"><a href="global.html#testContrast">testContrast</a></div><div class="sidebar-section-children"><a href="global.html#testLang">testLang</a></div><div class="sidebar-section-children"><a href="global.html#testOrder">testOrder</a></div><div class="sidebar-section-children"><a href="global.html#validLangCodes">validLangCodes</a></div></div></div></div></div><div class="navbar-container" id="VuAckcnZhf"><nav class="navbar"><div class="navbar-left-items"></div><div class="navbar-right-items"><div class="navbar-right-item"><button class="icon-button search-button" aria-label="open-search"><svg><use xlink:href="#search-icon"></use></svg></button></div><div class="navbar-right-item"><button class="icon-button theme-toggle" aria-label="toggle-theme"><svg><use class="theme-svg-use" xlink:href="#light-theme-icon"></use></svg></button></div><div class="navbar-right-item"><button class="icon-button font-size" aria-label="change-font-size"><svg><use xlink:href="#font-size-icon"></use></svg></button></div></div><nav></nav></nav></div><div class="toc-container"><div class="toc-content"><span class="bold">On this page</span><div id="eed4d2a0bfd64539bb9df78095dec881"></div></div></div><div class="body-wrapper"><div class="main-content"><div class="main-wrapper"><section id="source-page" class="source-page"><header><h1 id="title" class="has-anchor">suggestContrast.js</h1></header><article><pre class="prettyprint source lang-js"><code>/**
4
+ * @file Suggest color pairs that meet a minimum WCAG contrast ratio.
5
+ * @module suggestContrast
6
+ * @description Inspired by Tanaguru Contrast-Finder's HSL-space "Psycho" search strategy.
7
+ * Given a foreground/background pair, this utility finds nearby colors that meet
8
+ * the specified contrast ratio while staying perceptually close to the originals.
9
+ */
10
+
11
+ const { luminance } = require('./testContrast.js');
12
+ const { rgbToHex, rgbToHsl, hslToRgb, parseAnyColor } = require('./colorConversions.js');
13
+
14
+ /**
15
+ * Maximum per-channel RGB drift from the original color.
16
+ * Adapted from Tanaguru's DEFAULT_COLOR_COMPONENT_BOUNDER.
17
+ * @type {number}
18
+ */
19
+ const MAX_COMPONENT_DRIFT = 40;
20
+
21
+ /**
22
+ * Number of suggestions to return.
23
+ * @type {number}
24
+ */
25
+ const SUGGESTION_COUNT = 12;
26
+
27
+ /**
28
+ * Compute the WCAG contrast ratio between two RGB color objects.
29
+ * @param {{ r: number, g: number, b: number }} rgb1
30
+ * @param {{ r: number, g: number, b: number }} rgb2
31
+ * @returns {number} Contrast ratio rounded to 2 decimal places (1 to 21)
32
+ */
33
+ function computeContrastRatio(rgb1, rgb2) {
34
+ const lum1 = luminance(rgb1.r, rgb1.g, rgb1.b);
35
+ const lum2 = luminance(rgb2.r, rgb2.g, rgb2.b);
36
+ const lighter = Math.max(lum1, lum2);
37
+ const darker = Math.min(lum1, lum2);
38
+ return Math.round(((lighter + 0.05) / (darker + 0.05)) * 100) / 100;
39
+ }
40
+
41
+ /**
42
+ * Calculate the RGB Manhattan distance between two color objects.
43
+ * @param {{ r: number, g: number, b: number }} a
44
+ * @param {{ r: number, g: number, b: number }} b
45
+ * @returns {number} Sum of absolute differences across R, G, B channels
46
+ */
47
+ function rgbDistance(a, b) {
48
+ return Math.abs(a.r - b.r) + Math.abs(a.g - b.g) + Math.abs(a.b - b.b);
49
+ }
50
+
51
+ /**
52
+ * Check if a candidate color is within the allowed RGB drift from the original.
53
+ * @param {{ r: number, g: number, b: number }} candidate
54
+ * @param {{ r: number, g: number, b: number }} original
55
+ * @returns {boolean}
56
+ */
57
+ function withinDrift(candidate, original) {
58
+ return (
59
+ Math.abs(candidate.r - original.r) &lt;= MAX_COMPONENT_DRIFT &amp;&amp;
60
+ Math.abs(candidate.g - original.g) &lt;= MAX_COMPONENT_DRIFT &amp;&amp;
61
+ Math.abs(candidate.b - original.b) &lt;= MAX_COMPONENT_DRIFT
62
+ );
63
+ }
64
+
65
+ /**
66
+ * Clamp a number between min and max.
67
+ * @param {number} val
68
+ * @param {number} min
69
+ * @param {number} max
70
+ * @returns {number}
71
+ */
72
+ function clamp(val, min, max) {
73
+ return Math.max(min, Math.min(max, val));
74
+ }
75
+
76
+ /**
77
+ * Wrap a hue value to stay within 0-360.
78
+ * @param {number} h
79
+ * @returns {number}
80
+ */
81
+ function wrapHue(h) {
82
+ return ((h % 360) + 360) % 360;
83
+ }
84
+
85
+ /**
86
+ * Format an RGB object into a full suggestion color object with hex, rgb, and hsl representations.
87
+ * @param {{ r: number, g: number, b: number }} rgb
88
+ * @returns {{ hex: string, rgb: { r: number, g: number, b: number }, hsl: { h: number, s: number, l: number } }}
89
+ */
90
+ function formatColor(rgb) {
91
+ return {
92
+ hex: rgbToHex(rgb),
93
+ rgb: { r: rgb.r, g: rgb.g, b: rgb.b },
94
+ hsl: rgbToHsl(rgb),
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Derive the default minimum contrast ratio based on font size and weight per WCAG 2.x.
100
+ * @param {number} fontSize - Font size in pixels
101
+ * @param {boolean} isBold - Whether the text is bold
102
+ * @returns {number} Minimum contrast ratio (3.0 for large text, 4.5 for normal text)
103
+ */
104
+ function deriveMinRatio(fontSize, isBold) {
105
+ if (fontSize >= 18) {
106
+ return 3.0;
107
+ }
108
+ if (fontSize >= 14 &amp;&amp; isBold) {
109
+ return 3.0;
110
+ }
111
+ return 4.5;
112
+ }
113
+
114
+ /**
115
+ * Search for valid colors by adjusting lightness in one direction.
116
+ * @param {{ h: number, s: number, l: number }} baseHsl - Starting HSL color
117
+ * @param {{ r: number, g: number, b: number }} originalRgb - Original RGB for drift checking
118
+ * @param {{ r: number, g: number, b: number }} fixedRgb - The fixed color to check contrast against
119
+ * @param {number} targetRatio - Required contrast ratio
120
+ * @param {number} direction - +1 for lighter, -1 for darker
121
+ * @param {number} maxResults - Maximum results to collect
122
+ * @returns {Array&lt;{ r: number, g: number, b: number }>} Array of valid RGB colors found
123
+ */
124
+ function searchLightness(baseHsl, originalRgb, fixedRgb, targetRatio, direction, maxResults) {
125
+ const results = [];
126
+
127
+ for (let step = 1; step &lt;= 100; step++) {
128
+ const newL = baseHsl.l + direction * step;
129
+ if (newL &lt; 0 || newL > 100) {
130
+ break;
131
+ }
132
+
133
+ const candidateHsl = { h: baseHsl.h, s: baseHsl.s, l: Math.round(newL) };
134
+ const candidateRgb = hslToRgb(candidateHsl);
135
+ const ratio = computeContrastRatio(candidateRgb, fixedRgb);
136
+
137
+ if (ratio >= targetRatio &amp;&amp; withinDrift(candidateRgb, originalRgb)) {
138
+ results.push(candidateRgb);
139
+ if (results.length >= maxResults) {
140
+ break;
141
+ }
142
+ }
143
+ }
144
+
145
+ return results;
146
+ }
147
+
148
+ /**
149
+ * Search for valid colors by adjusting lightness, without the drift constraint.
150
+ * Used as a fallback when drift-constrained search yields too few results.
151
+ * @param {{ h: number, s: number, l: number }} baseHsl
152
+ * @param {{ r: number, g: number, b: number }} fixedRgb
153
+ * @param {number} targetRatio
154
+ * @param {number} direction
155
+ * @param {number} maxResults
156
+ * @returns {Array&lt;{ r: number, g: number, b: number }>}
157
+ */
158
+ function searchLightnessUnconstrained(baseHsl, fixedRgb, targetRatio, direction, maxResults) {
159
+ const results = [];
160
+
161
+ for (let step = 1; step &lt;= 100; step++) {
162
+ const newL = baseHsl.l + direction * step;
163
+ if (newL &lt; 0 || newL > 100) {
164
+ break;
165
+ }
166
+
167
+ const candidateHsl = { h: baseHsl.h, s: baseHsl.s, l: Math.round(newL) };
168
+ const candidateRgb = hslToRgb(candidateHsl);
169
+ const ratio = computeContrastRatio(candidateRgb, fixedRgb);
170
+
171
+ if (ratio >= targetRatio) {
172
+ results.push(candidateRgb);
173
+ if (results.length >= maxResults) {
174
+ break;
175
+ }
176
+ }
177
+ }
178
+
179
+ return results;
180
+ }
181
+
182
+ /**
183
+ * Generate hue/saturation variations from a base color that meet the contrast requirement.
184
+ * @param {{ r: number, g: number, b: number }} baseRgb - Base color found from lightness search
185
+ * @param {{ r: number, g: number, b: number }} fixedRgb - The color held constant
186
+ * @param {number} targetRatio - Required contrast ratio
187
+ * @param {Set&lt;string>} seen - Set of already-seen hex pair keys for dedup
188
+ * @param {string} fixedHex - Hex of the fixed color for building dedup keys
189
+ * @param {boolean} isForeground - Whether baseRgb is the foreground in the pair
190
+ * @returns {Array&lt;{ candidate: { r: number, g: number, b: number }, key: string }>}
191
+ */
192
+ function generateVariations(baseRgb, fixedRgb, targetRatio, seen, fixedHex, isForeground) {
193
+ const results = [];
194
+ const baseHsl = rgbToHsl(baseRgb);
195
+ const hueOffsets = [-10, -5, 5, 10];
196
+ const satOffsets = [-6, -3, 3, 6];
197
+
198
+ for (const hOff of hueOffsets) {
199
+ for (const sOff of satOffsets) {
200
+ const candidateHsl = {
201
+ h: wrapHue(baseHsl.h + hOff),
202
+ s: clamp(baseHsl.s + sOff, 0, 100),
203
+ l: baseHsl.l,
204
+ };
205
+ const candidateRgb = hslToRgb(candidateHsl);
206
+ const ratio = computeContrastRatio(candidateRgb, fixedRgb);
207
+
208
+ if (ratio >= targetRatio) {
209
+ const candHex = rgbToHex(candidateRgb);
210
+ const key = isForeground ? candHex + '|' + fixedHex : fixedHex + '|' + candHex;
211
+ if (!seen.has(key)) {
212
+ results.push({ candidate: candidateRgb, key });
213
+ }
214
+ }
215
+ }
216
+ }
217
+
218
+ return results;
219
+ }
220
+
221
+ /**
222
+ * Suggest color pairs that meet a minimum contrast ratio.
223
+ *
224
+ * Takes a foreground/background color pair and returns up to 12 alternative color pair
225
+ * suggestions that meet the specified (or WCAG-derived) minimum contrast ratio.
226
+ * Each suggestion stays perceptually close to the original colors.
227
+ *
228
+ * @param {Object} options
229
+ * @param {string} options.foreground - Foreground color (hex, rgb(), rgba(), or hsl() string)
230
+ * @param {string} options.background - Background color (hex, rgb(), rgba(), or hsl() string)
231
+ * @param {number} [options.fontSize=16] - Text size in pixels
232
+ * @param {number} [options.minRatio] - Minimum desired contrast ratio (1-21).
233
+ * Defaults based on fontSize: 4.5 for normal text, 3.0 for large text (>=18px or >=14px bold).
234
+ * @param {boolean} [options.isBold=false] - Whether text is bold (affects default ratio for 14-17px text)
235
+ * @returns {Array&lt;Object>} Array of up to 12 suggestion objects sorted by proximity to originals.
236
+ * Each object has: foreground ({hex, rgb, hsl}), background ({hex, rgb, hsl}), contrastRatio, distance.
237
+ * Returns empty array on invalid input.
238
+ */
239
+ function suggestContrast(options) {
240
+ if (!options || typeof options !== 'object') {
241
+ return [];
242
+ }
243
+
244
+ const fgRgb = parseAnyColor(options.foreground);
245
+ const bgRgb = parseAnyColor(options.background);
246
+
247
+ if (!fgRgb || !bgRgb) {
248
+ return [];
249
+ }
250
+
251
+ const fontSize =
252
+ typeof options.fontSize === 'number' &amp;&amp; options.fontSize > 0 ? options.fontSize : 16;
253
+ const isBold = options.isBold === true;
254
+
255
+ let targetRatio;
256
+ if (typeof options.minRatio === 'number') {
257
+ if (options.minRatio &lt; 1 || options.minRatio > 21) {
258
+ return [];
259
+ }
260
+ targetRatio = options.minRatio;
261
+ } else {
262
+ targetRatio = deriveMinRatio(fontSize, isBold);
263
+ }
264
+
265
+ const fgHsl = rgbToHsl(fgRgb);
266
+ const bgHsl = rgbToHsl(bgRgb);
267
+ const fgHex = rgbToHex(fgRgb);
268
+ const bgHex = rgbToHex(bgRgb);
269
+
270
+ const candidates = [];
271
+ const seen = new Set();
272
+
273
+ // --- Phase 1: Lightness adjustments ---
274
+
275
+ // 1a. Adjust foreground lightness (hold background fixed)
276
+ const fgDarker = searchLightness(fgHsl, fgRgb, bgRgb, targetRatio, -1, 3);
277
+ const fgLighter = searchLightness(fgHsl, fgRgb, bgRgb, targetRatio, 1, 3);
278
+ const fgResults = [...fgDarker, ...fgLighter];
279
+
280
+ // Fallback: if drift constraint was too tight, search without it
281
+ if (fgResults.length &lt; 3) {
282
+ const needed = 3 - fgResults.length;
283
+ const fallbackDarker = searchLightnessUnconstrained(fgHsl, bgRgb, targetRatio, -1, needed);
284
+ const fallbackLighter = searchLightnessUnconstrained(fgHsl, bgRgb, targetRatio, 1, needed);
285
+ const fallback = [...fallbackDarker, ...fallbackLighter];
286
+ // Deduplicate against what we already have
287
+ const existingHexes = new Set(fgResults.map(c => rgbToHex(c)));
288
+ for (const c of fallback) {
289
+ if (!existingHexes.has(rgbToHex(c)) &amp;&amp; fgResults.length &lt; 6) {
290
+ fgResults.push(c);
291
+ }
292
+ }
293
+ }
294
+
295
+ for (const fg of fgResults) {
296
+ const hex = rgbToHex(fg);
297
+ const key = hex + '|' + bgHex;
298
+ if (!seen.has(key)) {
299
+ seen.add(key);
300
+ candidates.push({
301
+ fg,
302
+ bg: bgRgb,
303
+ ratio: computeContrastRatio(fg, bgRgb),
304
+ dist: rgbDistance(fg, fgRgb),
305
+ });
306
+ }
307
+ }
308
+
309
+ // 1b. Adjust background lightness (hold foreground fixed)
310
+ const bgDarker = searchLightness(bgHsl, bgRgb, fgRgb, targetRatio, -1, 3);
311
+ const bgLighter = searchLightness(bgHsl, bgRgb, fgRgb, targetRatio, 1, 3);
312
+ const bgResults = [...bgDarker, ...bgLighter];
313
+
314
+ if (bgResults.length &lt; 3) {
315
+ const needed = 3 - bgResults.length;
316
+ const fallbackDarker = searchLightnessUnconstrained(bgHsl, fgRgb, targetRatio, -1, needed);
317
+ const fallbackLighter = searchLightnessUnconstrained(bgHsl, fgRgb, targetRatio, 1, needed);
318
+ const fallback = [...fallbackDarker, ...fallbackLighter];
319
+ const existingHexes = new Set(bgResults.map(c => rgbToHex(c)));
320
+ for (const c of fallback) {
321
+ if (!existingHexes.has(rgbToHex(c)) &amp;&amp; bgResults.length &lt; 6) {
322
+ bgResults.push(c);
323
+ }
324
+ }
325
+ }
326
+
327
+ for (const bg of bgResults) {
328
+ const hex = rgbToHex(bg);
329
+ const key = fgHex + '|' + hex;
330
+ if (!seen.has(key)) {
331
+ seen.add(key);
332
+ candidates.push({
333
+ fg: fgRgb,
334
+ bg,
335
+ ratio: computeContrastRatio(fgRgb, bg),
336
+ dist: rgbDistance(bg, bgRgb),
337
+ });
338
+ }
339
+ }
340
+
341
+ // --- Phase 2: Hue/Saturation diversity ---
342
+
343
+ // Use the first few Phase 1 results as bases for variation
344
+ const fgBases = fgResults.slice(0, 2);
345
+ const bgBases = bgResults.slice(0, 2);
346
+
347
+ for (const base of fgBases) {
348
+ const variations = generateVariations(base, bgRgb, targetRatio, seen, bgHex, true);
349
+ for (const v of variations) {
350
+ seen.add(v.key);
351
+ candidates.push({
352
+ fg: v.candidate,
353
+ bg: bgRgb,
354
+ ratio: computeContrastRatio(v.candidate, bgRgb),
355
+ dist: rgbDistance(v.candidate, fgRgb),
356
+ });
357
+ }
358
+ }
359
+
360
+ for (const base of bgBases) {
361
+ const variations = generateVariations(base, fgRgb, targetRatio, seen, fgHex, false);
362
+ for (const v of variations) {
363
+ seen.add(v.key);
364
+ candidates.push({
365
+ fg: fgRgb,
366
+ bg: v.candidate,
367
+ ratio: computeContrastRatio(fgRgb, v.candidate),
368
+ dist: rgbDistance(v.candidate, bgRgb),
369
+ });
370
+ }
371
+ }
372
+
373
+ // --- Sort by distance and return top SUGGESTION_COUNT ---
374
+
375
+ candidates.sort((a, b) => a.dist - b.dist);
376
+
377
+ return candidates.slice(0, SUGGESTION_COUNT).map(c => ({
378
+ foreground: formatColor(c.fg),
379
+ background: formatColor(c.bg),
380
+ contrastRatio: c.ratio,
381
+ distance: c.dist,
382
+ }));
383
+ }
384
+
385
+ module.exports = {
386
+ suggestContrast,
387
+ computeContrastRatio,
388
+ };
389
+ </code></pre></article></section></div></div></div><div class="search-container" id="PkfLWpAbet" style="display:none"><div class="wrapper" id="iCxFxjkHbP"><button class="icon-button search-close-button" id="VjLlGakifb" aria-label="close search"><svg><use xlink:href="#close-icon"></use></svg></button><div class="search-box-c"><svg><use xlink:href="#search-icon"></use></svg> <input type="text" id="vpcKVYIppa" class="search-input" placeholder="Search..." autofocus></div><div class="search-result-c" id="fWwVHRuDuN"><span class="search-result-c-text">Type anything to view search result</span></div></div></div><div class="mobile-menu-icon-container"><button class="icon-button" id="mobile-menu" data-isopen="false" aria-label="menu"><svg><use xlink:href="#menu-icon"></use></svg></button></div><div id="mobile-sidebar" class="mobile-sidebar-container"><div class="mobile-sidebar-wrapper"><div class="mobile-nav-links"></div><div class="mobile-sidebar-items-c"><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-modules"><div>Modules</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="module-QueryCache.html">QueryCache</a></div><div class="sidebar-section-children"><a href="module-afixt-test-utils.html">afixt-test-utils</a></div><div class="sidebar-section-children"><a href="module-colorConversions.html">colorConversions</a></div><div class="sidebar-section-children"><a href="module-constants.html">constants</a></div><div class="sidebar-section-children"><a href="module-cssUtils.html">cssUtils</a></div><div class="sidebar-section-children"><a href="module-formUtils.html">formUtils</a></div><div class="sidebar-section-children"><a href="module-suggestContrast.html">suggestContrast</a></div><div class="sidebar-section-children"><a href="module-tableUtils.html">tableUtils</a></div></div><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-global"><div>Global</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="global.html#RTL_LANGUAGES">RTL_LANGUAGES</a></div><div class="sidebar-section-children"><a href="global.html#_internal">_internal</a></div><div class="sidebar-section-children"><a href="global.html#arrayCount">arrayCount</a></div><div class="sidebar-section-children"><a href="global.html#arrayRemoveByValue">arrayRemoveByValue</a></div><div class="sidebar-section-children"><a href="global.html#arrayUnique">arrayUnique</a></div><div class="sidebar-section-children"><a href="global.html#cellColorDiffs">cellColorDiffs</a></div><div class="sidebar-section-children"><a href="global.html#cellCount">cellCount</a></div><div class="sidebar-section-children"><a href="global.html#checkInconsistent">checkInconsistent</a></div><div class="sidebar-section-children"><a href="global.html#checkMultiRowsInHeader">checkMultiRowsInHeader</a></div><div class="sidebar-section-children"><a href="global.html#checkMultiRowsWithColspan">checkMultiRowsWithColspan</a></div><div class="sidebar-section-children"><a href="global.html#cleanBlank">cleanBlank</a></div><div class="sidebar-section-children"><a href="global.html#colCount">colCount</a></div><div class="sidebar-section-children"><a href="global.html#collectSubtreeText">collectSubtreeText</a></div><div class="sidebar-section-children"><a href="global.html#countBordersPct">countBordersPct</a></div><div class="sidebar-section-children"><a href="global.html#extractMeaningfulContent">extractMeaningfulContent</a></div><div class="sidebar-section-children"><a href="global.html#getAccessibleName">getAccessibleName</a></div><div class="sidebar-section-children"><a href="global.html#getAccessibleText">getAccessibleText</a></div><div class="sidebar-section-children"><a href="global.html#getAriaAttributesByElement">getAriaAttributesByElement</a></div><div class="sidebar-section-children"><a href="global.html#getAttributeHandlers">getAttributeHandlers</a></div><div class="sidebar-section-children"><a href="global.html#getCSSGeneratedContent">getCSSGeneratedContent</a></div><div class="sidebar-section-children"><a href="global.html#getColorContrast">getColorContrast</a></div><div class="sidebar-section-children"><a href="global.html#getComputedBackgroundColor">getComputedBackgroundColor</a></div><div class="sidebar-section-children"><a href="global.html#getComputedRole">getComputedRole</a></div><div class="sidebar-section-children"><a href="global.html#getContrastRatio">getContrastRatio</a></div><div class="sidebar-section-children"><a href="global.html#getEffectiveDir">getEffectiveDir</a></div><div class="sidebar-section-children"><a href="global.html#getEventListeners">getEventListeners</a></div><div class="sidebar-section-children"><a href="global.html#getFocusableElements">getFocusableElements</a></div><div class="sidebar-section-children"><a href="global.html#getGeneratedContent">getGeneratedContent</a></div><div class="sidebar-section-children"><a href="global.html#getImageText">getImageText</a></div><div class="sidebar-section-children"><a href="global.html#getPropertyHandlers">getPropertyHandlers</a></div><div class="sidebar-section-children"><a href="global.html#getRelativeLuminance">getRelativeLuminance</a></div><div class="sidebar-section-children"><a href="global.html#getStyleObject">getStyleObject</a></div><div class="sidebar-section-children"><a href="global.html#getTwoLetterCode">getTwoLetterCode</a></div><div class="sidebar-section-children"><a href="global.html#hasAccessibleName">hasAccessibleName</a></div><div class="sidebar-section-children"><a href="global.html#hasAttribute">hasAttribute</a></div><div class="sidebar-section-children"><a href="global.html#hasBackgroundImage">hasBackgroundImage</a></div><div class="sidebar-section-children"><a href="global.html#hasCSSGeneratedContent">hasCSSGeneratedContent</a></div><div class="sidebar-section-children"><a href="global.html#hasHiddenParent">hasHiddenParent</a></div><div class="sidebar-section-children"><a href="global.html#hasParent">hasParent</a></div><div class="sidebar-section-children"><a href="global.html#hasValidAriaAttributes">hasValidAriaAttributes</a></div><div class="sidebar-section-children"><a href="global.html#isA11yVisible">isA11yVisible</a></div><div class="sidebar-section-children"><a href="global.html#isAriaAttributeValid">isAriaAttributeValid</a></div><div class="sidebar-section-children"><a href="global.html#isComplexTable">isComplexTable</a></div><div class="sidebar-section-children"><a href="global.html#isDataTable">isDataTable</a></div><div class="sidebar-section-children"><a href="global.html#isFocusable">isFocusable</a></div><div class="sidebar-section-children"><a href="global.html#isHidden">isHidden</a></div><div class="sidebar-section-children"><a href="global.html#isNotVisible">isNotVisible</a></div><div class="sidebar-section-children"><a href="global.html#isOffScreen">isOffScreen</a></div><div class="sidebar-section-children"><a href="global.html#isRTLLanguage">isRTLLanguage</a></div><div class="sidebar-section-children"><a href="global.html#isValidLanguageCode">isValidLanguageCode</a></div><div class="sidebar-section-children"><a href="global.html#isValidUrl">isValidUrl</a></div><div class="sidebar-section-children"><a href="global.html#langCodes">langCodes</a></div><div class="sidebar-section-children"><a href="global.html#listEventListeners">listEventListeners</a></div><div class="sidebar-section-children"><a href="global.html#luminance">luminance</a></div><div class="sidebar-section-children"><a href="global.html#matchesSelector">matchesSelector</a></div><div class="sidebar-section-children"><a href="global.html#parseColor">parseColor</a></div><div class="sidebar-section-children"><a href="global.html#parseRGB">parseRGB</a></div><div class="sidebar-section-children"><a href="global.html#rowCount">rowCount</a></div><div class="sidebar-section-children"><a href="global.html#rtls">rtls</a></div><div class="sidebar-section-children"><a href="global.html#sortByVisualOrder">sortByVisualOrder</a></div><div class="sidebar-section-children"><a href="global.html#strlen">strlen</a></div><div class="sidebar-section-children"><a href="global.html#testContrast">testContrast</a></div><div class="sidebar-section-children"><a href="global.html#testLang">testLang</a></div><div class="sidebar-section-children"><a href="global.html#testOrder">testOrder</a></div><div class="sidebar-section-children"><a href="global.html#validLangCodes">validLangCodes</a></div></div></div><div class="mobile-navbar-actions"><div class="navbar-right-item"><button class="icon-button search-button" aria-label="open-search"><svg><use xlink:href="#search-icon"></use></svg></button></div><div class="navbar-right-item"><button class="icon-button theme-toggle" aria-label="toggle-theme"><svg><use class="theme-svg-use" xlink:href="#light-theme-icon"></use></svg></button></div><div class="navbar-right-item"><button class="icon-button font-size" aria-label="change-font-size"><svg><use xlink:href="#font-size-icon"></use></svg></button></div></div></div></div><script type="text/javascript" src="scripts/core.min.js"></script><script src="scripts/search.min.js" defer="defer"></script><script src="scripts/third-party/fuse.js" defer="defer"></script><script type="text/javascript">var tocbotInstance=tocbot.init({tocSelector:"#eed4d2a0bfd64539bb9df78095dec881",contentSelector:".main-content",headingSelector:"h1, h2, h3",hasInnerContainers:!0,scrollContainer:".main-content",headingsOffset:130,onClick:bringLinkToView})</script></body></html>