mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-25 20:03:12 +03:00
🐛 Fixed link contrast in editor with very light/dark accent colors (#1870)
refs https://github.com/TryGhost/Team/issues/551 refs https://github.com/TryGhost/Ghost/issues/12767#issuecomment-800177254 - calculate contrast color of accent color against light/dark mode background color - lighten (dark mode) or darken (light mode) the accent color used in the editor to ensure it has enough contrast to be legible
This commit is contained in:
parent
6d324e31f1
commit
7286ae9fcf
@ -1,6 +1,13 @@
|
||||
/* eslint-disable ghost/ember/alias-model-in-controller */
|
||||
import Controller from '@ember/controller';
|
||||
import {computed} from '@ember/object';
|
||||
import {
|
||||
contrast,
|
||||
darkenToContrastThreshold,
|
||||
hexToRgb,
|
||||
lightenToContrastThreshold,
|
||||
rgbToHex
|
||||
} from 'ghost-admin/utils/color';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default Controller.extend({
|
||||
@ -8,6 +15,7 @@ export default Controller.extend({
|
||||
customViews: service(),
|
||||
config: service(),
|
||||
dropdown: service(),
|
||||
feature: service(),
|
||||
router: service(),
|
||||
session: service(),
|
||||
settings: service(),
|
||||
@ -30,5 +38,33 @@ export default Controller.extend({
|
||||
|
||||
return (router.currentRouteName !== 'error404' || session.isAuthenticated)
|
||||
&& !router.currentRouteName.match(/(signin|signup|setup|reset)/);
|
||||
}),
|
||||
|
||||
adjustedAccentColor: computed('settings.accentColor', 'feature.nightShift', function () {
|
||||
const accentColor = this.settings.get('accentColor');
|
||||
const nightShift = this.feature.get('nightShift');
|
||||
// hardcoded background colors because
|
||||
// grabbing color from .gh-main with getComputedStyle always returns #ffffff
|
||||
const backgroundColor = nightShift ? '#151719' : '#ffffff';
|
||||
|
||||
const accentRgb = hexToRgb(accentColor);
|
||||
const backgroundRgb = hexToRgb(backgroundColor);
|
||||
|
||||
// WCAG contrast. 1 = lowest contrast, 21 = highest contrast
|
||||
const accentContrast = contrast(backgroundRgb, accentRgb);
|
||||
|
||||
if (accentContrast > 2) {
|
||||
return accentColor;
|
||||
}
|
||||
|
||||
let adjustedAccentRgb = accentRgb;
|
||||
|
||||
if (nightShift) {
|
||||
adjustedAccentRgb = lightenToContrastThreshold(accentRgb, backgroundRgb, 2);
|
||||
} else {
|
||||
adjustedAccentRgb = darkenToContrastThreshold(accentRgb, backgroundRgb, 2);
|
||||
}
|
||||
|
||||
return rgbToHex(adjustedAccentRgb);
|
||||
})
|
||||
});
|
||||
|
@ -37,10 +37,10 @@
|
||||
{{#if this.settings.accentColor}}
|
||||
<style>
|
||||
.koenig-editor__editor a:not([class]) {
|
||||
color: {{this.settings.accentColor}};
|
||||
color: {{this.adjustedAccentColor}};
|
||||
}
|
||||
.koenig-editor__editor blockquote {
|
||||
border-left: 0.25rem solid {{this.settings.accentColor}};
|
||||
border-left: 0.25rem solid {{this.adjustedAccentColor}};
|
||||
}
|
||||
</style>
|
||||
{{/if}}
|
||||
|
152
ghost/admin/app/utils/color.js
Normal file
152
ghost/admin/app/utils/color.js
Normal file
@ -0,0 +1,152 @@
|
||||
export function hexToRgb(hex) {
|
||||
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
|
||||
hex = hex.replace(shorthandRegex, function (m, r, g, b) {
|
||||
return r + r + g + g + b + b;
|
||||
});
|
||||
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result ? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16)
|
||||
} : null;
|
||||
}
|
||||
|
||||
export function rgbToHex({r, g, b}) {
|
||||
function hex(x) {
|
||||
return ('0' + parseInt(x).toString(16)).slice(-2);
|
||||
}
|
||||
return '#' + hex(r) + hex(g) + hex(b);
|
||||
}
|
||||
|
||||
// returns {h,s,l} with range [0,1] for maximum precision in conversion
|
||||
export function rgbToHsl({r, g, b}) {
|
||||
r = r / 255;
|
||||
g = g / 255;
|
||||
b = b / 255;
|
||||
|
||||
const max = Math.max(r, g, b);
|
||||
const min = Math.min(r, g, b);
|
||||
|
||||
let h = 0;
|
||||
let s = 0;
|
||||
const l = (max + min) / 2;
|
||||
|
||||
if (max === min) {
|
||||
h = s = 0; // achromatic
|
||||
} else {
|
||||
const delta = max - min;
|
||||
|
||||
s = l > 0.5
|
||||
? delta / (2 - max - min)
|
||||
: delta / (max + min);
|
||||
|
||||
switch (max) {
|
||||
case r: h = (g - b) / delta + (g < b ? 6 : 0); break;
|
||||
case g: h = (b - r) / delta + 2; break;
|
||||
case b: h = (r - g) / delta + 4; break;
|
||||
}
|
||||
|
||||
h = h / 6;
|
||||
}
|
||||
|
||||
return {h, s, l};
|
||||
}
|
||||
|
||||
// expects {h,s,l} in range [0,1], returns {r,g,b} in the range [0,255]
|
||||
export function hslToRgb({h, s, l}) {
|
||||
let r, g, b;
|
||||
|
||||
function hue2rgb(p, q, t) {
|
||||
if (t < 0) {
|
||||
t += 1;
|
||||
}
|
||||
if (t > 1) {
|
||||
t -= 1;
|
||||
}
|
||||
if (t < 1 / 6) {
|
||||
return p + (q - p) * 6 * t;
|
||||
}
|
||||
if (t < 1 / 2) {
|
||||
return q;
|
||||
}
|
||||
if (t < 2 / 3) {
|
||||
return p + (q - p) * (2 / 3 - t) * 6;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
if (s === 0) {
|
||||
r = g = b = l;
|
||||
} else {
|
||||
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
||||
const p = 2 * l - q;
|
||||
|
||||
r = hue2rgb(p, q, h + 1 / 3);
|
||||
g = hue2rgb(p, q, h);
|
||||
b = hue2rgb(p, q, h - 1 / 3);
|
||||
}
|
||||
|
||||
r = r * 255;
|
||||
g = g * 255;
|
||||
b = b * 255;
|
||||
|
||||
return {r, g, b};
|
||||
}
|
||||
|
||||
// https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
|
||||
export function luminance({r, g, b}) {
|
||||
const a = [r, g, b].map(function (v) {
|
||||
v = v / 255;
|
||||
|
||||
return v <= 0.03928
|
||||
? v / 12.92
|
||||
: Math.pow((v + 0.055) / 1.055, 2.4);
|
||||
});
|
||||
|
||||
return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722;
|
||||
}
|
||||
|
||||
// https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
|
||||
export function contrast(rgb1, rgb2) {
|
||||
const lum1 = luminance(rgb1);
|
||||
const lum2 = luminance(rgb2);
|
||||
const brightest = Math.max(lum1, lum2);
|
||||
const darkest = Math.min(lum1, lum2);
|
||||
|
||||
return (brightest + 0.05) / (darkest + 0.05);
|
||||
}
|
||||
|
||||
export function lightenToContrastThreshold(foregroundRgb, backgroundRgb, contrastThreshold) {
|
||||
let newRgb = foregroundRgb;
|
||||
|
||||
while (contrast(newRgb, backgroundRgb) < contrastThreshold) {
|
||||
let {h,s,l} = rgbToHsl(newRgb);
|
||||
|
||||
if (l >= 1) {
|
||||
break;
|
||||
}
|
||||
|
||||
l = Math.min(l + 0.05, 1);
|
||||
newRgb = hslToRgb({h,s,l});
|
||||
}
|
||||
|
||||
return newRgb;
|
||||
}
|
||||
|
||||
export function darkenToContrastThreshold(foregroundRgb, backgroundRgb, contrastThreshold) {
|
||||
let newRgb = foregroundRgb;
|
||||
|
||||
while (contrast(newRgb, backgroundRgb) < contrastThreshold) {
|
||||
let {h,s,l} = rgbToHsl(newRgb);
|
||||
|
||||
if (l <= 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
l = Math.max(l - 0.05, 0);
|
||||
newRgb = hslToRgb({h,s,l});
|
||||
}
|
||||
|
||||
return newRgb;
|
||||
}
|
Loading…
Reference in New Issue
Block a user