diff options
Diffstat (limited to 'web_src')
-rw-r--r-- | web_src/css/features/projects.css | 27 | ||||
-rw-r--r-- | web_src/css/repo.css | 15 | ||||
-rw-r--r-- | web_src/css/repo/issue-list.css | 17 | ||||
-rw-r--r-- | web_src/css/themes/theme-gitea-dark.css | 2 | ||||
-rw-r--r-- | web_src/css/themes/theme-gitea-light.css | 2 | ||||
-rw-r--r-- | web_src/js/components/ContextPopup.vue | 20 | ||||
-rw-r--r-- | web_src/js/features/repo-projects.js | 61 | ||||
-rw-r--r-- | web_src/js/utils/color.js | 30 | ||||
-rw-r--r-- | web_src/js/utils/color.test.js | 39 |
9 files changed, 87 insertions, 126 deletions
diff --git a/web_src/css/features/projects.css b/web_src/css/features/projects.css index cec5e6fc64..e23c146748 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 653af379d5..c50d13a174 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -2273,8 +2273,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 fe8231d718..77905956f0 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 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 d87eb1a180..65a6089522 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'; const {appSubUrl, i18n} = window.config; @@ -59,16 +58,11 @@ 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'; - } - return {name: label.name, color: `#${label.color}`, textColor}; - }); + return this.issue.labels.map((label) => ({ + name: label.name, + color: `#${label.color}`, + textColor: contrastColor(`#${label.color}`), + })); }, }, mounted() { @@ -108,7 +102,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"> <div v-for="label in labels" :key="label.name" 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'); }); |