From 1fbe0c1a2089deccfb5973e22733add419147941 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 30 Mar 2024 20:36:28 +0300 Subject: [PATCH] Fix and rewrite contrast color calculation, fix project-related bugs Also includes https://github.com/go-gitea/gitea/pull/30183 --- modules/templates/helper.go | 6 +- modules/templates/util_render.go | 11 +-- 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 | 20 ++---- web_src/js/features/repo-projects.js | 86 ++++++++++-------------- web_src/js/utils/color.js | 30 ++++----- web_src/js/utils/color.test.js | 39 +++++------ 14 files changed, 148 insertions(+), 203 deletions(-) diff --git a/modules/templates/helper.go b/modules/templates/helper.go index 24520647499a2..e69adbd7057c1 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 d1c9b082fab00..0b53965f25805 100644 --- a/modules/templates/util_render.go +++ b/modules/templates/util_render.go @@ -123,16 +123,10 @@ 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 - if util.UseLightTextOnBackground(r, g, b) { - textColor = "#eee" - } - description := emoji.ReplaceAliases(template.HTMLEscapeString(label.Description)) if label.IsArchived() { @@ -153,7 +147,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) @@ -162,6 +156,7 @@ func RenderLabel(ctx context.Context, locale translation.Locale, label *issues_m darkenFactor := math.Max(luminance-darken, 0.0) / math.Max(luminance, 1.0/255.0) lightenFactor := math.Min(luminance+lighten, 1.0) / math.Max(luminance, 1.0/255.0) + 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 240b045c287a9..9c520dce786e2 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 d96ac36730ab6..be6e6b122a515 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 b45174b086a7c..0944ba900d8b8 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -66,13 +66,13 @@
{{range .Columns}} -
+
{{.NumIssues ctx}}
- {{.Title}} + {{.Title}}
{{if $canWriteProject}} {{end}}
- -
- +
{{range (index $.IssuesMap .ID)}}
diff --git a/web_src/css/features/projects.css b/web_src/css/features/projects.css index 30df994c38a60..ff2a65e03a51d 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 496a63d2b8ae6..a53d0a98e7303 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -2371,8 +2371,21 @@ 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 fe8231d7182f4..77905956f0e57 100644 --- a/web_src/css/repo/issue-list.css +++ b/web_src/css/repo/issue-list.css @@ -34,23 +34,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 ed6718e40c953..c74f334c2dfc3 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 b10ad7d840d85..01dd8ba4f7b7d 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 d87eb1a180ea7..65a6089522184 100644 --- a/web_src/js/components/ContextPopup.vue +++ b/web_src/js/components/ContextPopup.vue @@ -1,7 +1,6 @@