From 9934931f1ff4093936b757186bd5d36b9a511c75 Mon Sep 17 00:00:00 2001 From: silverwind <me@silverwind.io> Date: Sun, 7 Apr 2024 18:19:25 +0200 Subject: [PATCH] [PORT] gitea##30237: Fix and rewrite contrast color calculation, fix project-related bugs 1. The previous color contrast calculation function was incorrect at least for the `#84b6eb` where it output low-contrast white instead of black. I've rewritten these functions now to accept hex colors and to match GitHub's calculation and to output pure white/black for maximum contrast. Before and after: <img width="94" alt="Screenshot 2024-04-02 at 01 53 46" src="https://github.com/go-gitea/gitea/assets/115237/00b39e15-a377-4458-95cf-ceec74b78228"><img width="90" alt="Screenshot 2024-04-02 at 01 51 30" src="https://github.com/go-gitea/gitea/assets/115237/1677067a-8d8f-47eb-82c0-76330deeb775"> 2. Fix project-related issues: - Expose the new `ContrastColor` function as template helper and use it for project cards, replacing the previous JS solution which eliminates a flash of wrong color on page load. - Fix a bug where if editing a project title, the counter would get lost. - Move `rgbToHex` function to color utils. Co-authored-by: delvh <dev.lh@web.de> Co-authored-by: Giteabot <teabot@gitea.io> --- Conflict resolution: Trivial. (cherry picked from commit 36887ed3921d03f1864360c95bd2ecf853bfbe72) (cherry picked from commit f6c0c39f1aef167bb14375a009cf463c6bf031fb) --- modules/templates/helper.go | 6 +-- modules/templates/util_render.go | 12 ++--- modules/util/color.go | 42 +++++++--------- modules/util/color_test.go | 46 +++++++++--------- templates/projects/view.tmpl | 8 ++-- web_src/css/features/projects.css | 27 +++++------ web_src/css/repo.css | 15 +++++- web_src/css/repo/issue-list.css | 17 ------- web_src/css/themes/theme-gitea-dark.css | 2 - web_src/css/themes/theme-gitea-light.css | 2 - web_src/js/components/ContextPopup.vue | 24 ++++------ web_src/js/features/repo-projects.js | 61 ++++++++---------------- web_src/js/utils/color.js | 30 ++++++------ web_src/js/utils/color.test.js | 39 +++++++-------- 14 files changed, 136 insertions(+), 195 deletions(-) diff --git a/modules/templates/helper.go b/modules/templates/helper.go index c9799a38ec..3558dcf94c 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -53,13 +53,13 @@ func NewFuncMap() template.FuncMap { "JsonUtils": NewJsonUtils, // ----------------------------------------------------------------- - // svg / avatar / icon + // svg / avatar / icon / color "svg": svg.RenderHTML, "EntryIcon": base.EntryIcon, "MigrationIcon": MigrationIcon, "ActionIcon": ActionIcon, - - "SortArrow": SortArrow, + "SortArrow": SortArrow, + "ContrastColor": util.ContrastColor, // ----------------------------------------------------------------- // time / number / format diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go index 5446741287..c4c5376afd 100644 --- a/modules/templates/util_render.go +++ b/modules/templates/util_render.go @@ -135,16 +135,9 @@ func RenderIssueTitle(ctx context.Context, text string, metas map[string]string) func RenderLabel(ctx context.Context, locale translation.Locale, label *issues_model.Label) template.HTML { var ( archivedCSSClass string - textColor = "#111" + textColor = util.ContrastColor(label.Color) labelScope = label.ExclusiveScope() ) - r, g, b := util.HexToRBGColor(label.Color) - - // Determine if label text should be light or dark to be readable on background color - // this doesn't account for saturation or transparency - if util.UseLightTextOnBackground(r, g, b) { - textColor = "#eee" - } description := emoji.ReplaceAliases(template.HTMLEscapeString(label.Description)) @@ -168,7 +161,7 @@ func RenderLabel(ctx context.Context, locale translation.Locale, label *issues_m // Make scope and item background colors slightly darker and lighter respectively. // More contrast needed with higher luminance, empirically tweaked. - luminance := util.GetLuminance(r, g, b) + luminance := util.GetRelativeLuminance(label.Color) contrast := 0.01 + luminance*0.03 // Ensure we add the same amount of contrast also near 0 and 1. darken := contrast + math.Max(luminance+contrast-1.0, 0.0) @@ -178,6 +171,7 @@ func RenderLabel(ctx context.Context, locale translation.Locale, label *issues_m lightenFactor := math.Min(luminance+lighten, 1.0) / math.Max(luminance, 1.0/255.0) opacity := GetLabelOpacityByte(label.IsArchived()) + r, g, b := util.HexToRBGColor(label.Color) scopeBytes := []byte{ uint8(math.Min(math.Round(r*darkenFactor), 255)), uint8(math.Min(math.Round(g*darkenFactor), 255)), diff --git a/modules/util/color.go b/modules/util/color.go index 240b045c28..9c520dce78 100644 --- a/modules/util/color.go +++ b/modules/util/color.go @@ -4,22 +4,10 @@ package util import ( "fmt" - "math" "strconv" "strings" ) -// Check similar implementation in web_src/js/utils/color.js and keep synchronization - -// Return R, G, B values defined in reletive luminance -func getLuminanceRGB(channel float64) float64 { - sRGB := channel / 255 - if sRGB <= 0.03928 { - return sRGB / 12.92 - } - return math.Pow((sRGB+0.055)/1.055, 2.4) -} - // Get color as RGB values in 0..255 range from the hex color string (with or without #) func HexToRBGColor(colorString string) (float64, float64, float64) { hexString := colorString @@ -47,19 +35,23 @@ func HexToRBGColor(colorString string) (float64, float64, float64) { return r, g, b } -// return luminance given RGB channels -// Reference from: https://www.w3.org/WAI/GL/wiki/Relative_luminance -func GetLuminance(r, g, b float64) float64 { - R := getLuminanceRGB(r) - G := getLuminanceRGB(g) - B := getLuminanceRGB(b) - luminance := 0.2126*R + 0.7152*G + 0.0722*B - return luminance +// Returns relative luminance for a SRGB color - https://en.wikipedia.org/wiki/Relative_luminance +// Keep this in sync with web_src/js/utils/color.js +func GetRelativeLuminance(color string) float64 { + r, g, b := HexToRBGColor(color) + return (0.2126729*r + 0.7151522*g + 0.0721750*b) / 255 } -// Reference from: https://firsching.ch/github_labels.html -// In the future WCAG 3 APCA may be a better solution. -// Check if text should use light color based on RGB of background -func UseLightTextOnBackground(r, g, b float64) bool { - return GetLuminance(r, g, b) < 0.453 +func UseLightText(backgroundColor string) bool { + return GetRelativeLuminance(backgroundColor) < 0.453 +} + +// Given a background color, returns a black or white foreground color that the highest +// contrast ratio. In the future, the APCA contrast function, or CSS `contrast-color` will be better. +// https://github.com/color-js/color.js/blob/eb7b53f7a13bb716ec8b28c7a56f052cd599acd9/src/contrast/APCA.js#L42 +func ContrastColor(backgroundColor string) string { + if UseLightText(backgroundColor) { + return "#fff" + } + return "#000" } diff --git a/modules/util/color_test.go b/modules/util/color_test.go index d96ac36730..be6e6b122a 100644 --- a/modules/util/color_test.go +++ b/modules/util/color_test.go @@ -33,33 +33,31 @@ func Test_HexToRBGColor(t *testing.T) { } } -func Test_UseLightTextOnBackground(t *testing.T) { +func Test_UseLightText(t *testing.T) { cases := []struct { - r float64 - g float64 - b float64 - expected bool + color string + expected string }{ - {215, 58, 74, true}, - {0, 117, 202, true}, - {207, 211, 215, false}, - {162, 238, 239, false}, - {112, 87, 255, true}, - {0, 134, 114, true}, - {228, 230, 105, false}, - {216, 118, 227, true}, - {255, 255, 255, false}, - {43, 134, 133, true}, - {43, 135, 134, true}, - {44, 135, 134, true}, - {59, 182, 179, true}, - {124, 114, 104, true}, - {126, 113, 108, true}, - {129, 112, 109, true}, - {128, 112, 112, true}, + {"#d73a4a", "#fff"}, + {"#0075ca", "#fff"}, + {"#cfd3d7", "#000"}, + {"#a2eeef", "#000"}, + {"#7057ff", "#fff"}, + {"#008672", "#fff"}, + {"#e4e669", "#000"}, + {"#d876e3", "#000"}, + {"#ffffff", "#000"}, + {"#2b8684", "#fff"}, + {"#2b8786", "#fff"}, + {"#2c8786", "#000"}, + {"#3bb6b3", "#000"}, + {"#7c7268", "#fff"}, + {"#7e716c", "#fff"}, + {"#81706d", "#fff"}, + {"#807070", "#fff"}, + {"#84b6eb", "#000"}, } for n, c := range cases { - result := UseLightTextOnBackground(c.r, c.g, c.b) - assert.Equal(t, c.expected, result, "case %d: error should match", n) + assert.Equal(t, c.expected, ContrastColor(c.color), "case %d: error should match", n) } } diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index d89750862e..861c7ef5a9 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -66,13 +66,13 @@ <div id="project-board"> <div class="board {{if .CanWriteProjects}}sortable{{end}}"> {{range .Columns}} - <div class="ui segment project-column" style="background: {{.Color}} !important;" data-id="{{.ID}}" data-sorting="{{.Sorting}}" data-url="{{$.Link}}/{{.ID}}"> + <div class="ui segment project-column"{{if .Color}} style="background: {{.Color}} !important; color: {{ContrastColor .Color}} !important"{{end}} data-id="{{.ID}}" data-sorting="{{.Sorting}}" data-url="{{$.Link}}/{{.ID}}"> <div class="project-column-header{{if $canWriteProject}} tw-cursor-grab{{end}}"> <div class="ui large label project-column-title tw-py-1"> <div class="ui small circular grey label project-column-issue-count"> {{.NumIssues ctx}} </div> - {{.Title}} + <span class="project-column-title-label">{{.Title}}</span> </div> {{if $canWriteProject}} <div class="ui dropdown jump item"> @@ -153,9 +153,7 @@ </div> {{end}} </div> - - <div class="divider"></div> - + <div class="divider"{{if .Color}} style="color: {{ContrastColor .Color}} !important"{{end}}></div> <div class="ui cards" data-url="{{$.Link}}/{{.ID}}" data-project="{{$.Project.ID}}" data-board="{{.ID}}" id="board_{{.ID}}"> {{range (index $.IssuesMap .ID)}} <div class="issue-card gt-word-break {{if $canWriteProject}}tw-cursor-grab{{end}}" data-issue="{{.ID}}"> diff --git a/web_src/css/features/projects.css b/web_src/css/features/projects.css index 30df994c38..ff2a65e03a 100644 --- a/web_src/css/features/projects.css +++ b/web_src/css/features/projects.css @@ -22,34 +22,27 @@ cursor: default; } +.project-column .issue-card { + color: var(--color-text); +} + .project-column-header { display: flex; align-items: center; justify-content: space-between; } -.project-column-header.dark-label { - color: var(--color-project-board-dark-label) !important; -} - -.project-column-header.dark-label .project-column-title { - color: var(--color-project-board-dark-label) !important; -} - -.project-column-header.light-label { - color: var(--color-project-board-light-label) !important; -} - -.project-column-header.light-label .project-column-title { - color: var(--color-project-board-light-label) !important; -} - .project-column-title { background: none !important; line-height: 1.25 !important; cursor: inherit; } +.project-column-title, +.project-column-issue-count { + color: inherit !important; +} + .project-column > .cards { flex: 1; display: flex; @@ -64,6 +57,8 @@ .project-column > .divider { margin: 5px 0; + border-color: currentcolor; + opacity: .5; } .project-column:first-child { diff --git a/web_src/css/repo.css b/web_src/css/repo.css index 3c43c0b079..3c65a0500e 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -2461,8 +2461,21 @@ td .commit-summary { height: 0.5em; } +.labels-list { + display: flex; + flex-wrap: wrap; + gap: 0.25em; +} + +.labels-list a { + display: flex; + text-decoration: none; +} + .labels-list .label { - margin: 2px 0; + padding: 0 6px; + margin: 0 !important; + min-height: 20px; display: inline-flex !important; line-height: 1.3; /* there is a `font-size: 1.25em` for inside emoji, so here the line-height needs to be larger slightly */ } diff --git a/web_src/css/repo/issue-list.css b/web_src/css/repo/issue-list.css index d19421fcbc..37090f71b4 100644 --- a/web_src/css/repo/issue-list.css +++ b/web_src/css/repo/issue-list.css @@ -69,23 +69,6 @@ } } -#issue-list .flex-item-title .labels-list { - display: flex; - flex-wrap: wrap; - gap: 0.25em; -} - -#issue-list .flex-item-title .labels-list a { - display: flex; - text-decoration: none; -} - -#issue-list .flex-item-title .labels-list .label { - padding: 0 6px; - margin: 0; - min-height: 20px; -} - #issue-list .flex-item-body .branches { display: inline-flex; } diff --git a/web_src/css/themes/theme-gitea-dark.css b/web_src/css/themes/theme-gitea-dark.css index ed6718e40c..c74f334c2d 100644 --- a/web_src/css/themes/theme-gitea-dark.css +++ b/web_src/css/themes/theme-gitea-dark.css @@ -215,8 +215,6 @@ --color-placeholder-text: var(--color-text-light-3); --color-editor-line-highlight: var(--color-primary-light-5); --color-project-board-bg: var(--color-secondary-light-2); - --color-project-board-dark-label: #0e1011; - --color-project-board-light-label: #dde0e2; --color-caret: var(--color-text); /* should ideally be --color-text-dark, see #15651 */ --color-reaction-bg: #e8e8ff12; --color-reaction-hover-bg: var(--color-primary-light-4); diff --git a/web_src/css/themes/theme-gitea-light.css b/web_src/css/themes/theme-gitea-light.css index b10ad7d840..01dd8ba4f7 100644 --- a/web_src/css/themes/theme-gitea-light.css +++ b/web_src/css/themes/theme-gitea-light.css @@ -215,8 +215,6 @@ --color-placeholder-text: var(--color-text-light-3); --color-editor-line-highlight: var(--color-primary-light-6); --color-project-board-bg: var(--color-secondary-light-4); - --color-project-board-dark-label: #0e1114; - --color-project-board-light-label: #eaeef2; --color-caret: var(--color-text-dark); --color-reaction-bg: #0000170a; --color-reaction-hover-bg: var(--color-primary-light-5); diff --git a/web_src/js/components/ContextPopup.vue b/web_src/js/components/ContextPopup.vue index ac6a8f3bb6..70b12dcb28 100644 --- a/web_src/js/components/ContextPopup.vue +++ b/web_src/js/components/ContextPopup.vue @@ -1,7 +1,6 @@ <script> import {SvgIcon} from '../svg.js'; -import {useLightTextOnBackground} from '../utils/color.js'; -import tinycolor from 'tinycolor2'; +import {contrastColor} from '../utils/color.js'; import {GET} from '../modules/fetch.js'; import {emojiHTML} from '../features/emoji.js'; import {htmlEscape} from 'escape-goat'; @@ -61,20 +60,13 @@ export default { }, labels() { - return this.issue.labels.map((label) => { - let textColor; - const {r, g, b} = tinycolor(label.color).toRgb(); - if (useLightTextOnBackground(r, g, b)) { - textColor = '#eeeeee'; - } else { - textColor = '#111111'; - } - label.name = htmlEscape(label.name); - label.name = label.name.replaceAll(/:[-+\w]+:/g, (emoji) => { + return this.issue.labels.map((label) => ({ + name: htmlEscape(label.name).replaceAll(/:[-+\w]+:/g, (emoji) => { return emojiHTML(emoji.substring(1, emoji.length - 1)); - }); - return {name: label.name, color: `#${label.color}`, textColor}; - }); + }), + color: `#${label.color}`, + textColor: contrastColor(`#${label.color}`), + })); }, }, mounted() { @@ -114,7 +106,7 @@ export default { <p><small>{{ issue.repository.full_name }} on {{ createdAt }}</small></p> <p><svg-icon :name="icon" :class="['text', color]"/> <strong>{{ issue.title }}</strong> #{{ issue.number }}</p> <p>{{ body }}</p> - <div> + <div class="labels-list"> <!-- eslint-disable-next-line vue/no-v-html --> <div v-for="label in labels" :key="label.name" class="ui label" :style="{ color: label.textColor, backgroundColor: label.color }" v-html="label.name"/> </div> diff --git a/web_src/js/features/repo-projects.js b/web_src/js/features/repo-projects.js index 80e945a0f2..a869c24c82 100644 --- a/web_src/js/features/repo-projects.js +++ b/web_src/js/features/repo-projects.js @@ -1,8 +1,8 @@ import $ from 'jquery'; -import {useLightTextOnBackground} from '../utils/color.js'; -import tinycolor from 'tinycolor2'; +import {contrastColor} from '../utils/color.js'; import {createSortable} from '../modules/sortable.js'; import {POST, DELETE, PUT} from '../modules/fetch.js'; +import tinycolor from 'tinycolor2'; function updateIssueCount(cards) { const parent = cards.parentElement; @@ -65,14 +65,11 @@ async function initRepoProjectSortable() { boardColumns = mainBoard.getElementsByClassName('project-column'); for (let i = 0; i < boardColumns.length; i++) { const column = boardColumns[i]; - if (parseInt($(column).data('sorting')) !== i) { + if (parseInt(column.getAttribute('data-sorting')) !== i) { try { - await PUT($(column).data('url'), { - data: { - sorting: i, - color: rgbToHex(window.getComputedStyle($(column)[0]).backgroundColor), - }, - }); + const bgColor = column.style.backgroundColor; // will be rgb() string + const color = bgColor ? tinycolor(bgColor).toHexString() : ''; + await PUT(column.getAttribute('data-url'), {data: {sorting: i, color}}); } catch (error) { console.error(error); } @@ -102,16 +99,10 @@ export function initRepoProject() { for (const modal of document.getElementsByClassName('edit-project-column-modal')) { const projectHeader = modal.closest('.project-column-header'); - const projectTitleLabel = projectHeader?.querySelector('.project-column-title'); + const projectTitleLabel = projectHeader?.querySelector('.project-column-title-label'); const projectTitleInput = modal.querySelector('.project-column-title-input'); const projectColorInput = modal.querySelector('#new_project_column_color'); const boardColumn = modal.closest('.project-column'); - const bgColor = boardColumn?.style.backgroundColor; - - if (bgColor) { - setLabelColor(projectHeader, rgbToHex(bgColor)); - } - modal.querySelector('.edit-project-column-button')?.addEventListener('click', async function (e) { e.preventDefault(); try { @@ -126,10 +117,21 @@ export function initRepoProject() { } finally { projectTitleLabel.textContent = projectTitleInput?.value; projectTitleInput.closest('form')?.classList.remove('dirty'); - if (projectColorInput?.value) { - setLabelColor(projectHeader, projectColorInput.value); + const dividers = boardColumn.querySelectorAll(':scope > .divider'); + if (projectColorInput.value) { + const color = contrastColor(projectColorInput.value); + boardColumn.style.setProperty('background', projectColorInput.value, 'important'); + boardColumn.style.setProperty('color', color, 'important'); + for (const divider of dividers) { + divider.style.setProperty('color', color); + } + } else { + boardColumn.style.removeProperty('background'); + boardColumn.style.removeProperty('color'); + for (const divider of dividers) { + divider.style.removeProperty('color'); + } } - boardColumn.style = `background: ${projectColorInput.value} !important`; $('.ui.modal').modal('hide'); } }); @@ -182,24 +184,3 @@ export function initRepoProject() { createNewColumn(url, $columnTitle, $projectColorInput); }); } - -function setLabelColor(label, color) { - const {r, g, b} = tinycolor(color).toRgb(); - if (useLightTextOnBackground(r, g, b)) { - label.classList.remove('dark-label'); - label.classList.add('light-label'); - } else { - label.classList.remove('light-label'); - label.classList.add('dark-label'); - } -} - -function rgbToHex(rgb) { - rgb = rgb.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+).*\)$/); - return `#${hex(rgb[1])}${hex(rgb[2])}${hex(rgb[3])}`; -} - -function hex(x) { - const hexDigits = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']; - return Number.isNaN(x) ? '00' : hexDigits[(x - x % 16) / 16] + hexDigits[x % 16]; -} diff --git a/web_src/js/utils/color.js b/web_src/js/utils/color.js index 0ba6af49ee..198f97c454 100644 --- a/web_src/js/utils/color.js +++ b/web_src/js/utils/color.js @@ -1,23 +1,21 @@ -// Check similar implementation in modules/util/color.go and keep synchronization -// Return R, G, B values defined in reletive luminance -function getLuminanceRGB(channel) { - const sRGB = channel / 255; - return (sRGB <= 0.03928) ? sRGB / 12.92 : ((sRGB + 0.055) / 1.055) ** 2.4; +import tinycolor from 'tinycolor2'; + +// Returns relative luminance for a SRGB color - https://en.wikipedia.org/wiki/Relative_luminance +// Keep this in sync with modules/util/color.go +function getRelativeLuminance(color) { + const {r, g, b} = tinycolor(color).toRgb(); + return (0.2126729 * r + 0.7151522 * g + 0.072175 * b) / 255; } -// Reference from: https://www.w3.org/WAI/GL/wiki/Relative_luminance -function getLuminance(r, g, b) { - const R = getLuminanceRGB(r); - const G = getLuminanceRGB(g); - const B = getLuminanceRGB(b); - return 0.2126 * R + 0.7152 * G + 0.0722 * B; +function useLightText(backgroundColor) { + return getRelativeLuminance(backgroundColor) < 0.453; } -// Reference from: https://firsching.ch/github_labels.html -// In the future WCAG 3 APCA may be a better solution. -// Check if text should use light color based on RGB of background -export function useLightTextOnBackground(r, g, b) { - return getLuminance(r, g, b) < 0.453; +// Given a background color, returns a black or white foreground color that the highest +// contrast ratio. In the future, the APCA contrast function, or CSS `contrast-color` will be better. +// https://github.com/color-js/color.js/blob/eb7b53f7a13bb716ec8b28c7a56f052cd599acd9/src/contrast/APCA.js#L42 +export function contrastColor(backgroundColor) { + return useLightText(backgroundColor) ? '#fff' : '#000'; } function resolveColors(obj) { diff --git a/web_src/js/utils/color.test.js b/web_src/js/utils/color.test.js index e129109ef0..fee9afc776 100644 --- a/web_src/js/utils/color.test.js +++ b/web_src/js/utils/color.test.js @@ -1,21 +1,22 @@ -import {useLightTextOnBackground} from './color.js'; +import {contrastColor} from './color.js'; -test('useLightTextOnBackground', () => { - expect(useLightTextOnBackground(215, 58, 74)).toBe(true); - expect(useLightTextOnBackground(0, 117, 202)).toBe(true); - expect(useLightTextOnBackground(207, 211, 215)).toBe(false); - expect(useLightTextOnBackground(162, 238, 239)).toBe(false); - expect(useLightTextOnBackground(112, 87, 255)).toBe(true); - expect(useLightTextOnBackground(0, 134, 114)).toBe(true); - expect(useLightTextOnBackground(228, 230, 105)).toBe(false); - expect(useLightTextOnBackground(216, 118, 227)).toBe(true); - expect(useLightTextOnBackground(255, 255, 255)).toBe(false); - expect(useLightTextOnBackground(43, 134, 133)).toBe(true); - expect(useLightTextOnBackground(43, 135, 134)).toBe(true); - expect(useLightTextOnBackground(44, 135, 134)).toBe(true); - expect(useLightTextOnBackground(59, 182, 179)).toBe(true); - expect(useLightTextOnBackground(124, 114, 104)).toBe(true); - expect(useLightTextOnBackground(126, 113, 108)).toBe(true); - expect(useLightTextOnBackground(129, 112, 109)).toBe(true); - expect(useLightTextOnBackground(128, 112, 112)).toBe(true); +test('contrastColor', () => { + expect(contrastColor('#d73a4a')).toBe('#fff'); + expect(contrastColor('#0075ca')).toBe('#fff'); + expect(contrastColor('#cfd3d7')).toBe('#000'); + expect(contrastColor('#a2eeef')).toBe('#000'); + expect(contrastColor('#7057ff')).toBe('#fff'); + expect(contrastColor('#008672')).toBe('#fff'); + expect(contrastColor('#e4e669')).toBe('#000'); + expect(contrastColor('#d876e3')).toBe('#000'); + expect(contrastColor('#ffffff')).toBe('#000'); + expect(contrastColor('#2b8684')).toBe('#fff'); + expect(contrastColor('#2b8786')).toBe('#fff'); + expect(contrastColor('#2c8786')).toBe('#000'); + expect(contrastColor('#3bb6b3')).toBe('#000'); + expect(contrastColor('#7c7268')).toBe('#fff'); + expect(contrastColor('#7e716c')).toBe('#fff'); + expect(contrastColor('#81706d')).toBe('#fff'); + expect(contrastColor('#807070')).toBe('#fff'); + expect(contrastColor('#84b6eb')).toBe('#000'); });