summaryrefslogtreecommitdiffstats
path: root/web_src
diff options
context:
space:
mode:
Diffstat (limited to 'web_src')
-rw-r--r--web_src/css/features/projects.css27
-rw-r--r--web_src/css/repo.css15
-rw-r--r--web_src/css/repo/issue-list.css17
-rw-r--r--web_src/css/themes/theme-gitea-dark.css2
-rw-r--r--web_src/css/themes/theme-gitea-light.css2
-rw-r--r--web_src/js/components/ContextPopup.vue20
-rw-r--r--web_src/js/features/repo-projects.js61
-rw-r--r--web_src/js/utils/color.js30
-rw-r--r--web_src/js/utils/color.test.js39
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');
});