mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-25 09: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 */
|
/* eslint-disable ghost/ember/alias-model-in-controller */
|
||||||
import Controller from '@ember/controller';
|
import Controller from '@ember/controller';
|
||||||
import {computed} from '@ember/object';
|
import {computed} from '@ember/object';
|
||||||
|
import {
|
||||||
|
contrast,
|
||||||
|
darkenToContrastThreshold,
|
||||||
|
hexToRgb,
|
||||||
|
lightenToContrastThreshold,
|
||||||
|
rgbToHex
|
||||||
|
} from 'ghost-admin/utils/color';
|
||||||
import {inject as service} from '@ember/service';
|
import {inject as service} from '@ember/service';
|
||||||
|
|
||||||
export default Controller.extend({
|
export default Controller.extend({
|
||||||
@ -8,6 +15,7 @@ export default Controller.extend({
|
|||||||
customViews: service(),
|
customViews: service(),
|
||||||
config: service(),
|
config: service(),
|
||||||
dropdown: service(),
|
dropdown: service(),
|
||||||
|
feature: service(),
|
||||||
router: service(),
|
router: service(),
|
||||||
session: service(),
|
session: service(),
|
||||||
settings: service(),
|
settings: service(),
|
||||||
@ -30,5 +38,33 @@ export default Controller.extend({
|
|||||||
|
|
||||||
return (router.currentRouteName !== 'error404' || session.isAuthenticated)
|
return (router.currentRouteName !== 'error404' || session.isAuthenticated)
|
||||||
&& !router.currentRouteName.match(/(signin|signup|setup|reset)/);
|
&& !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}}
|
{{#if this.settings.accentColor}}
|
||||||
<style>
|
<style>
|
||||||
.koenig-editor__editor a:not([class]) {
|
.koenig-editor__editor a:not([class]) {
|
||||||
color: {{this.settings.accentColor}};
|
color: {{this.adjustedAccentColor}};
|
||||||
}
|
}
|
||||||
.koenig-editor__editor blockquote {
|
.koenig-editor__editor blockquote {
|
||||||
border-left: 0.25rem solid {{this.settings.accentColor}};
|
border-left: 0.25rem solid {{this.adjustedAccentColor}};
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
{{/if}}
|
{{/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